././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740697287.4211738 osc_placement-4.6.0/0000775000175000017500000000000000000000000014360 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/.coveragerc0000664000175000017500000000011200000000000016473 0ustar00zuulzuul00000000000000[run] branch = True source = osc_placement [report] ignore_errors = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/.mailmap0000664000175000017500000000013100000000000015774 0ustar00zuulzuul00000000000000# Format is: # # ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/.stestr.conf0000664000175000017500000000007200000000000016630 0ustar00zuulzuul00000000000000[DEFAULT] test_path=./osc_placement/tests/unit top_dir=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/.zuul.yaml0000664000175000017500000000212100000000000016315 0ustar00zuulzuul00000000000000- project: templates: - openstack-python3-jobs - check-requirements - publish-openstack-docs-pti - release-notes-jobs-python3 - openstackclient-plugin-jobs check: jobs: - openstack-tox-functional-py39: required-projects: - openstack/placement - openstack-tox-functional-py310: required-projects: - openstack/placement - openstack-tox-functional-py311: required-projects: - openstack/placement - openstack-tox-functional-py312: required-projects: - openstack/placement gate: jobs: - openstack-tox-functional-py39: required-projects: - openstack/placement - openstack-tox-functional-py310: required-projects: - openstack/placement - openstack-tox-functional-py311: required-projects: - openstack/placement - openstack-tox-functional-py312: required-projects: - openstack/placement ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697287.0 osc_placement-4.6.0/AUTHORS0000664000175000017500000000250700000000000015434 0ustar00zuulzuul0000000000000098k <18552437190@163.com> Ameed Ashour Andreas Jaeger Andrey Volkov Balazs Gibizer Balazs Gibizer Balazs Gibizer Bence Romsics Chen Chris Dent Corey Bryant Doug Hellmann Fan Zhang Ghanshyam Mann Ian Wienand Lajos Katona Matt Riedemann OpenStack Release Bot Pawel Baclawski Roman Podoliaka Sean Mooney Stephen Finucane Takashi Kajinami Takashi Kajinami Takashi NATSUME Takashi Natsume Tetsuro Nakamura Xiaopengli Yikun Jiang caoyuan l Luis likui liyou01 melanie witt shupeng <15050873171@163.com> songwenping ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/CONTRIBUTING.rst0000664000175000017500000000125500000000000017024 0ustar00zuulzuul00000000000000If 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 If you already have a good understanding of how the system works and your OpenStack accounts are set up, you can skip to the development workflow section of this documentation to learn how changes to OpenStack should be submitted for review via the Gerrit tool: http://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. Bugs should be filed on StoryBoard, not GitHub: https://storyboard.openstack.org/#!/project/openstack/osc-placement ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697287.0 osc_placement-4.6.0/ChangeLog0000664000175000017500000001515400000000000016140 0ustar00zuulzuul00000000000000CHANGES ======= 4.6.0 ----- * Update python classifier as per the 2025.1 cycle testing runtime * Update master for stable/2024.2 4.5.0 ----- * Remove use of distutils 4.4.0 ----- * Replace simplejson by built-in json * Make python 3.12 functional job voting * Update testing of python versions * reno: Update master for unmaintained/zed * Update master for stable/2024.1 * reno: Update master for unmaintained/xena * reno: Update master for unmaintained/wallaby * reno: Update master for unmaintained/victoria * Update bug tracker url 4.3.0 ----- * reno: Update master for unmaintained/yoga * tox: Drop envdir * Bump hacking * add pyproject.toml to support pip 23.1 * Update master for stable/2023.2 4.2.0 ----- * Update master for stable/2023.1 4.1.0 ----- * Use pypi released version of placement in functional tests * Make tox.ini tox 4.0.0 compatible * Update gate jobs as per the 2023.1 cycle testing runtime * Switch to 2023.1 Python3 unit tests and generic template name * Update master for stable/zed 4.0.0 ----- * Support microversion 1.39 * Add Python3 zed unit tests * Change minversion of tox to 3.18.0 * Update master for stable/xena * Update master for stable/yoga 3.2.0 ----- * Add Python3 yoga unit tests * Updating python testing as per Yoga testing runtime * Replace deprecated assertRaisesRegexp * Remove usage of six * Remove usage of six 3.1.1 ----- * Fix allocation show / unset with empty allocation * Repro allocation show bug with empty allocation 3.1.0 ----- * Add support for microversion 1.38 consumer types 3.0.1 ----- * default to max version when no session 3.0.0 ----- * Verify result for inventory set --dry-run * Add "--resource-class" to allocation unset * Note env OS\_PLACEMENT\_API\_VERSION support * Switch default to use latest microversion * Support auto-negotiated microversion * setup.cfg: Replace dashes with underscores * Mark microversion 1.37 supported * Update master for stable/wallaby * Add openstackclient-plugin-jobs 2.2.0 ----- * remove unicode from code * remove unicode from code * Add functional-py39 tox target * Use TOX\_CONSTRAINTS\_FILE * Add py38 package metadata * Add Python3 wallaby unit tests * Update master for stable/victoria * Support granular allocation candidate list * Support multiple member\_of query parameter * Add to functional-py38 test to check/gate 2.1.0 ----- * Remove six.PY3 * Include usage in 'inventory list', 'inventory show' * tox: Trivial cleanup * tox: Add functional-pyNN targets * trivial: Fix formatting of command help texts * Update command help information * Remove Babel * Use unittest.mock instead of third party mock * Switch to newer openstackdocstheme and reno versions * Fix hacking min version to 3.0.1 * Cleanup py27 support * Add Python3 victoria unit tests * Update master for stable/ussuri 2.0.0 ----- * Improve tests for warning messages * Remove redundant functional-py3\* tox environments 1.8.0 ----- * Follow up to I627bfd1ff699d075028da6afafbe7fb9b2f13058 * Provide a useful message in case of 5xx error * [ussuri][goal] Drop python 2.7 support and testing * Add support for microversion 1.28 in allocation set * Add resource provider allocation unset command * Fix deps for using venv tox target to create a release note * gitignore: Ignore .stestr directory * docs: Misc cleanups * Fixups for pdf docs * Be explicit about auth type in functional tests * Use os-endpoint instead of os-url for functional tests * Update master for stable/train * Build pdf docs * Update osc-placement bug link in README 1.7.0 ----- * Follow up for Ib0cbb58d0adbbcfe83ee48d2ff6c9af1a516a7ae * Add --dry-run option to 'resource provider inventory set' * Add --amend option to 'resource provider inventory set' * Add --aggregate option to 'resource provider inventory set' * Cap sphinx for py2 to match global requirements 1.6.0 ----- * Update api-ref location * Add Python 3 Train unit tests * Replace git.openstack.org URLs with opendev.org URLs * Add support for 1.22 microversion * Expose version error message generically * Dropping the py35 testing * Use PlacementFixture in functional tests * OpenDev Migration Patch * Improve aggregate version check error messages with min\_version * Fix the metavar on "resource provider aggregate set" * Only enable keystone and placement for functional test runs * Remove unused cruft from doc and releasenotes config * Indicate python 3.6 support in the classifiers * Use openstackdocstheme instead of oslosphinx * Update home-page * Replace openstack.org git:// URLs with https:// * Update master for stable/stein * Update bugs link in contributing doc * Microversion 1.21 support * Add support for 1.19 microversion 1.5.0 ----- * Add support for 1.18 microversion * Update tox and tests to work with modern setups * add python 3.7 unit test job 1.4.0 ----- * Enforce key-value'ness for 'allocation candidate list --resource' * tox: Hide deprecation warnings from stdlib * Update author-email in setup.cfg * add python 3.6 unit test job * switch documentation job to new PTI * import zuul job settings from project-config * Random names for functional tests * Add image link in README.rst * Update reno for stable/rocky * Resource provider examples 1.3.0 ----- * Allocation candidates parameter: required (v1.17) * Limit allocation candidates (v1.15, v1.16) * Add nested resource providers (v1.14) * Fix docstring for delete allocation method * New dict format of allocations (v1.11, v1.12) * CLI allocation candidates (v1.10) * Usages per project and user (v1.8, v1.9) * Fix the 1.6 release note format * Remove doc/build during tox -e docs 1.2.0 ----- * fix tox python3 overrides * Resource class set (v1.7) * Fix error message asserts in functional test * CLI for traits (v1.6) * Fix error message in test assert * RP delete inventories (v1.5) 1.1.0 ----- * RP list: member\_of and resources parameters (v1.3, v1.4) * Initialize 'result' variable in functional.base * Resolve nits from I552688b9ee32b719a576a7a9ed5e4d5aa31d7b3f * Do not depend on jenkins user in devstack gate * Add osc-placement-dsvm-functional-py3 job * Migrate legacy-osc-placement-dsvm-functional job in-tree * tox.ini settings for global constraints are out of date * Update doc link in README.rst * Update reno for stable/queens 1.0.0 ----- * Usage docs and initial release note for osc-placement * Address review comments from allocations patch * CLI for resource classes (v1.2) * CLI for aggregates (v1.1) * Address comments from original inventory patch * Add missing runtime requirements * CLI for usages * CLI for allocations * CLI for inventories * CLI for resource providers * Fix the bug link in the readme * tests: add a hook for functional testing in the gate 0.1.0 ----- * Initial commit ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/HACKING.rst0000664000175000017500000000022600000000000016156 0ustar00zuulzuul00000000000000osc-placement Style Commandments ================================ Read the OpenStack Style Commandments http://docs.openstack.org/developer/hacking/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/LICENSE0000664000175000017500000002363700000000000015400 0ustar00zuulzuul00000000000000 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. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/MANIFEST.in0000664000175000017500000000013600000000000016116 0ustar00zuulzuul00000000000000include AUTHORS include ChangeLog exclude .gitignore exclude .gitreview global-exclude *.pyc ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740697287.4211738 osc_placement-4.6.0/PKG-INFO0000644000175000017500000000347100000000000015460 0ustar00zuulzuul00000000000000Metadata-Version: 2.1 Name: osc-placement Version: 4.6.0 Summary: OpenStackClient plugin for the Placement service Home-page: https://docs.openstack.org/osc-placement/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Requires-Python: >=3.9 License-File: LICENSE Requires-Dist: pbr>=2.0.0 Requires-Dist: keystoneauth1>=3.3.0 Requires-Dist: osc-lib>=1.2.0 Requires-Dist: oslo.utils>=3.37.0 ============= osc-placement ============= .. image:: https://governance.openstack.org/tc/badges/osc-placement.svg :target: https://governance.openstack.org/tc/reference/tags/index.html OpenStackClient plugin for the Placement service This is an OpenStackClient plugin, that provides CLI for the Placement service. Python API binding is not implemented - Placement API consumers are encouraged to use the REST API directly, CLI is provided only for convenience of users. * Free software: Apache license * Documentation: https://docs.openstack.org/osc-placement/latest/index.html * Source: https://opendev.org/openstack/osc-placement * Bugs: https://launchpad.net/placement * Release notes: https://docs.openstack.org/releasenotes/osc-placement/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/README.rst0000664000175000017500000000135700000000000016055 0ustar00zuulzuul00000000000000============= osc-placement ============= .. image:: https://governance.openstack.org/tc/badges/osc-placement.svg :target: https://governance.openstack.org/tc/reference/tags/index.html OpenStackClient plugin for the Placement service This is an OpenStackClient plugin, that provides CLI for the Placement service. Python API binding is not implemented - Placement API consumers are encouraged to use the REST API directly, CLI is provided only for convenience of users. * Free software: Apache license * Documentation: https://docs.openstack.org/osc-placement/latest/index.html * Source: https://opendev.org/openstack/osc-placement * Bugs: https://launchpad.net/placement * Release notes: https://docs.openstack.org/releasenotes/osc-placement/ ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740697287.3931727 osc_placement-4.6.0/doc/0000775000175000017500000000000000000000000015125 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/doc/requirements.txt0000664000175000017500000000066200000000000020415 0ustar00zuulzuul00000000000000# 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 sphinx-feature-classification>=0.2.0 # Apache-2.0 openstackdocstheme>=2.2.1 # Apache-2.0 cliff>=2.14 # releasenotes reno>=3.1.0 # Apache-2.0 # redirect tests in docs whereto>=0.3.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740697287.3931727 osc_placement-4.6.0/doc/source/0000775000175000017500000000000000000000000016425 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740697287.397173 osc_placement-4.6.0/doc/source/cli/0000775000175000017500000000000000000000000017174 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/doc/source/cli/index.rst0000664000175000017500000000016300000000000021035 0ustar00zuulzuul00000000000000====================== Command Line Reference ====================== .. autoprogram-cliff:: openstack.placement.v1././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/doc/source/conf.py0000775000175000017500000000423500000000000017733 0ustar00zuulzuul00000000000000# -*- 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. # -- 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 = [ 'openstackdocstheme', 'sphinx.ext.autodoc', 'cliff.sphinxext' ] # The master toctree document. master_doc = 'index' # General information about the project. project = 'osc-placement' copyright = '2016, OpenStack Foundation' # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- 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 = 'openstackdocs' # See: https://docs.openstack.org/cliff/2.6.0/sphinxext.html autoprogram_cliff_application = 'openstack' autoprogram_cliff_ignored = [ '--help', '--format', '--column', '--max-width', '--fit-width', '--print-empty', '--prefix', '--noindent', '--quote'] # openstackdocstheme options openstackdocs_repo_name = 'openstack/osc-placement' openstackdocs_pdf_link = True openstackdocs_auto_name = False openstackdocs_use_storyboard = True # -- Options for LaTeX output ------------------------------------------------- # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_use_xindy = False latex_documents = [ ('index', 'doc-osc-placement.tex', 'osc-placement Documentation', 'OpenStack Foundation', 'manual'), ] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740697287.397173 osc_placement-4.6.0/doc/source/contributor/0000775000175000017500000000000000000000000020777 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/doc/source/contributor/index.rst0000664000175000017500000000011700000000000022637 0ustar00zuulzuul00000000000000============ Contributing ============ .. include:: ../../../CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/doc/source/index.rst0000664000175000017500000000076300000000000020274 0ustar00zuulzuul00000000000000============= osc-placement ============= .. We exclude the first few lines of the README since we don't need the badges (which don't render without extensions due to their SVG'ness) and we don't want two headers in this file .. include:: ../../README.rst :start-line: 7 Contents -------- .. toctree:: :maxdepth: 2 install/index contributor/index cli/index user/index .. only:: html Indices and tables ================== * :ref:`genindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740697287.397173 osc_placement-4.6.0/doc/source/install/0000775000175000017500000000000000000000000020073 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/doc/source/install/index.rst0000664000175000017500000000032000000000000021727 0ustar00zuulzuul00000000000000============ Installation ============ At the command line:: $ pip install osc-placement Or, if you have virtualenvwrapper installed:: $ mkvirtualenv osc-placement $ pip install osc-placement ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740697287.397173 osc_placement-4.6.0/doc/source/user/0000775000175000017500000000000000000000000017403 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/doc/source/user/index.rst0000664000175000017500000001627500000000000021257 0ustar00zuulzuul00000000000000================== User Documentation ================== This document describes various usage aspects of the *osc-placement* plugin including but not limited to command line examples and explanations, references and microversion usage. The full Placement API reference can be found here: https://docs.openstack.org/api-ref/placement/ Microversion usage ------------------ By default, all commands are run with the 1.0 Placement API version. One can specify a different microversion using the ``--os-placement-api-version`` option, for example:: $ openstack resource provider aggregate list --os-placement-api-version 1.1 dc43b86a-1261-4f8b-8330-28289fe754e3 +--------------------------------------+ | uuid | +--------------------------------------+ | 42896e0d-205d-4fe3-bd1e-100924931787 | | 42896e0d-205d-4fe3-bd1e-100924931788 | +--------------------------------------+ Alternatively, the ``OS_PLACEMENT_API_VERSION`` environment variable can be set, for example:: $ export OS_PLACEMENT_API_VERSION=1.1 $ openstack resource provider aggregate list dc43b86a-1261-4f8b-8330-28289fe754e3 +--------------------------------------+ | uuid | +--------------------------------------+ | 42896e0d-205d-4fe3-bd1e-100924931787 | | 42896e0d-205d-4fe3-bd1e-100924931788 | +--------------------------------------+ The Placement API version history can be found here: https://docs.openstack.org/nova/latest/user/placement.html#rest-api-version-history Examples -------- This section provides some common examples for command line usage. To see the list of available commands for resource providers, run:: $ openstack resource -h Resource providers ~~~~~~~~~~~~~~~~~~ Resource provider command subset have a basic CRUD interface. First, it can be easily created: .. code-block:: console $ p=$(openstack resource provider create Baremetal_node_01 -c uuid -f value) and renamed: .. code-block:: console $ openstack resource provider set $p --name Baremetal_node_02 +------------+--------------------------------------+ | Field | Value | +------------+--------------------------------------+ | uuid | c33caafc-b59c-46bc-b396-19f117171fec | | name | Baremetal_node_02 | | generation | 0 | +------------+--------------------------------------+ To get all allocations related to the resource provider use an ``--allocations`` option for the show command: .. code-block:: console $ openstack resource provider show $p --allocations +-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | Field | Value | +-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ | uuid | c33caafc-b59c-46bc-b396-19f117171fec | | name | Baremetal_node_02 | | generation | 4 | | allocations | {u'45f4ccf9-36e3-4d13-8c6b-80fd6c66a195': {u'resources': {u'VCPU': 1, u'MEMORY_MB': 512, u'DISK_GB': 10}}, u'2892c6f6-6ee7-4a34-aa20-156b8216de3c': {u'resources': {u'VCPU': 1, u'MEMORY_MB': 512, u'DISK_GB': 10}}} | +-------------+----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------+ A resource provider cannot be deleted if it has allocations, otherwise just issue: .. code-block:: console $ openstack resource provider delete $p and it is done. Allocations ~~~~~~~~~~~ One can set allocations against a resource provider for a given consumer multiple ways. When setting allocations against a single resource provider, it is generally easiest to use something like:: $ openstack resource provider allocation set 45f4ccf9-36e3-4d13-8c6b-80fd6c66a195 --allocation rp=dc43b86a-1261-4f8b-8330-28289fe754e3,DISK_GB=10,VCPU=1,MEMORY_MB=512 +--------------------------------------+------------+-------------------------------------------------+ | resource_provider | generation | resources | +--------------------------------------+------------+-------------------------------------------------+ | dc43b86a-1261-4f8b-8330-28289fe754e3 | 9 | {u'VCPU': 1, u'MEMORY_MB': 512, u'DISK_GB': 10} | +--------------------------------------+------------+-------------------------------------------------+ Alternatively one can set resource allocations against separate providers:: $ openstack resource provider allocation set 45f4ccf9-36e3-4d13-8c6b-80fd6c66a195 --allocation rp=dc43b86a-1261-4f8b-8330-28289fe754e3,VCPU=1,MEMORY_MB=512 --allocation rp=762746bc-de0d-47a7-b47a-a14028643663,DISK_GB=10 +--------------------------------------+------------+---------------------------------+ | resource_provider | generation | resources | +--------------------------------------+------------+---------------------------------+ | dc43b86a-1261-4f8b-8330-28289fe754e3 | 9 | {u'VCPU': 1, u'MEMORY_MB': 512} | | 762746bc-de0d-47a7-b47a-a14028643663 | 1 | {u'DISK_GB': 10} | +--------------------------------------+------------+---------------------------------+ In this scenario, the consumer, 45f4ccf9-36e3-4d13-8c6b-80fd6c66a195, has VCPU and MEMORY_MB allocations against one provider, dc43b86a-1261-4f8b-8330-28289fe754e3, and DISK_GB allocations against another provider, 762746bc-de0d-47a7-b47a-a14028643663. .. note:: When setting allocations for a consumer, the command overwrites any existing allocations for that consumer. So if you want to add or change one resource class allocation but leave other existing resource class allocations unchanged, you must also specify those other existing unchanged allocations so they are not removed. Resource classes ~~~~~~~~~~~~~~~~ There is a standard set of resource classes defined within the Placement service itself. These standard resource classes cannot be modified. Users can create and delete *custom* resource classes, which have a name prefix of ``CUSTOM_``. ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740697287.397173 osc_placement-4.6.0/osc_placement/0000775000175000017500000000000000000000000017174 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/__init__.py0000664000175000017500000000123500000000000021306 0ustar00zuulzuul00000000000000# -*- 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( 'osc_placement').version_string() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/http.py0000664000175000017500000000606700000000000020536 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 json import logging import keystoneauth1.exceptions.http as ks_exceptions import osc_lib.exceptions as exceptions from osc_placement import version _http_error_to_exc = { cls.http_status: cls for cls in exceptions.ClientException.__subclasses__() } LOG = logging.getLogger(__name__) @contextlib.contextmanager def _wrap_http_exceptions(): """Reraise osc-lib exceptions with detailed messages.""" try: yield except ks_exceptions.HttpError as exc: if 400 <= exc.http_status < 500: detail = json.loads(exc.response.content)['errors'][0]['detail'] msg = detail.split('\n')[-1].strip() exc_class = _http_error_to_exc.get(exc.http_status, exceptions.CommandError) raise exc_class(exc.http_status, msg) from exc else: raise class SessionClient(object): def __init__(self, session, ks_filter, api_version='1.0'): self.session = session self.ks_filter = ks_filter self.negotiate_api_version(api_version) def request(self, method, url, **kwargs): version = kwargs.pop('version', None) api_version = (self.ks_filter['service_type'] + ' ' + (version or self.api_version)) headers = kwargs.pop('headers', {}) headers.setdefault('OpenStack-API-Version', api_version) headers.setdefault('Accept', 'application/json') with _wrap_http_exceptions(): return self.session.request(url, method, headers=headers, endpoint_filter=self.ks_filter, **kwargs) def negotiate_api_version(self, api_version): """Set api_version to self. If negotiate version (only majorversion) is given, talk to server to pick up max microversion supported both by client and by server. """ if api_version not in version.NEGOTIATE_VERSIONS: self.api_version = api_version return client_ver = version.MAX_VERSION_NO_GAP self.api_version = client_ver resp = self.request('GET', '/', raise_exc=False) if resp.status_code == 406: server_ver = resp.json()['errors'][0]['max_version'] self.api_version = server_ver LOG.debug('Microversion %s not supported in server. ' 'Falling back to microversion %s', client_ver, server_ver) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/plugin.py0000664000175000017500000000346100000000000021050 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. "OpenStackClient plugin for Placement service" import logging from osc_lib import utils from osc_placement import version LOG = logging.getLogger(__name__) API_NAME = 'placement' API_VERSION_OPTION = 'os_placement_api_version' API_VERSIONS = {v: 'osc_placement.http.SessionClient' for v in version.SUPPORTED_VERSIONS} def make_client(instance): client_class = utils.get_client_class( API_NAME, instance._api_version[API_NAME], API_VERSIONS ) ks_filter = {'service_type': API_NAME, 'region_name': instance._region_name, 'interface': instance.interface} LOG.debug('Instantiating placement client: %s', client_class) return client_class(session=instance.session, ks_filter=ks_filter, api_version=instance._api_version[API_NAME]) def build_option_parser(parser): default = version.NEGOTIATE_VERSIONS[0] parser.add_argument( '--os-placement-api-version', metavar='', default=utils.env( 'OS_PLACEMENT_API_VERSION', default=default ), help='Placement API version, default=%s ' '(Env: OS_PLACEMENT_API_VERSION)' % default) return parser ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740697287.401173 osc_placement-4.6.0/osc_placement/resources/0000775000175000017500000000000000000000000021206 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/resources/__init__.py0000664000175000017500000000000000000000000023305 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/resources/aggregate.py0000664000175000017500000000751400000000000023515 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from osc_lib.command import command from osc_lib import exceptions from osc_placement import version BASE_URL = '/resource_providers/{uuid}/aggregates' FIELDS = ('uuid',) class SetAggregate(command.Lister, version.CheckerMixin): """Associate a list of aggregates with the resource provider. Each request cleans up previously associated resource provider aggregates entirely and sets the new ones. Passing empty aggregate UUID list will remove all associations with aggregates for the particular resource provider. This command requires at least ``--os-placement-api-version 1.1``. """ def get_parser(self, prog_name): parser = super(SetAggregate, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider' ) parser.add_argument( '--aggregate', metavar='', help='UUID of the aggregate. Specify multiple times to associate ' 'a resource provider with multiple aggregates.', action='append', default=[] ) parser.add_argument( '--generation', metavar='', type=int, help='The generation of resource provider. Must match the server-' 'side generation of the resource provider or the operation ' 'will fail.\n\n' 'This param requires at least ' '``--os-placement-api-version 1.19``.' ) return parser @version.check(version.ge('1.1')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL.format(uuid=parsed_args.uuid) aggregate = parsed_args.aggregate generation = None if 'generation' in parsed_args and parsed_args.generation is not None: self.check_version(version.ge('1.19')) generation = parsed_args.generation if self.compare_version(version.lt('1.19')): resp = http.request('PUT', url, json=aggregate).json() # Microversion 1.19 and beyond a generation argument is # required to write aggregates. elif generation is not None: data = {'aggregates': aggregate, 'resource_provider_generation': generation} resp = http.request('PUT', url, json=data).json() else: raise exceptions.CommandError( 'A generation must be specified.') return FIELDS, [[r] for r in resp['aggregates']] class ListAggregate(command.Lister): """List resource provider aggregates. This command requires at least ``--os-placement-api-version 1.1``. """ def get_parser(self, prog_name): parser = super(ListAggregate, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider' ) return parser @version.check(version.ge('1.1')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL.format(uuid=parsed_args.uuid) resp = http.request('GET', url).json() return FIELDS, [[r] for r in resp['aggregates']] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/resources/allocation.py0000664000175000017500000003656100000000000023720 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils from osc_placement import version BASE_URL = '/allocations' def parse_allocations(allocation_strings): allocations = {} for allocation_string in allocation_strings: if '=' not in allocation_string or ',' not in allocation_string: raise ValueError('Incorrect allocation string format') parsed = dict(kv.split('=') for kv in allocation_string.split(',')) if 'rp' not in parsed: raise ValueError('Resource provider parameter is required ' 'for allocation string') resources = {k: int(v) for k, v in parsed.items() if k != 'rp'} if parsed['rp'] not in allocations: allocations[parsed['rp']] = resources else: prev_rp = allocations[parsed['rp']] for resource, value in resources.items(): if resource in prev_rp and prev_rp[resource] != value: raise exceptions.CommandError( 'Conflict detected for ' 'resource provider {} resource class {}'.format( parsed['rp'], resource)) allocations[parsed['rp']].update(resources) return allocations class SetAllocation(command.Lister, version.CheckerMixin): """Replaces the set of resource allocation(s) for a given consumer. Note that this is a full replacement of the existing allocations. If you want to retain the existing allocations and add a new resource class allocation, you must specify all resource class allocations, old and new. From ``--os-placement-api-version 1.8`` it is required to specify ``--project-id`` and ``--user-id`` to set allocations. It is highly recommended to provide a ``--project-id`` and ``--user-id`` when setting allocations for accounting and data consistency reasons. Starting with ``--os-placement-api-version 1.12`` the API response contains the ``project_id`` and ``user_id`` of allocations which also appears in the CLI output. Starting with ``--os-placement-api-version 1.28`` a consumer generation is used which facilitates safe concurrent modification of an allocation. Starting with ``--os-placement-api-version 1.38`` it is required to specify ``--consumer-type`` to set allocations. It is helpful to provide a ``--consumer-type`` when setting allocations so that resource usages can be filtered on consumer types. """ def get_parser(self, prog_name): parser = super(SetAllocation, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the consumer' ) parser.add_argument( '--allocation', metavar='', action='append', default=[], help='Create (or update) an allocation of a resource class. ' 'Specify option multiple times to set multiple allocations.' ) parser.add_argument( '--project-id', metavar='project_id', help='ID of the consuming project. ' 'This option is required starting from ' '``--os-placement-api-version 1.8``.', required=self.compare_version(version.ge('1.8')) ) parser.add_argument( '--user-id', metavar='user_id', help='ID of the consuming user. ' 'This option is required starting from ' '``--os-placement-api-version 1.8``.', required=self.compare_version(version.ge('1.8')) ) parser.add_argument( '--consumer-type', metavar='consumer_type', help='The type of the consumer. ' 'This option is required starting from ' '``--os-placement-api-version 1.38``.', required=self.compare_version(version.ge('1.38')) ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL + '/' + parsed_args.uuid # Determine if we need to honor consumer generations. supports_consumer_generation = self.compare_version(version.ge('1.28')) if supports_consumer_generation: # Get the existing consumer generation via GET. payload = http.request('GET', url).json() consumer_generation = payload.get('consumer_generation') allocations = parse_allocations(parsed_args.allocation) if not allocations: raise exceptions.CommandError( 'At least one resource allocation must be specified') if self.compare_version(version.ge('1.12')): allocations = { rp: {'resources': resources} for rp, resources in allocations.items()} else: allocations = [ {'resource_provider': {'uuid': rp}, 'resources': resources} for rp, resources in allocations.items()] payload = {'allocations': allocations} # Include consumer_generation for 1.28+. Note that if this is the # first set of allocations the consumer_generation will be None. if supports_consumer_generation: payload['consumer_generation'] = consumer_generation if self.compare_version(version.ge('1.8')): payload['project_id'] = parsed_args.project_id payload['user_id'] = parsed_args.user_id elif parsed_args.project_id or parsed_args.user_id: self.log.warning('--project-id and --user-id options do not ' 'affect allocation for ' '--os-placement-api-version less than 1.8') if self.compare_version(version.ge('1.38')): payload['consumer_type'] = parsed_args.consumer_type elif parsed_args.consumer_type: self.log.warning('--consumer-type option does not affect ' 'allocation for --os-placement-api-version less ' 'than 1.38') http.request('PUT', url, json=payload) resp = http.request('GET', url).json() per_provider = resp['allocations'].items() props = {} fields = ('resource_provider', 'generation', 'resources') if self.compare_version(version.ge('1.12')): fields += ('project_id', 'user_id') props['project_id'] = resp['project_id'] props['user_id'] = resp['user_id'] if self.compare_version(version.ge('1.38')): fields += ('consumer_type',) props['consumer_type'] = resp['consumer_type'] allocs = [dict(resource_provider=k, **props, **v) for k, v in per_provider] rows = (utils.get_dict_properties(a, fields) for a in allocs) return fields, rows class UnsetAllocation(command.Lister, version.CheckerMixin): """Removes one or more sets of provider allocations for a consumer. Note that omitting both the ``--provider`` and the ``--resource-class`` option is equivalent to removing all allocations for the given consumer. This command requires ``--os-placement-api-version 1.12`` or greater. Use ``openstack resource provider allocation set`` for older versions. """ def get_parser(self, prog_name): parser = super(UnsetAllocation, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the consumer. It is strongly recommended to use ' '``--os-placement-api-version 1.28`` or greater when using ' 'this option to ensure the other allocation information is ' 'retained. ' ) parser.add_argument( '--provider', metavar='provider_uuid', action='append', default=[], help='UUID of a specific resource provider from which to remove ' 'allocations for the given consumer. This is useful when the ' 'consumer has allocations on more than one provider, for ' 'example after evacuating a server to another compute node ' 'and you want to cleanup allocations on the source compute ' 'node resource provider in order to delete it. Specify ' 'multiple times to remove allocations against multiple ' 'resource providers. Omit this option to remove all ' 'allocations for the consumer, or to remove all allocations' 'of a specific resource class from all the resource provider ' 'with the ``--resource_class`` option. ' ) parser.add_argument( '--resource-class', metavar='resource_class', action='append', default=[], help='Name of a resource class from which to remove allocations ' 'for the given consumer. This is useful when the consumer ' 'has allocations on more than one resource class. ' 'By default, this will remove allocations for the given ' 'resource class from all the providers. If ``--provider`` ' 'option is also specified, allocations to remove will be ' 'limited to that resource class of the given resource ' 'provider.' ) return parser # NOTE(mriedem): We require >= 1.12 because PUT requires project_id/user_id # since 1.8 but GET does not return project_id/user_id until 1.12 and we # do not want to add --project-id and --user-id options to this command # like in the set command. If someone needs to use an older microversion or # change the user/project they can use the set command. @version.check(version.ge('1.12')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL + '/' + parsed_args.uuid # Get the current allocations. payload = http.request('GET', url).json() allocations = payload['allocations'] if parsed_args.resource_class: # Remove the given resource class. Do not error out if the # consumer does not have allocations against that resource # class. rp_uuids = set(allocations) if parsed_args.provider: # If providers are also specified, we limit to remove # allocations only from those providers rp_uuids &= set(parsed_args.provider) for rp_uuid in rp_uuids: for rc in parsed_args.resource_class: allocations[rp_uuid]['resources'].pop(rc, None) if not allocations[rp_uuid]['resources']: allocations.pop(rp_uuid, None) else: if parsed_args.provider: # Remove the given provider(s) from the allocations if it # exists. Do not error out if the consumer does not have # allocations against a provider in case we lost a race since # the allocations are in the state the user wants them in # anyway. for rp_uuid in parsed_args.provider: allocations.pop(rp_uuid, None) else: # No --provider(s) specified so remove allocations from all # providers. allocations = {} supports_consumer_generation = self.compare_version(version.ge('1.28')) # 1.28+ allows PUTing an empty allocations dict as long as a # consumer_generation is specified. if allocations or supports_consumer_generation: payload['allocations'] = allocations http.request('PUT', url, json=payload) else: # The user must have removed all of the allocations so just DELETE # the allocations since we cannot PUT with an empty allocations # dict before 1.28. http.request('DELETE', url) resp = http.request('GET', url).json() per_provider = resp['allocations'].items() props = {} fields = ('resource_provider', 'generation', 'resources', 'project_id', 'user_id') if self.compare_version(version.ge('1.38')): fields += ('consumer_type',) props['consumer_type'] = resp.get('consumer_type') allocs = [dict(project_id=resp['project_id'], user_id=resp['user_id'], resource_provider=k, **props, **v) for k, v in per_provider] rows = (utils.get_dict_properties(a, fields) for a in allocs) return fields, rows class ShowAllocation(command.Lister, version.CheckerMixin): """Show resource allocations for a given consumer. Starting with ``--os-placement-api-version 1.12`` the API response contains the ``project_id`` and ``user_id`` of allocations which also appears in the CLI output. Starting with ``--os-placement-api-version 1.38`` the API response contains the ``consumer_type`` of consumer which also appears in the CLI output. """ def get_parser(self, prog_name): parser = super(ShowAllocation, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the consumer' ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL + '/' + parsed_args.uuid resp = http.request('GET', url).json() per_provider = resp['allocations'].items() props = {} fields = ('resource_provider', 'generation', 'resources') if self.compare_version(version.ge('1.12')): fields += ('project_id', 'user_id') props['project_id'] = resp.get('project_id') props['user_id'] = resp.get('user_id') if self.compare_version(version.ge('1.38')): fields += ('consumer_type',) props['consumer_type'] = resp.get('consumer_type') allocs = [dict(resource_provider=k, **props, **v) for k, v in per_provider] rows = (utils.get_dict_properties(a, fields) for a in allocs) return fields, rows class DeleteAllocation(command.Command): """Delete all resource allocations for a given consumer.""" def get_parser(self, prog_name): parser = super(DeleteAllocation, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the consumer' ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL + '/' + parsed_args.uuid http.request('DELETE', url) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/resources/allocation_candidate.py0000664000175000017500000003160000000000000025701 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 collections from osc_lib.command import command from osc_lib import exceptions from osc_placement.resources import common from osc_placement import version BASE_URL = '/allocation_candidates' class GroupAction(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): group, = values namespace._current_group = group groups = namespace.__dict__.setdefault('groups', {}) groups[group] = collections.defaultdict(list) class AppendToGroup(argparse.Action): def __call__(self, parser, namespace, values, option_string=None): if getattr(namespace, '_current_group', None) is None: groups = namespace.__dict__.setdefault('groups', {}) namespace._current_group = '' groups[''] = collections.defaultdict(list) namespace.groups[namespace._current_group][self.dest].append(values) class ListAllocationCandidate(command.Lister, version.CheckerMixin): """List allocation candidates. Returns a representation of a collection of allocation requests and resource provider summaries. Each allocation request has information to issue an ``openstack resource provider allocation set`` request to claim resources against a related set of resource providers. As several allocation requests are available its necessary to select one. To make a decision, resource provider summaries are provided with the inventory/capacity information. For example:: $ export OS_PLACEMENT_API_VERSION=1.10 $ openstack allocation candidate list --resource VCPU=1 +---+------------+-------------------------+-------------------------+ | # | allocation | resource provider | inventory used/capacity | +---+------------+-------------------------+-------------------------+ | 1 | VCPU=1 | 66bcaca9-9263-45b1-a569 | VCPU=0/128 | | | | -ea708ff7a968 | | +---+------------+-------------------------+-------------------------+ In this case, the user is looking for resource providers that can have capacity to allocate 1 ``VCPU`` resource class. There is one resource provider that can serve that allocation request and that resource providers current ``VCPU`` inventory used is 0 and available capacity is 128. This command requires at least ``--os-placement-api-version 1.10``. """ def get_parser(self, prog_name): parser = super(ListAllocationCandidate, self).get_parser(prog_name) parser.add_argument( '--resource', metavar='=', dest='resources', action=AppendToGroup, help='String indicating an amount of resource of a specified ' 'class that providers in each allocation request must ' 'collectively have the capacity and availability to serve. ' 'Can be specified multiple times per resource class. ' 'For example: ' '``--resource VCPU=4 --resource DISK_GB=64 ' '--resource MEMORY_MB=2048``' ) parser.add_argument( '--limit', metavar='', help='A positive integer to limit ' 'the maximum number of allocation candidates. ' 'This option requires at least ' '``--os-placement-api-version 1.16``.' ) parser.add_argument( '--required', metavar='', action=AppendToGroup, help='A required trait. May be repeated. Allocation candidates ' 'must collectively contain all of the required traits. ' 'This option requires at least ' '``--os-placement-api-version 1.17``. ' 'Since ``--os-placement-api-version 1.39`` the value of ' 'this parameter can be a comma separated list of trait names ' 'to express OR relationship between those traits.' ) parser.add_argument( '--forbidden', metavar='', action=AppendToGroup, help='A forbidden trait. May be repeated. Returned allocation ' 'candidates must not contain any of the specified traits. ' 'This option requires at least ' '``--os-placement-api-version 1.22``.' ) # NOTE(tetsuro): --aggregate-uuid is deprecated in Jan 2020 in 1.x # release. Do not remove before Jan 2021 and a 2.x release. aggregate_group = parser.add_mutually_exclusive_group() aggregate_group.add_argument( "--member-of", action=AppendToGroup, metavar='', help='A list of comma-separated UUIDs of the resource provider ' 'aggregates. The returned allocation candidates must be ' 'associated with at least one of the aggregates identified ' 'by uuid. This param requires at least ' '``--os-placement-api-version 1.21`` and can be repeated to ' 'add(restrict) the condition with ' '``--os-placement-api-version 1.24`` or greater. ' 'For example, to get candidates in either of agg1 or agg2 ' 'and definitely in agg3, specify:\n\n' '``--member_of , --member_of ``' ) aggregate_group.add_argument( '--aggregate-uuid', action=AppendToGroup, metavar='', help=argparse.SUPPRESS ) parser.add_argument( '--group', action=GroupAction, metavar='', help='An integer to group granular requests. If specified, ' 'following given options of resources, required/forbidden ' 'traits, and aggregate are associated to that group and will ' 'be satisfied by the same resource provider in the response. ' 'Can be repeated to get candidates from multiple resource ' 'providers in the same resource provider tree. ' 'For example, ``--group 1 --resource VCPU=3 --required ' 'HW_CPU_X86_AVX --group 2 --resource VCPU=2 --required ' 'HW_CPU_X86_SSE`` will provide candidates where three VCPUs ' 'comes from a provider with ``HW_CPU_X86_AVX`` trait and ' 'two VCPUs from a provider with ``HW_CPU_X86_SSE`` trait. ' 'This option requires at least ' '``--os-placement-api-version 1.25`` or greater, but to have ' 'placement server be aware of resource provider tree, use ' '``--os-placement-api-version 1.29`` or greater.' ) parser.add_argument( '--group-policy', choices=['none', 'isolate'], default='none', metavar='', help='This indicates how the groups should interact when multiple ' 'groups are supplied. With group_policy=none (default), ' 'separate groups may or may not be satisfied by the same ' 'provider. With group_policy=isolate, numbered groups are ' 'guaranteed to be satisfied by different providers.' ) return parser @version.check(version.ge('1.10')) def take_action(self, parsed_args): http = self.app.client_manager.placement params = {} if 'groups' not in parsed_args: raise exceptions.CommandError( 'At least one --resource must be specified.') if 'limit' in parsed_args and parsed_args.limit: # Fail if --limit but not high enough microversion. self.check_version(version.ge('1.16')) params['limit'] = int(parsed_args.limit) if any(parsed_args.groups): self.check_version(version.ge('1.25')) params['group_policy'] = parsed_args.group_policy for suffix, group in parsed_args.groups.items(): def _get_key(name): return name + suffix if 'resources' not in group: raise exceptions.CommandError( '--resources should be provided in group %s', suffix) for resource in group['resources']: if not len(resource.split('=')) == 2: raise exceptions.CommandError( 'Arguments to --resource must be of form ' '=') params[_get_key('resources')] = ','.join( resource.replace('=', ':') for resource in group['resources']) # We need to handle required and forbidden together as they all # end up in the same query param on the API. # First just check that the requested feature is aligned with the # request microversion required_traits = [] if 'required' in group and group['required']: # Fail if --required but not high enough microversion. self.check_version(version.ge('1.17')) if any(',' in required for required in group['required']): self.check_version(version.ge('1.39')) required_traits = group['required'] forbidden_traits = [] if 'forbidden' in group and group['forbidden']: self.check_version(version.ge('1.22')) forbidden_traits = ['!' + f for f in group['forbidden']] # Then collect the required query params containing both required # and forbidden traits params[_get_key('required')] = ( common.get_required_query_param_from_args( required_traits, forbidden_traits) ) if 'aggregate_uuid' in group and group['aggregate_uuid']: # Fail if --aggregate_uuid but not high enough microversion. self.check_version(version.ge('1.21')) self.deprecated_option_warning( "--aggregate-uuid", "--member-of") params[_get_key('member_of')] = 'in:' + ','.join( group['aggregate_uuid']) if 'member_of' in group and group['member_of']: # Fail if --member-of but not high enough microversion. self.check_version(version.ge('1.21')) params[_get_key('member_of')] = [ 'in:' + aggs for aggs in group['member_of']] resp = http.request('GET', BASE_URL, params=params).json() rp_resources = {} include_traits = self.compare_version(version.ge('1.17')) if include_traits: rp_traits = {} for rp_uuid, resources in resp['provider_summaries'].items(): rp_resources[rp_uuid] = ','.join( '%s=%s/%s' % (rc, value['used'], value['capacity']) for rc, value in resources['resources'].items()) if include_traits: rp_traits[rp_uuid] = ','.join(resources['traits']) rows = [] if self.compare_version(version.ge('1.12')): for i, allocation_req in enumerate(resp['allocation_requests']): for rp, resources in allocation_req['allocations'].items(): req = ','.join( '%s=%s' % (rc, value) for rc, value in resources['resources'].items()) if include_traits: row = [i + 1, req, rp, rp_resources[rp], rp_traits[rp]] else: row = [i + 1, req, rp, rp_resources[rp]] rows.append(row) else: for i, allocation_req in enumerate(resp['allocation_requests']): for allocation in allocation_req['allocations']: rp = allocation['resource_provider']['uuid'] req = ','.join( '%s=%s' % (rc, value) for rc, value in allocation['resources'].items()) rows.append([i + 1, req, rp, rp_resources[rp]]) fields = ('#', 'allocation', 'resource provider', 'inventory used/capacity') if include_traits: fields += ('traits',) return fields, rows ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/resources/common.py0000664000175000017500000000370100000000000023051 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from urllib import parse as urlparse def encode(value, encoding='utf-8'): """Return a byte repr of a string for a given encoding. Byte strings and values of other types are returned as is. """ if isinstance(value, str): return value.encode(encoding) else: return value def url_with_filters(url, filters=None): """Add a percent-encoded string of filters (a dict) to a base url.""" if filters: filters = [(encode(k), encode(v)) for k, v in filters.items()] urlencoded_filters = urlparse.urlencode(filters) url = urlparse.urljoin(url, '?' + urlencoded_filters) return url def get_required_query_param_from_args(required_traits, forbidden_traits): # Iterate the required params and collect OR groups and simple # AND traits separately. Each OR group needs a separate query param # while the AND traits and forbidden traits can be collated to a single # query param required_query_params = [] and_traits = [] for required in required_traits: if ',' in required: required_query_params.append('in:' + required) else: and_traits.append(required) # We need an extra required query param for the and_traits and the # forbidden traits and_query = ','.join(and_traits + forbidden_traits) if and_query: required_query_params.append(and_query) return required_query_params ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/resources/inventory.py0000664000175000017500000003611100000000000023617 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 itertools from osc_lib.command import command from osc_lib import exceptions from osc_lib.i18n import _ from osc_lib import utils from oslo_utils import excutils from osc_placement.resources import common from osc_placement import version BASE_URL = '/resource_providers/{uuid}/inventories' PER_CLASS_URL = BASE_URL + '/{resource_class}' RP_BASE_URL = '/resource_providers' USAGES_BASE_URL = '/resource_providers/{uuid}/usages' INVENTORY_FIELDS = { 'allocation_ratio': { 'type': float, 'required': False, 'help': ('It is used in determining whether consumption ' 'of the resource of the provider can exceed ' 'physical constraints. For example, for a vCPU resource ' 'with: allocation_ratio = 16.0, total = 8. ' 'Overall capacity is equal to 128 vCPUs.') }, 'min_unit': { 'type': int, 'required': False, 'help': ('A minimum amount any single allocation against ' 'an inventory can have.') }, 'max_unit': { 'type': int, 'required': False, 'help': ('A maximum amount any single allocation against ' 'an inventory can have.') }, 'reserved': { 'type': int, 'required': False, 'help': ('The amount of the resource a provider has reserved ' 'for its own use.') }, 'step_size': { 'type': int, 'required': False, 'help': ('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.') }, 'total': { 'type': int, 'required': True, 'help': ('The actual amount of the resource that the provider ' 'can accommodate.') } } FIELDS = tuple(INVENTORY_FIELDS.keys()) RC_HELP = (' is an entity that indicates standard or ' 'deployer-specific resources that can be provided by a resource ' 'provider. For example, VCPU, MEMORY_MB, DISK_GB.') def parse_resource_argument(resource): parts = resource.split('=') if len(parts) != 2: raise ValueError( 'Resource argument must have "name=value" format') name, value = parts parts = name.split(':') if len(parts) == 2: name, field = parts elif len(parts) == 1: name = parts[0] field = 'total' else: raise ValueError('Resource argument can contain only one colon') if not all([name, field, value]): raise ValueError('Name, field and value must be not empty') if field not in INVENTORY_FIELDS: raise ValueError('Unknown inventory field %s' % field) value = INVENTORY_FIELDS[field]['type'](value) return name, field, value class SetInventory(command.Lister, version.CheckerMixin): """Replaces the set of inventory records for the resource provider. Note that by default this is a full replacement of the existing inventory. If you want to retain the existing inventory and add a new resource class inventory, you must specify all resource class inventory, old and new, or specify the ``--amend`` option. If a specific inventory field is not specified for a given resource class, it is assumed to be the total, i.e. ``--resource VCPU=16`` is equivalent to ``--resource VCPU:total=16``. Example:: openstack resource provider inventory set \ --resource VCPU=16 \ --resource MEMORY_MB=2048 \ --resource MEMORY_MB:step_size=128 """ def get_parser(self, prog_name): parser = super(SetInventory, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider or UUID of the aggregate, ' 'if --aggregate is specified' ) fields_help = '\n'.join( '{} - {}'.format(f, INVENTORY_FIELDS[f]['help'].lower()) for f in INVENTORY_FIELDS) parser.add_argument( '--resource', metavar=':=', help='String describing resource.\n' + RC_HELP + '\n' ' (optional) can be:\n' + fields_help, default=[], action='append' ) parser.add_argument( '--aggregate', action='store_true', help='If this option is specified, the inventories for all ' 'resource providers that are members of the aggregate will ' 'be set. This option requires at least ' '``--os-placement-api-version 1.3``' ) parser.add_argument( '--amend', action='store_true', help='If this option is specified, the inventories will be ' 'amended instead of being fully replaced' ) parser.add_argument( '--dry-run', action='store_true', help='If this option is specified, the inventories that would be ' 'set will be returned without actually setting any ' 'inventories' ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement if parsed_args.aggregate: self.check_version(version.ge('1.3')) filters = {'member_of': parsed_args.uuid} url = common.url_with_filters(RP_BASE_URL, filters) rps = http.request('GET', url).json()['resource_providers'] if not rps: raise exceptions.CommandError( 'No resource providers found in aggregate with uuid %s.' % parsed_args.uuid) else: url = RP_BASE_URL + '/' + parsed_args.uuid rps = [http.request('GET', url).json()] resources_list = [] ret = 0 for rp in rps: inventories = collections.defaultdict(dict) url = BASE_URL.format(uuid=rp['uuid']) if parsed_args.amend: # Get existing inventories # TODO(melwitt): Do something to handle the possibility of the # GET failing here (example: resource provider deleted from # underneath us). payload = http.request('GET', url).json() inventories.update(payload['inventories']) payload['inventories'] = inventories else: payload = {'inventories': inventories, 'resource_provider_generation': rp['generation']} # Apply resource values to inventories for r in parsed_args.resource: name, field, value = parse_resource_argument(r) inventories[name][field] = value try: if not parsed_args.dry_run: resources = http.request('PUT', url, json=payload).json() else: resources = payload except Exception as exp: with excutils.save_and_reraise_exception() as err_ctx: if parsed_args.aggregate: self.log.error(_('Failed to set inventory for ' 'resource provider %(rp)s: %(exp)s.'), {'rp': rp['uuid'], 'exp': exp}) err_ctx.reraise = False ret += 1 continue resources_list.append((rp['uuid'], resources)) if ret > 0: msg = _('Failed to set inventory for %(ret)s of %(total)s ' 'resource providers.') % {'ret': ret, 'total': len(rps)} raise exceptions.CommandError(msg) def get_rows(fields, resources, rp_uuid=None): inventories = [ dict(resource_class=k, **v) for k, v in resources['inventories'].items() ] prepend = (rp_uuid, ) if rp_uuid else () # This is a generator expression rows = (prepend + utils.get_dict_properties(i, fields) for i in inventories) return rows fields = ('resource_class', ) + FIELDS if parsed_args.aggregate: # If this is an aggregate batch, create output that will include # resource provider as the first field to differentiate the values rows = () for rp_uuid, resources in resources_list: subrows = get_rows(fields, resources, rp_uuid=rp_uuid) rows = itertools.chain(rows, subrows) fields = ('resource_provider', ) + fields return fields, rows else: # If this was not an aggregate batch, show output for the one # resource provider (show payload of the first item in the list), # keeping the behavior prior to the addition of --aggregate option return fields, get_rows(fields, resources_list[0][1]) class SetClassInventory(command.ShowOne): """Replace the inventory record of the class for the resource provider. Example:: openstack resource provider inventory class set VCPU \ --total 16 \ --max_unit 4 \ --reserved 1 """ def get_parser(self, prog_name): parser = super(SetClassInventory, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider' ) parser.add_argument( 'resource_class', metavar='', help=RC_HELP ) for name, props in INVENTORY_FIELDS.items(): parser.add_argument( '--' + name, metavar='<{}>'.format(name), required=props['required'], type=props['type'], help=props['help']) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement url = RP_BASE_URL + '/' + parsed_args.uuid rp = http.request('GET', url).json() payload = {'resource_provider_generation': rp['generation']} for field in FIELDS: value = getattr(parsed_args, field, None) if value is not None: payload[field] = value url = PER_CLASS_URL.format(uuid=parsed_args.uuid, resource_class=parsed_args.resource_class) resource = http.request('PUT', url, json=payload).json() return FIELDS, utils.get_dict_properties(resource, FIELDS) class DeleteInventory(command.Command, version.CheckerMixin): """Delete the inventory. Depending on the resource class argument presence, delete all inventory for a given resource provider or for a resource provider/class pair. Delete all inventories for given resource provider requires at least ``--os-placement-api-version 1.5``. """ def get_parser(self, prog_name): parser = super(DeleteInventory, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider' ) parser.add_argument( '--resource-class', metavar='', required=self.compare_version(version.lt('1.5')), help=(RC_HELP + '\n' 'This argument can be omitted starting with ' '``--os-placement-api-version 1.5``. If it is omitted all ' 'inventories of the specified resource provider ' 'will be deleted.') ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL params = {'uuid': parsed_args.uuid} if parsed_args.resource_class is not None: url = PER_CLASS_URL params = {'uuid': parsed_args.uuid, 'resource_class': parsed_args.resource_class} http.request('DELETE', url.format(**params)) class ShowInventory(command.ShowOne): """Show the inventory for a given resource provider/class pair.""" def get_parser(self, prog_name): parser = super(ShowInventory, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider' ) parser.add_argument( 'resource_class', metavar='', help=RC_HELP ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement url = PER_CLASS_URL.format(uuid=parsed_args.uuid, resource_class=parsed_args.resource_class) resource = http.request('GET', url).json() # TODO(stephenfin): We should just include this information in the # above API. Alternatively, we should add an API to retrieve usage for # a single resource class url = USAGES_BASE_URL.format(uuid=parsed_args.uuid) resources = http.request('GET', url).json()['usages'] resource['used'] = resources[parsed_args.resource_class] fields = FIELDS + ('used', ) return fields, utils.get_dict_properties(resource, fields) class ListInventory(command.Lister): """List inventories for a given resource provider.""" def get_parser(self, prog_name): parser = super(ListInventory, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider' ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL.format(uuid=parsed_args.uuid) resources = http.request('GET', url).json() inventories = [ dict(resource_class=k, **v) for k, v in resources['inventories'].items() ] # TODO(stephenfin): We should just include this information in the # above API url = USAGES_BASE_URL.format(uuid=parsed_args.uuid) resources = http.request('GET', url).json()['usages'] for inventory in inventories: inventory['used'] = resources[inventory['resource_class']] fields = ('resource_class', ) + FIELDS + ('used', ) rows = (utils.get_dict_properties(i, fields) for i in inventories) return fields, rows ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/resources/resource_class.py0000664000175000017500000001037500000000000024602 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from osc_lib.command import command from osc_lib import utils from osc_placement import version BASE_URL = '/resource_classes' PER_CLASS_URL = BASE_URL + '/{name}' FIELDS = ('name',) class ListResourceClass(command.Lister): """Return a list of all resource classes. This command requires at least ``--os-placement-api-version 1.2``. """ def get_parser(self, prog_name): parser = super(ListResourceClass, self).get_parser(prog_name) return parser @version.check(version.ge('1.2')) def take_action(self, parsed_args): http = self.app.client_manager.placement resource_classes = http.request( 'GET', BASE_URL).json()['resource_classes'] rows = (utils.get_dict_properties(i, FIELDS) for i in resource_classes) return FIELDS, rows class CreateResourceClass(command.Command): """Create a new resource class. This command requires at least ``--os-placement-api-version 1.2``. """ def get_parser(self, prog_name): parser = super(CreateResourceClass, self).get_parser(prog_name) parser.add_argument( 'name', metavar='', help='Name of the resource class' ) return parser @version.check(version.ge('1.2')) def take_action(self, parsed_args): http = self.app.client_manager.placement http.request('POST', BASE_URL, json={'name': parsed_args.name}) class SetResourceClass(command.Command): """Create or validate the existence of single resource class. Unlike ``openstack resource class create``, this command also succeeds if the resource class already exists, which makes this an idempotent check or create command. This command requires at least ``--os-placement-api-version 1.7``. """ def get_parser(self, prog_name): parser = super(SetResourceClass, self).get_parser(prog_name) parser.add_argument( 'name', metavar='', help='Name of the resource class' ) return parser @version.check(version.ge('1.7')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL + '/' + parsed_args.name http.request('PUT', url) class ShowResourceClass(command.ShowOne): """Return a representation of the resource class identified by ````. This command requires at least ``--os-placement-api-version 1.2``. """ def get_parser(self, prog_name): parser = super(ShowResourceClass, self).get_parser(prog_name) parser.add_argument( 'name', metavar='', help='Name of the resource class' ) return parser @version.check(version.ge('1.2')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = PER_CLASS_URL.format(name=parsed_args.name) resource = http.request('GET', url).json() return FIELDS, utils.get_dict_properties(resource, FIELDS) class DeleteResourceClass(command.Command): """Delete the resource class identified by ````. Only custom resource classes can be deleted. This command requires at least ``--os-placement-api-version 1.2``. """ def get_parser(self, prog_name): parser = super(DeleteResourceClass, self).get_parser(prog_name) parser.add_argument( 'name', metavar='', help='Name of the resource class' ) return parser @version.check(version.ge('1.2')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = PER_CLASS_URL.format(name=parsed_args.name) http.request('DELETE', url) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/resources/resource_provider.py0000664000175000017500000003035500000000000025327 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse from osc_lib.command import command from osc_lib import utils from osc_placement.resources import common from osc_placement import version BASE_URL = '/resource_providers' ALLOCATIONS_URL = BASE_URL + '/{uuid}/allocations' class CreateResourceProvider(command.ShowOne, version.CheckerMixin): """Create a new resource provider""" def get_parser(self, prog_name): parser = super(CreateResourceProvider, self).get_parser(prog_name) parser.add_argument( '--parent-provider', metavar='', help='UUID of the parent provider.' ' Omit for no parent.' ' This option requires at least' ' ``--os-placement-api-version 1.14``.' ) parser.add_argument( '--uuid', metavar='', help='UUID of the resource provider' ) parser.add_argument( 'name', metavar='', help='Name of the resource provider' ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement data = {'name': parsed_args.name} if 'uuid' in parsed_args and parsed_args.uuid: data['uuid'] = parsed_args.uuid if ('parent_provider' in parsed_args and parsed_args.parent_provider): self.check_version(version.ge('1.14')) data['parent_provider_uuid'] = parsed_args.parent_provider resp = http.request('POST', BASE_URL, json=data) resource = http.request('GET', resp.headers['Location']).json() fields = ('uuid', 'name', 'generation') if self.compare_version(version.ge('1.14')): fields += ('root_provider_uuid', 'parent_provider_uuid') return fields, utils.get_dict_properties(resource, fields) class ListResourceProvider(command.Lister, version.CheckerMixin): """List resource providers""" def get_parser(self, prog_name): parser = super(ListResourceProvider, self).get_parser(prog_name) parser.add_argument( '--uuid', metavar='', help='UUID of the resource provider' ) parser.add_argument( '--name', metavar='', help='Name of the resource provider' ) parser.add_argument( '--resource', metavar='=', default=[], action='append', help='A resource class value pair indicating an ' 'amount of resource of a specified class that a provider ' 'must have the capacity to serve. May be repeated.\n\n' 'This param requires at least ' '``--os-placement-api-version 1.4``.' ) parser.add_argument( '--in-tree', metavar='', help='Restrict listing to the same "provider tree"' ' as the specified provider UUID.' ' This option requires at least' ' ``--os-placement-api-version 1.14``.' ) parser.add_argument( '--required', metavar='', action='append', default=[], help='A required trait. May be repeated. Resource providers ' 'must collectively contain all of the required traits. ' 'This option requires at least ' '``--os-placement-api-version 1.18``. ' 'Since ``--os-placement-api-version 1.39`` the value of ' 'this parameter can be a comma separated list of trait names ' 'to express OR relationship between those traits.' ) parser.add_argument( '--forbidden', metavar='', action='append', default=[], help='A forbidden trait. May be repeated. Returned resource ' 'providers must not contain any of the specified traits. ' 'This option requires at least ' '``--os-placement-api-version 1.22``.' ) # NOTE(tetsuro): --aggregate-uuid is deprecated in Jan 2020 in 1.x # release. Do not remove before Jan 2021 and a 2.x release. aggregate_group = parser.add_mutually_exclusive_group() aggregate_group.add_argument( "--member-of", default=[], action='append', metavar='', help='A list of comma-separated UUIDs of the resource provider ' 'aggregates. The returned resource providers must be ' 'associated with at least one of the aggregates identified ' 'by uuid. This param requires at least ' '``--os-placement-api-version 1.3`` and can be repeated to ' 'add(restrict) the condition with ' '``--os-placement-api-version 1.24`` or greater. ' 'For example, to get candidates either in agg1 or in agg2 ' 'and definitely in agg3, specify:\n\n' '``--member_of , --member_of ``' ) aggregate_group.add_argument( '--aggregate-uuid', default=[], action='append', metavar='', help=argparse.SUPPRESS ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement filters = {} if parsed_args.name: filters['name'] = parsed_args.name if parsed_args.uuid: filters['uuid'] = parsed_args.uuid if parsed_args.aggregate_uuid: self.check_version(version.ge('1.3')) self.deprecated_option_warning("--aggregate-uuid", "--member-of") filters['member_of'] = 'in:' + ','.join(parsed_args.aggregate_uuid) if parsed_args.resource: self.check_version(version.ge('1.4')) filters['resources'] = ','.join( resource.replace('=', ':') for resource in parsed_args.resource) if 'in_tree' in parsed_args and parsed_args.in_tree: self.check_version(version.ge('1.14')) filters['in_tree'] = parsed_args.in_tree # We need to handle required and forbidden together as they all end up # in the same query param on the API. # First just check that the requested feature is aligned with the # request microversion required_traits = [] if 'required' in parsed_args and parsed_args.required: self.check_version(version.ge('1.18')) if any(',' in required for required in parsed_args.required): self.check_version(version.ge('1.39')) required_traits = parsed_args.required forbidden_traits = [] if 'forbidden' in parsed_args and parsed_args.forbidden: self.check_version(version.ge('1.22')) forbidden_traits = ['!' + f for f in parsed_args.forbidden] # Then collect the required query params containing both required and # forbidden traits filters['required'] = common.get_required_query_param_from_args( required_traits, forbidden_traits) if 'member_of' in parsed_args and parsed_args.member_of: # Fail if --member-of but not high enough microversion. self.check_version(version.ge('1.3')) filters['member_of'] = [ 'in:' + aggs for aggs in parsed_args.member_of] resources = http.request( 'GET', BASE_URL, params=filters).json()['resource_providers'] fields = ('uuid', 'name', 'generation') if self.compare_version(version.ge('1.14')): fields += ('root_provider_uuid', 'parent_provider_uuid') rows = (utils.get_dict_properties(r, fields) for r in resources) return fields, rows class ShowResourceProvider(command.ShowOne, version.CheckerMixin): """Show resource provider details""" def get_parser(self, prog_name): parser = super(ShowResourceProvider, self).get_parser(prog_name) # TODO(avolkov): show by uuid or name parser.add_argument( '--allocations', action='store_true', help='include the info on allocations of the provider resources' ) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider' ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL + '/' + parsed_args.uuid resource = http.request('GET', url).json() fields = ('uuid', 'name', 'generation') if self.compare_version(version.ge('1.14')): fields += ('root_provider_uuid', 'parent_provider_uuid') if parsed_args.allocations: allocs_url = ALLOCATIONS_URL.format(uuid=parsed_args.uuid) allocs = http.request('GET', allocs_url).json()['allocations'] resource['allocations'] = allocs fields += ('allocations',) return fields, utils.get_dict_properties(resource, fields) class SetResourceProvider(command.ShowOne, version.CheckerMixin): """Update an existing resource provider""" def get_parser(self, prog_name): parser = super(SetResourceProvider, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider' ) parser.add_argument( '--name', metavar='', help='A new name of the resource provider', required=True ) parser.add_argument( '--parent-provider', metavar='', help='UUID of the parent provider.' ' Can only be set if the resource provider has no parent yet.' ' This option requires at least' ' ``--os-placement-api-version 1.14``.' ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL + '/' + parsed_args.uuid data = dict(name=parsed_args.name) # Not knowing the previous state of a resource the client cannot catch # it, but if the user tries to re-parent a resource provider the server # returns an easy to understand error: # Unable to save resource provider RP-ID: # Object action update failed because: # re-parenting a provider is not currently allowed. # (HTTP 400) if ('parent_provider' in parsed_args and parsed_args.parent_provider): self.check_version(version.ge('1.14')) data['parent_provider_uuid'] = parsed_args.parent_provider resource = http.request('PUT', url, json=data).json() fields = ('uuid', 'name', 'generation') if self.compare_version(version.ge('1.14')): fields += ('root_provider_uuid', 'parent_provider_uuid') return fields, utils.get_dict_properties(resource, fields) class DeleteResourceProvider(command.Command): """Delete a resource provider""" def get_parser(self, prog_name): parser = super(DeleteResourceProvider, self).get_parser(prog_name) # TODO(avolkov): delete by uuid or name parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider' ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL + '/' + parsed_args.uuid http.request('DELETE', url) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/resources/trait.py0000664000175000017500000001623400000000000022711 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you # may not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from osc_lib.command import command from osc_placement import version BASE_URL = '/traits' RP_BASE_URL = '/resource_providers/{uuid}' RP_TRAITS_URL = '/resource_providers/{uuid}/traits' FIELDS = ('name',) class ListTrait(command.Lister): """Return a list of valid trait strings. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(ListTrait, self).get_parser(prog_name) parser.add_argument( '--name', metavar='', help=('A string to filter traits. The following options ' 'are available: startswith operator filters the ' 'traits whose name begins with a specific prefix, ' 'e.g. name=startswith:CUSTOM, in operator filters ' 'the traits whose name is in the specified list, ' 'e.g. name=in:HW_CPU_X86_AVX,HW_CPU_X86_SSE, ' 'HW_CPU_X86_INVALID_FEATURE.') ) parser.add_argument( '--associated', action='store_true', help=('If this parameter is presented, the returned ' 'traits will be those that are associated with at ' 'least one resource provider.') ) return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL params = {} if parsed_args.name: params['name'] = parsed_args.name if parsed_args.associated: params['associated'] = parsed_args.associated traits = http.request('GET', url, params=params).json()['traits'] return FIELDS, [[t] for t in traits] class ShowTrait(command.ShowOne): """Check if a trait name exists in this cloud. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(ShowTrait, self).get_parser(prog_name) parser.add_argument( 'name', metavar='', help='Name of the trait.' ) return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = '/'.join([BASE_URL, parsed_args.name]) http.request('GET', url) return FIELDS, [parsed_args.name] class CreateTrait(command.Command): """Create a new custom trait. Custom traits must begin with the prefix ``CUSTOM_`` and contain only the letters A through Z, the numbers 0 through 9 and the underscore "_" character. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(CreateTrait, self).get_parser(prog_name) parser.add_argument( 'name', metavar='', help='Name of the trait.' ) return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = '/'.join([BASE_URL, parsed_args.name]) http.request('PUT', url) class DeleteTrait(command.Command): """Delete the trait specified by {name}. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(DeleteTrait, self).get_parser(prog_name) parser.add_argument( 'name', metavar='', help='Name of the trait.' ) return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = '/'.join([BASE_URL, parsed_args.name]) http.request('DELETE', url) class ListResourceProviderTrait(command.Lister): """List traits associated with the resource provider identified by {uuid}. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(ListResourceProviderTrait, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider.' ) return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = RP_TRAITS_URL.format(uuid=parsed_args.uuid) traits = http.request('GET', url).json()['traits'] return FIELDS, [[t] for t in traits] class SetResourceProviderTrait(command.Lister): """Associate traits with the resource provider identified by {uuid}. All the associated traits will be replaced by the traits specified. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(SetResourceProviderTrait, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider.' ) parser.add_argument( '--trait', metavar='', help='Name of the trait. May be repeated.', default=[], action='append' ) return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = RP_BASE_URL.format(uuid=parsed_args.uuid) rp = http.request('GET', url).json() url = RP_TRAITS_URL.format(uuid=parsed_args.uuid) payload = { 'resource_provider_generation': rp['generation'], 'traits': parsed_args.trait } traits = http.request('PUT', url, json=payload).json()['traits'] return FIELDS, [[t] for t in traits] class DeleteResourceProviderTrait(command.Command): """Dissociate all the traits from the resource provider. Note that this command is not atomic if multiple processes are managing traits for the same provider. This command requires at least ``--os-placement-api-version 1.6``. """ def get_parser(self, prog_name): parser = super(DeleteResourceProviderTrait, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider.' ) return parser @version.check(version.ge('1.6')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = RP_TRAITS_URL.format(uuid=parsed_args.uuid) http.request('DELETE', url) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/resources/usage.py0000664000175000017500000000555100000000000022672 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from osc_lib.command import command from osc_lib import utils from osc_placement import version BASE_URL = '/resource_providers/{uuid}/usages' USAGES_URL = '/usages' FIELDS = ('resource_class', 'usage') class ResourceShowUsage(command.Lister, version.CheckerMixin): """Show resource usages for a project (and optionally user) per class. Gives a report of usage information for resources associated with the project identified by the ``project_id`` argument and user identified by the ``--user-id`` option. This command requires at least ``--os-placement-api-version 1.9``. """ def get_parser(self, prog_name): parser = super(ResourceShowUsage, self).get_parser(prog_name) parser.add_argument( 'project_id', metavar='', help='ID of the project.' ) parser.add_argument( '--user-id', metavar='', help='ID of the user.' ) return parser @version.check(version.ge('1.9')) def take_action(self, parsed_args): http = self.app.client_manager.placement url = USAGES_URL params = {'project_id': parsed_args.project_id} if parsed_args.user_id: params['user_id'] = parsed_args.user_id per_class = http.request( 'GET', url, params=params).json()['usages'] usages = [{'resource_class': k, 'usage': v} for k, v in per_class.items()] rows = (utils.get_dict_properties(u, FIELDS) for u in usages) return FIELDS, rows class ShowUsage(command.Lister): """Show resource usages per class for a given resource provider.""" def get_parser(self, prog_name): parser = super(ShowUsage, self).get_parser(prog_name) parser.add_argument( 'uuid', metavar='', help='UUID of the resource provider' ) return parser def take_action(self, parsed_args): http = self.app.client_manager.placement url = BASE_URL.format(uuid=parsed_args.uuid) per_class = http.request('GET', url).json()['usages'] usages = [{'resource_class': k, 'usage': v} for k, v in per_class.items()] rows = (utils.get_dict_properties(u, FIELDS) for u in usages) return FIELDS, rows ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1740697287.401173 osc_placement-4.6.0/osc_placement/tests/0000775000175000017500000000000000000000000020336 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/__init__.py0000664000175000017500000000000000000000000022435 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740697287.4051733 osc_placement-4.6.0/osc_placement/tests/functional/0000775000175000017500000000000000000000000022500 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/functional/__init__.py0000664000175000017500000000000000000000000024577 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/functional/base.py0000664000175000017500000004367200000000000024000 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 json import logging import random import fixtures from openstackclient import shell from oslotest import base from placement.tests.functional.fixtures import capture from placement.tests.functional.fixtures import placement # A list of logger names that will be reset to a log level # of WARNING. Due (we think) to a poor interaction between the # way osc does logging and oslo.logging, all packages are producing # DEBUG logs. This results in test attachments (when capturing logs) # that are sometimes larger than subunit.parser can deal with. The # packages chosen here are ones that do not provide useful information. RESET_LOGGING = [ 'keystoneauth.session', 'oslo_policy.policy', 'placement.objects.trait', 'placement.objects.resource_class', 'placement.objects.resource_provider', 'oslo_concurrency.lockutils', 'osc_lib.shell', ] RP_PREFIX = 'osc-placement-functional-tests-' ARGUMENTS_MISSING = 'the following arguments are required' ARGUMENTS_REQUIRED = 'the following arguments are required: %s' class CommandException(Exception): def __init__(self, *args, **kwargs): super(CommandException, self).__init__(args[0]) self.cmd = kwargs['cmd'] class BaseTestCase(base.BaseTestCase): VERSION = '1.0' def setUp(self): super(BaseTestCase, self).setUp() self.useFixture(capture.Logging()) self.placement = self.useFixture(placement.PlacementFixture()) # Work around needing to reset the session's notion of where # we are going. def mock_get(obj, instance, owner): return obj.factory(instance) # NOTE(cdent): This is fragile, but is necessary to work around # the rather complex start up optimizations that are done in osc_lib. # If/when osc_lib changes this will at least fail fast. self.useFixture(fixtures.MonkeyPatch( 'osc_lib.clientmanager.ClientCache.__get__', mock_get)) # Reset log level on a set of packages. See comment on RESET_LOGGING # assigment, above. for name in RESET_LOGGING: logging.getLogger(name).setLevel(logging.WARNING) def openstack(self, cmd, may_fail=False, use_json=False, may_print_to_stderr=False): to_exec = [] # Make all requests as a noauth admin user. to_exec += [ '--os-endpoint', self.placement.endpoint, '--os-token', self.placement.token, '--os-auth-type', 'admin_token', ] if self.VERSION is not None: to_exec += ['--os-placement-api-version', self.VERSION] to_exec += cmd.split() if use_json: to_exec += ['-f', 'json'] # Context manager here instead of setUp because we only want # output trapping around the run(). self.output = io.StringIO() self.error = io.StringIO() stdout_fix = fixtures.MonkeyPatch('sys.stdout', self.output) stderr_fix = fixtures.MonkeyPatch('sys.stderr', self.error) with stdout_fix, stderr_fix: try: os_shell = shell.OpenStackShell() return_code = os_shell.run(to_exec) # Catch SystemExit to trap some error responses, mostly from the # argparse lib which has a tendency to exit for you instead of # politely telling you it wants to. except SystemExit as exc: return_code = exc.code # We may have error/warning messages in stderr, so treat it # separately from the stdout. output = self.output.getvalue() error = self.error.getvalue() if return_code: msg = 'Command: "%s"\noutput: %s' % (' '.join(to_exec), error) if not may_fail: raise CommandException(msg, cmd=' '.join(to_exec)) if use_json and output: output = json.loads(output) if may_print_to_stderr: return output, error if error: msg = ('Test code error - The command did not fail but it ' 'has a warning message. Set the "may_print_to_stderr" ' 'argument to true to get and validate the message:\n' 'Command: "%s"\nstderr: %s') % ( ' '.join(to_exec), error) raise CommandException(msg, cmd=' '.join(to_exec)) return output def rand_name(self, name='', prefix=None): """Generate a random name that includes a random number :param str name: The name that you want to include :param str prefix: The prefix that you want to include :return: a random name. The format is '--'. (e.g. 'prefixfoo-namebar-154876201') :rtype: string """ # NOTE(lajos katona): This method originally is in tempest-lib. randbits = str(random.randint(1, 0x7fffffff)) rand_name = randbits if name: rand_name = name + '-' + rand_name if prefix: rand_name = prefix + '-' + rand_name return rand_name def assertCommandFailed(self, message, func, *args, **kwargs): signature = [func] signature.extend(args) try: func(*args, **kwargs) self.fail('Command does not fail as required (%s)' % signature) except CommandException as e: self.assertIn( message, str(e), 'Command "%s" fails with different message' % e.cmd) def resource_provider_create(self, name='', parent_provider_uuid=None): if not name: name = self.rand_name(name='', prefix=RP_PREFIX) to_exec = 'resource provider create ' + name if parent_provider_uuid is not None: to_exec += ' --parent-provider ' + parent_provider_uuid res = self.openstack(to_exec, use_json=True) def cleanup(): try: self.resource_provider_delete(res['uuid']) except CommandException as exc: # may have already been deleted by a test case err_message = str(exc).lower() if 'no resource provider' not in err_message: raise self.addCleanup(cleanup) return res def resource_provider_set(self, uuid, name, parent_provider_uuid=None): to_exec = 'resource provider set ' + uuid + ' --name ' + name if parent_provider_uuid is not None: to_exec += ' --parent-provider ' + parent_provider_uuid return self.openstack(to_exec, use_json=True) def resource_provider_show(self, uuid, allocations=False): cmd = 'resource provider show ' + uuid if allocations: cmd = cmd + ' --allocations' return self.openstack(cmd, use_json=True) def resource_provider_list(self, uuid=None, name=None, aggregate_uuids=None, resources=None, in_tree=None, required=None, forbidden=None, member_of=None, may_print_to_stderr=False): to_exec = 'resource provider list' if uuid: to_exec += ' --uuid ' + uuid if name: to_exec += ' --name ' + name if aggregate_uuids: to_exec += ' ' + ' '.join( '--aggregate-uuid %s' % a for a in aggregate_uuids) if resources: to_exec += ' ' + ' '.join('--resource %s' % r for r in resources) if in_tree: to_exec += ' --in-tree ' + in_tree if required: to_exec += ' ' + ' '.join('--required %s' % t for t in required) if forbidden: to_exec += ' ' + ' '.join('--forbidden %s' % f for f in forbidden) if member_of: to_exec += ' ' + ' '.join( ['--member-of %s' % m for m in member_of]) return self.openstack( to_exec, use_json=True, may_print_to_stderr=may_print_to_stderr) def resource_provider_delete(self, uuid): return self.openstack('resource provider delete ' + uuid) def resource_allocation_show(self, consumer_uuid, columns=()): cmd = 'resource provider allocation show ' + consumer_uuid cmd += ' '.join(' --column %s' % c for c in columns) return self.openstack(cmd, use_json=True) def resource_allocation_set(self, consumer_uuid, allocations, project_id=None, user_id=None, consumer_type=None, use_json=True, may_print_to_stderr=False): cmd = 'resource provider allocation set {allocs} {uuid}'.format( uuid=consumer_uuid, allocs=' '.join('--allocation {}'.format(a) for a in allocations) ) if project_id: cmd += ' --project-id %s' % project_id if user_id: cmd += ' --user-id %s' % user_id if consumer_type: cmd += ' --consumer-type %s' % consumer_type result = self.openstack( cmd, use_json=use_json, may_print_to_stderr=may_print_to_stderr) def cleanup(uuid): try: self.openstack('resource provider allocation delete ' + uuid) except CommandException as exc: # may have already been deleted by a test case if 'not found' in str(exc).lower(): pass self.addCleanup(cleanup, consumer_uuid) return result def resource_allocation_unset( self, consumer_uuid, provider=None, resource_class=None, use_json=True, columns=(), ): cmd = 'resource provider allocation unset %s' % consumer_uuid if resource_class: cmd += ' ' + ' '.join( '--resource-class %s' % rc for rc in resource_class) if provider: # --provider can be specified multiple times so if we only get # a single string value convert to a list. if isinstance(provider, str): provider = [provider] cmd += ' ' + ' '.join( '--provider %s' % rp_uuid for rp_uuid in provider) cmd += ' '.join(' --column %s' % c for c in columns) result = self.openstack(cmd, use_json=use_json) def cleanup(uuid): try: self.openstack('resource provider allocation delete ' + uuid) except CommandException as exc: # may have already been deleted by a test case if 'not found' in str(exc).lower(): pass self.addCleanup(cleanup, consumer_uuid) return result def resource_allocation_delete(self, consumer_uuid): cmd = 'resource provider allocation delete ' + consumer_uuid return self.openstack(cmd) def resource_inventory_show( self, uuid, resource_class, *, include_used=False, ): resource = self.openstack( f'resource provider inventory show {uuid} {resource_class}', use_json=True, ) if not include_used: del resource['used'] return resource def resource_inventory_list(self, uuid, *, include_used=False): resources = self.openstack( f'resource provider inventory list {uuid}', use_json=True, ) if not include_used: for resource in resources: del resource['used'] return resources def resource_inventory_delete(self, uuid, resource_class=None): cmd = 'resource provider inventory delete {uuid}'.format(uuid=uuid) if resource_class: cmd += ' --resource-class ' + resource_class self.openstack(cmd) def resource_inventory_set(self, uuid, *resources, **kwargs): opts = [] if kwargs.get('aggregate'): opts.append('--aggregate') if kwargs.get('amend'): opts.append('--amend') if kwargs.get('dry_run'): opts.append('--dry-run') fmt = 'resource provider inventory set {uuid} {resources} {opts}' cmd = fmt.format( uuid=uuid, resources=' '.join(['--resource %s' % r for r in resources]), opts=' '.join(opts)) return self.openstack(cmd, use_json=True) def resource_inventory_class_set(self, uuid, resource_class, **kwargs): opts = ['--%s=%s' % (k, v) for k, v in kwargs.items()] cmd = 'resource provider inventory class set {uuid} {rc} {opts}'.\ format(uuid=uuid, rc=resource_class, opts=' '.join(opts)) return self.openstack(cmd, use_json=True) def resource_provider_show_usage(self, uuid): return self.openstack('resource provider usage show ' + uuid, use_json=True) def resource_show_usage(self, project_id, user_id=None): cmd = 'resource usage show %s' % project_id if user_id: cmd += ' --user-id %s' % user_id return self.openstack(cmd, use_json=True) def resource_provider_aggregate_list(self, uuid): return self.openstack('resource provider aggregate list ' + uuid, use_json=True) def resource_provider_aggregate_set(self, uuid, *aggregates, **kwargs): generation = kwargs.get('generation') cmd = 'resource provider aggregate set %s ' % uuid cmd += ' '.join('--aggregate %s' % aggregate for aggregate in aggregates) if generation is not None: cmd += ' --generation %s' % generation return self.openstack(cmd, use_json=True) def resource_class_list(self): return self.openstack('resource class list', use_json=True) def resource_class_show(self, name): return self.openstack('resource class show ' + name, use_json=True) def resource_class_create(self, name): return self.openstack('resource class create ' + name) def resource_class_set(self, name): return self.openstack('resource class set ' + name) def resource_class_delete(self, name): return self.openstack('resource class delete ' + name) def trait_list(self, name=None, associated=False): cmd = 'trait list' if name: cmd += ' --name ' + name if associated: cmd += ' --associated' return self.openstack(cmd, use_json=True) def trait_show(self, name): cmd = 'trait show %s' % name return self.openstack(cmd, use_json=True) def trait_create(self, name): cmd = 'trait create %s' % name self.openstack(cmd) def cleanup(): try: self.trait_delete(name) except CommandException as exc: # may have already been deleted by a test case err_message = str(exc).lower() if 'http 404' not in err_message: raise self.addCleanup(cleanup) def trait_delete(self, name): cmd = 'trait delete %s' % name self.openstack(cmd) def resource_provider_trait_list(self, uuid): cmd = 'resource provider trait list %s ' % uuid return self.openstack(cmd, use_json=True) def resource_provider_trait_set(self, uuid, *traits): cmd = 'resource provider trait set %s ' % uuid cmd += ' '.join('--trait %s' % trait for trait in traits) return self.openstack(cmd, use_json=True) def resource_provider_trait_delete(self, uuid): cmd = 'resource provider trait delete %s ' % uuid self.openstack(cmd) def allocation_candidate_list(self, resources=None, required=None, forbidden=None, limit=None, aggregate_uuids=None, member_of=None, may_print_to_stderr=False): cmd = 'allocation candidate list ' cmd += self._allocation_candidates_option( resources, required, forbidden, aggregate_uuids, member_of) if limit is not None: cmd += ' --limit %d' % limit return self.openstack( cmd, use_json=True, may_print_to_stderr=may_print_to_stderr) def allocation_candidate_granular(self, groups, group_policy=None, limit=None): cmd = 'allocation candidate list ' for suffix, req_group in groups.items(): if suffix: cmd += ' --group %s' % suffix cmd += self._allocation_candidates_option(**req_group) if limit is not None: cmd += ' --limit %d' % limit if group_policy is not None: cmd += ' --group-policy %s' % group_policy return self.openstack(cmd, use_json=True) def _allocation_candidates_option(self, resources=None, required=None, forbidden=None, aggregate_uuids=None, member_of=None): opt = '' if resources: opt += ' ' + ' '.join( '--resource %s' % resource for resource in resources) if required is not None: opt += ''.join([' --required %s' % t for t in required]) if forbidden: opt += ' ' + ' '.join('--forbidden %s' % f for f in forbidden) if aggregate_uuids: opt += ' ' + ' '.join( '--aggregate-uuid %s' % a for a in aggregate_uuids) if member_of: opt += ' ' + ' '.join(['--member-of %s' % m for m in member_of]) return opt ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/functional/test_aggregate.py0000664000175000017500000001617400000000000026050 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 osc_placement.tests.functional import base class TestAggregate(base.BaseTestCase): VERSION = '1.1' def test_fail_if_no_rp(self): self.assertCommandFailed( base.ARGUMENTS_MISSING, self.openstack, 'resource provider aggregate list') def test_fail_if_rp_not_found(self): self.assertCommandFailed( 'No resource provider', self.resource_provider_aggregate_list, 'fake-uuid') def test_return_empty_list_if_no_aggregates(self): rp = self.resource_provider_create() self.assertEqual( [], self.resource_provider_aggregate_list(rp['uuid'])) def test_success_set_aggregate(self): rp = self.resource_provider_create() aggs = {str(uuid.uuid4()) for _ in range(2)} rows = self.resource_provider_aggregate_set( rp['uuid'], *aggs) self.assertEqual(aggs, {r['uuid'] for r in rows}) rows = self.resource_provider_aggregate_list(rp['uuid']) self.assertEqual(aggs, {r['uuid'] for r in rows}) self.resource_provider_aggregate_set(rp['uuid']) rows = self.resource_provider_aggregate_list(rp['uuid']) self.assertEqual([], rows) def test_set_aggregate_fail_if_no_rp(self): self.assertCommandFailed( base.ARGUMENTS_MISSING, self.openstack, 'resource provider aggregate set') def test_success_set_multiple_aggregates(self): # each rp is associated with two aggregates rps = [self.resource_provider_create() for _ in range(2)] aggs = {str(uuid.uuid4()) for _ in range(2)} for rp in rps: rows = self.resource_provider_aggregate_set(rp['uuid'], *aggs) self.assertEqual(aggs, {r['uuid'] for r in rows}) # remove association for the first aggregate rows = self.resource_provider_aggregate_set(rps[0]['uuid']) self.assertEqual([], rows) # second rp should be in aggregates rows = self.resource_provider_aggregate_list(rps[1]['uuid']) self.assertEqual(aggs, {r['uuid'] for r in rows}) # cleanup rows = self.resource_provider_aggregate_set(rps[1]['uuid']) self.assertEqual([], rows) def test_success_set_large_number_aggregates(self): rp = self.resource_provider_create() aggs = {str(uuid.uuid4()) for _ in range(100)} rows = self.resource_provider_aggregate_set( rp['uuid'], *aggs) self.assertEqual(aggs, {r['uuid'] for r in rows}) rows = self.resource_provider_aggregate_set(rp['uuid']) self.assertEqual([], rows) def test_fail_if_incorrect_aggregate_uuid(self): rp = self.resource_provider_create() aggs = ['abc', 'efg'] self.assertCommandFailed( "is not a 'uuid'", self.resource_provider_aggregate_set, rp['uuid'], *aggs) # In version 1.1 a generation is not allowed. def test_fail_generation_arg_version_handling(self): rp = self.resource_provider_create() agg = str(uuid.uuid4()) self.assertCommandFailed( "Operation or argument is not supported with version 1.1", self.resource_provider_aggregate_set, rp['uuid'], agg, generation=5) class TestAggregate119(TestAggregate): VERSION = '1.19' def test_success_set_aggregate(self): rp = self.resource_provider_create() aggs = {str(uuid.uuid4()) for _ in range(2)} rows = self.resource_provider_aggregate_set( rp['uuid'], *aggs, generation=rp['generation']) self.assertEqual(aggs, {r['uuid'] for r in rows}) rows = self.resource_provider_aggregate_list(rp['uuid']) self.assertEqual(aggs, {r['uuid'] for r in rows}) self.resource_provider_aggregate_set( rp['uuid'], *[], generation=rp['generation'] + 1) rows = self.resource_provider_aggregate_list(rp['uuid']) self.assertEqual([], rows) def test_success_set_multiple_aggregates(self): # each rp is associated with two aggregates rps = [self.resource_provider_create() for _ in range(2)] aggs = {str(uuid.uuid4()) for _ in range(2)} for rp in rps: rows = self.resource_provider_aggregate_set( rp['uuid'], *aggs, generation=rp['generation']) self.assertEqual(aggs, {r['uuid'] for r in rows}) # remove association for the first aggregate rows = self.resource_provider_aggregate_set( rps[0]['uuid'], *[], generation=rp['generation'] + 1) self.assertEqual([], rows) # second rp should be in aggregates rows = self.resource_provider_aggregate_list(rps[1]['uuid']) self.assertEqual(aggs, {r['uuid'] for r in rows}) # cleanup rows = self.resource_provider_aggregate_set( rps[1]['uuid'], *[], generation=rp['generation'] + 1) self.assertEqual([], rows) def test_success_set_large_number_aggregates(self): rp = self.resource_provider_create() aggs = {str(uuid.uuid4()) for _ in range(100)} rows = self.resource_provider_aggregate_set( rp['uuid'], *aggs, generation=rp['generation']) self.assertEqual(aggs, {r['uuid'] for r in rows}) rows = self.resource_provider_aggregate_set( rp['uuid'], *[], generation=rp['generation'] + 1) self.assertEqual([], rows) def test_fail_incorrect_generation(self): rp = self.resource_provider_create() agg = str(uuid.uuid4()) self.assertCommandFailed( "Please update the generation and try again.", self.resource_provider_aggregate_set, rp['uuid'], agg, generation=99999) def test_fail_generation_not_int(self): rp = self.resource_provider_create() agg = str(uuid.uuid4()) self.assertCommandFailed( "invalid int value", self.resource_provider_aggregate_set, rp['uuid'], agg, generation='barney') def test_fail_if_incorrect_aggregate_uuid(self): rp = self.resource_provider_create() aggs = ['abc', 'efg'] self.assertCommandFailed( "is not a 'uuid'", self.resource_provider_aggregate_set, rp['uuid'], *aggs, generation=rp['generation']) # In version 1.19 a generation is required. def test_fail_generation_arg_version_handling(self): rp = self.resource_provider_create() agg = str(uuid.uuid4()) self.assertCommandFailed( "A generation must be specified.", self.resource_provider_aggregate_set, rp['uuid'], agg) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/functional/test_allocation.py0000664000175000017500000004120100000000000026234 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 osc_placement.tests.functional import base class TestAllocation(base.BaseTestCase): def setUp(self): super(TestAllocation, self).setUp() self.rp1 = self.resource_provider_create() self.resource_inventory_set( self.rp1['uuid'], 'VCPU=4', 'MEMORY_MB=1024') def test_allocation_show_not_found(self): consumer_uuid = str(uuid.uuid4()) result = self.resource_allocation_show(consumer_uuid) self.assertEqual([], result) def test_allocation_create(self): consumer_uuid = str(uuid.uuid4()) created_alloc = self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(self.rp1['uuid']), 'rp={},MEMORY_MB=512'.format(self.rp1['uuid'])] ) retrieved_alloc = self.resource_allocation_show(consumer_uuid) expected = [ {'resource_provider': self.rp1['uuid'], 'generation': 2, 'resources': {'VCPU': 2, 'MEMORY_MB': 512}} ] self.assertEqual(expected, created_alloc) self.assertEqual(expected, retrieved_alloc) # Test that specifying --project-id and --user-id before microversion # 1.8 does not result in an error but display a warning. output, warning = self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(self.rp1['uuid']), 'rp={},MEMORY_MB=512'.format(self.rp1['uuid'])], project_id='fake-project', user_id='fake-user', may_print_to_stderr=True) expected = [ {'resource_provider': self.rp1['uuid'], 'generation': 3, 'resources': {'VCPU': 2, 'MEMORY_MB': 512}} ] self.assertEqual(expected, output) self.assertIn( '--project-id and --user-id options do not affect allocation for ' '--os-placement-api-version less than 1.8', warning) # Test that specifying --consumer-type before microversion 1.38 does # not result in an error but display a warning. output, warning = self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(self.rp1['uuid']), 'rp={},MEMORY_MB=512'.format(self.rp1['uuid'])], consumer_type='fake-type', may_print_to_stderr=True) expected = [ {'resource_provider': self.rp1['uuid'], 'generation': 4, 'resources': {'VCPU': 2, 'MEMORY_MB': 512}} ] self.assertEqual(expected, output) self.assertIn( '--consumer-type option does not affect allocation for ' '--os-placement-api-version less than 1.38', warning) def test_allocation_create_empty(self): consumer_uuid = str(uuid.uuid4()) exc = self.assertRaises(base.CommandException, self.resource_allocation_set, consumer_uuid, []) self.assertIn('At least one resource allocation must be specified', str(exc)) def test_allocation_delete(self): consumer_uuid = str(uuid.uuid4()) self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(self.rp1['uuid']), 'rp={},MEMORY_MB=512'.format(self.rp1['uuid'])] ) self.assertTrue(self.resource_allocation_show(consumer_uuid)) self.resource_allocation_delete(consumer_uuid) self.assertEqual([], self.resource_allocation_show(consumer_uuid)) def test_allocation_delete_not_found(self): consumer_uuid = str(uuid.uuid4()) msg = "No allocations for consumer '{}'".format(consumer_uuid) exc = self.assertRaises(base.CommandException, self.resource_allocation_delete, consumer_uuid) self.assertIn(msg, str(exc)) class TestAllocation18(base.BaseTestCase): VERSION = '1.8' def test_allocation_create(self): consumer_uuid = str(uuid.uuid4()) project_id = str(uuid.uuid4()) user_id = str(uuid.uuid4()) rp1 = self.resource_provider_create() self.resource_inventory_set( rp1['uuid'], 'VCPU=4', 'VCPU:max_unit=4', 'MEMORY_MB=1024', 'MEMORY_MB:max_unit=1024') created_alloc = self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(rp1['uuid']), 'rp={},MEMORY_MB=512'.format(rp1['uuid'])], project_id=project_id, user_id=user_id ) retrieved_alloc = self.resource_allocation_show(consumer_uuid) expected = [ {'resource_provider': rp1['uuid'], 'generation': 2, 'resources': {'VCPU': 2, 'MEMORY_MB': 512}} ] self.assertEqual(expected, created_alloc) self.assertEqual(expected, retrieved_alloc) class TestAllocation112(base.BaseTestCase): VERSION = '1.12' def setUp(self): super(TestAllocation112, self).setUp() self.rp1 = self.resource_provider_create() self.resource_inventory_set( self.rp1['uuid'], 'VCPU=4', 'MEMORY_MB=1024') def test_allocation_update(self): consumer_uuid = str(uuid.uuid4()) project_uuid = str(uuid.uuid4()) user_uuid = str(uuid.uuid4()) created_alloc = self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(self.rp1['uuid']), 'rp={},MEMORY_MB=512'.format(self.rp1['uuid'])], project_id=project_uuid, user_id=user_uuid ) retrieved_alloc = self.resource_allocation_show(consumer_uuid) expected = [ {'resource_provider': self.rp1['uuid'], 'generation': 2, 'project_id': project_uuid, 'user_id': user_uuid, 'resources': {'VCPU': 2, 'MEMORY_MB': 512}} ] self.assertEqual(expected, created_alloc) self.assertEqual(expected, retrieved_alloc) def test_allocation_update_to_empty(self): consumer_uuid = str(uuid.uuid4()) project_uuid = str(uuid.uuid4()) user_uuid = str(uuid.uuid4()) self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(self.rp1['uuid'])], project_id=project_uuid, user_id=user_uuid, ) result = self.resource_allocation_unset( consumer_uuid, columns=("resources",)) self.assertEqual([], result) def test_allocation_show_empty(self): alloc = self.resource_allocation_show( str(uuid.uuid4()), columns=('resources',)) self.assertEqual([], alloc) class TestAllocation128(TestAllocation112): """Tests allocation set command with --os-placement-api-version 1.28. The 1.28 microversion adds the consumer_generation parameter to the GET and PUT /allocations/{consumer_id} APIs. """ VERSION = '1.28' def test_allocation_update(self): consumer_uuid = str(uuid.uuid4()) project_uuid = str(uuid.uuid4()) user_uuid = str(uuid.uuid4()) # First create the initial set of allocations using rp1. created_alloc = self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(self.rp1['uuid']), 'rp={},MEMORY_MB=512'.format(self.rp1['uuid'])], project_id=project_uuid, user_id=user_uuid ) retrieved_alloc = self.resource_allocation_show(consumer_uuid) expected = [ {'resource_provider': self.rp1['uuid'], 'generation': 2, 'project_id': project_uuid, 'user_id': user_uuid, 'resources': {'VCPU': 2, 'MEMORY_MB': 512}} ] self.assertEqual(expected, created_alloc) self.assertEqual(expected, retrieved_alloc) # Now update the allocations which should use the consumer generation. updated_alloc = self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=4'.format(self.rp1['uuid']), 'rp={},MEMORY_MB=1024'.format(self.rp1['uuid'])], project_id=project_uuid, user_id=user_uuid ) expected[0].update({ 'generation': expected[0]['generation'] + 1, 'resources': {'VCPU': 4, 'MEMORY_MB': 1024} }) self.assertEqual(expected, updated_alloc) class TestAllocation138(TestAllocation128): """Tests allocation set command with --os-placement-api-version 1.38. The 1.38 microversion adds the consumer_type parameter to the GET and PUT /allocations/{consumer_id} APIs """ VERSION = '1.38' def test_allocation_update(self): consumer_uuid = str(uuid.uuid4()) project_uuid = str(uuid.uuid4()) user_uuid = str(uuid.uuid4()) # First create the initial set of allocations using rp1. created_alloc = self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(self.rp1['uuid']), 'rp={},MEMORY_MB=512'.format(self.rp1['uuid'])], project_id=project_uuid, user_id=user_uuid, consumer_type='INSTANCE' ) retrieved_alloc = self.resource_allocation_show(consumer_uuid) expected = [ {'resource_provider': self.rp1['uuid'], 'generation': 2, 'project_id': project_uuid, 'user_id': user_uuid, 'resources': {'VCPU': 2, 'MEMORY_MB': 512}, 'consumer_type': 'INSTANCE'} ] self.assertEqual(expected, created_alloc) self.assertEqual(expected, retrieved_alloc) # Now update the allocations which should use the consumer generation. updated_alloc = self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=4'.format(self.rp1['uuid']), 'rp={},MEMORY_MB=1024'.format(self.rp1['uuid'])], project_id=project_uuid, user_id=user_uuid, consumer_type='MIGRATION' ) expected[0].update({ 'generation': expected[0]['generation'] + 1, 'resources': {'VCPU': 4, 'MEMORY_MB': 1024}, 'consumer_type': 'MIGRATION' }) self.assertEqual(expected, updated_alloc) def test_allocation_update_to_empty(self): consumer_uuid = str(uuid.uuid4()) project_uuid = str(uuid.uuid4()) user_uuid = str(uuid.uuid4()) self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(self.rp1['uuid'])], project_id=project_uuid, user_id=user_uuid, consumer_type="INSTANCE", ) result = self.resource_allocation_unset( consumer_uuid, columns=('resources', 'consumer_type')) self.assertEqual([], result) class TestAllocationUnsetOldVersion(base.BaseTestCase): def test_invalid_version(self): """Negative test to ensure the unset command requires >= 1.12.""" consumer_uuid = str(uuid.uuid4()) self.assertCommandFailed('requires at least version 1.12', self.resource_allocation_unset, consumer_uuid) class TestAllocationUnset112(base.BaseTestCase): VERSION = '1.12' def setUp(self): super(TestAllocationUnset112, self).setUp() # Create four providers with inventory. self.rp1 = self.resource_provider_create() self.rp2 = self.resource_provider_create() self.rp3 = self.resource_provider_create() self.rp4 = self.resource_provider_create() self.resource_inventory_set( self.rp1['uuid'], 'VCPU=4', 'MEMORY_MB=1024') self.resource_inventory_set( self.rp2['uuid'], 'VGPU=1') self.resource_inventory_set( self.rp3['uuid'], 'VCPU=4', 'MEMORY_MB=1024', 'VGPU=1') self.resource_inventory_set( self.rp4['uuid'], 'VCPU=4', 'MEMORY_MB=1024', 'VGPU=1') self.consumer_uuid1 = str(uuid.uuid4()) self.consumer_uuid2 = str(uuid.uuid4()) self.project_uuid = str(uuid.uuid4()) self.user_uuid = str(uuid.uuid4()) # Create allocations against rp1 and rp2 for consumer1. self.resource_allocation_set( self.consumer_uuid1, ['rp={},VCPU=2'.format(self.rp1['uuid']), 'rp={},MEMORY_MB=512'.format(self.rp1['uuid']), 'rp={},VGPU=1'.format(self.rp2['uuid'])], project_id=self.project_uuid, user_id=self.user_uuid) # Create allocations against rp3 and rp4 for consumer1. self.resource_allocation_set( self.consumer_uuid2, ['rp={},VCPU=1'.format(self.rp3['uuid']), 'rp={},MEMORY_MB=256'.format(self.rp3['uuid']), 'rp={},VGPU=1'.format(self.rp3['uuid']), 'rp={},VCPU=1'.format(self.rp4['uuid']), 'rp={},MEMORY_MB=256'.format(self.rp4['uuid'])], project_id=self.project_uuid, user_id=self.user_uuid) def test_allocation_unset_one_provider(self): """Tests removing allocations for one specific provider.""" # Remove the allocation for rp1. updated_allocs = self.resource_allocation_unset( self.consumer_uuid1, provider=self.rp1['uuid']) expected = [ {'resource_provider': self.rp2['uuid'], 'generation': 3, 'project_id': self.project_uuid, 'user_id': self.user_uuid, 'resources': {'VGPU': 1}} ] self.assertEqual(expected, updated_allocs) def test_allocation_unset_one_resource_class(self): """Tests removing allocations for resource classes.""" updated_allocs = self.resource_allocation_unset( self.consumer_uuid2, resource_class=['MEMORY_MB']) expected = [ {'resource_provider': self.rp3['uuid'], 'generation': 3, 'project_id': self.project_uuid, 'user_id': self.user_uuid, 'resources': {'VCPU': 1, 'VGPU': 1}}, {'resource_provider': self.rp4['uuid'], 'generation': 3, 'project_id': self.project_uuid, 'user_id': self.user_uuid, 'resources': {'VCPU': 1}} ] self.assertEqual(expected, updated_allocs) def test_allocation_unset_resource_classes(self): """Tests removing allocations for resource classes.""" updated_allocs = self.resource_allocation_unset( self.consumer_uuid2, resource_class=['VCPU', 'MEMORY_MB']) expected = [ {'resource_provider': self.rp3['uuid'], 'generation': 3, 'project_id': self.project_uuid, 'user_id': self.user_uuid, 'resources': {'VGPU': 1}} ] self.assertEqual(expected, updated_allocs) def test_allocation_unset_provider_and_rc(self): """Tests removing allocations of resource classes for a provider .""" updated_allocs = self.resource_allocation_unset( self.consumer_uuid2, provider=self.rp3['uuid'], resource_class=['VCPU', 'MEMORY_MB']) expected = [ {'resource_provider': self.rp3['uuid'], 'generation': 3, 'project_id': self.project_uuid, 'user_id': self.user_uuid, 'resources': {'VGPU': 1}}, {'resource_provider': self.rp4['uuid'], 'generation': 3, 'project_id': self.project_uuid, 'user_id': self.user_uuid, 'resources': {'VCPU': 1, 'MEMORY_MB': 256}}, ] self.assertEqual(expected, updated_allocs) def test_allocation_unset_remove_all_providers(self): """Tests removing all allocations by omitting the --provider option.""" # For this test pass use_json=False to make sure we get nothing back # in the output since there are no more allocations. updated_allocs = self.resource_allocation_unset( self.consumer_uuid1, use_json=False) self.assertEqual('', updated_allocs.strip()) class TestAllocationUnset128(TestAllocationUnset112): """Tests allocation unset command with --os-placement-api-version 1.28. The 1.28 microversion adds the consumer_generation parameter to the GET and PUT /allocations/{consumer_id} APIs. """ VERSION = '1.28' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/functional/test_allocation_candidate.py0000664000175000017500000005260600000000000030243 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 osc_placement.tests.functional import base def sorted_resources(resource): return ','.join(sorted(resource.split(','))) class TestAllocationCandidate(base.BaseTestCase): VERSION = '1.10' def test_list_no_resource_specified_error(self): self.assertCommandFailed( 'At least one --resource must be specified', self.openstack, 'allocation candidate list') def test_list_non_key_value_resource_specified_error(self): self.assertCommandFailed( 'Arguments to --resource must be of form ' '=', self.openstack, 'allocation candidate list --resource VCPU') def test_list_empty(self): self.assertEqual([], self.allocation_candidate_list( resources=['MEMORY_MB=999999999'])) def test_list_one(self): rp = self.resource_provider_create() self.resource_inventory_set(rp['uuid'], 'MEMORY_MB=1024') candidates = self.allocation_candidate_list( resources=('MEMORY_MB=256',)) self.assertIn( rp['uuid'], [candidate['resource provider'] for candidate in candidates]) def assertResourceEqual(self, r1, r2): self.assertEqual(sorted_resources(r1), sorted_resources(r2)) def test_list_multiple(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() self.resource_inventory_set( rp1['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') self.resource_inventory_set( rp2['uuid'], 'MEMORY_MB=16384', 'DISK_GB=1024') candidates = self.allocation_candidate_list( resources=('MEMORY_MB=1024', 'DISK_GB=80')) rps = {c['resource provider']: c for c in candidates} self.assertResourceEqual( 'MEMORY_MB=1024,DISK_GB=80', rps[rp1['uuid']]['allocation']) self.assertResourceEqual( 'MEMORY_MB=1024,DISK_GB=80', rps[rp2['uuid']]['allocation']) self.assertResourceEqual( 'MEMORY_MB=0/8192,DISK_GB=0/512', rps[rp1['uuid']]['inventory used/capacity']) self.assertResourceEqual( 'MEMORY_MB=0/16384,DISK_GB=0/1024', rps[rp2['uuid']]['inventory used/capacity']) def test_list_shared(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() self.resource_inventory_set(rp1['uuid'], 'MEMORY_MB=8192') self.resource_inventory_set(rp2['uuid'], 'DISK_GB=1024') agg = str(uuid.uuid4()) self.resource_provider_aggregate_set(rp1['uuid'], agg) self.resource_provider_aggregate_set(rp2['uuid'], agg) self.resource_provider_trait_set( rp2['uuid'], 'MISC_SHARES_VIA_AGGREGATE') candidates = self.allocation_candidate_list( resources=('MEMORY_MB=1024', 'DISK_GB=80')) rps = {c['resource provider']: c for c in candidates} self.assertResourceEqual( 'MEMORY_MB=1024', rps[rp1['uuid']]['allocation']) self.assertResourceEqual( 'DISK_GB=80', rps[rp2['uuid']]['allocation']) self.assertResourceEqual( 'MEMORY_MB=0/8192', rps[rp1['uuid']]['inventory used/capacity']) self.assertResourceEqual( 'DISK_GB=0/1024', rps[rp2['uuid']]['inventory used/capacity']) self.assertEqual( rps[rp2['uuid']]['#'], rps[rp1['uuid']]['#']) def test_fail_if_unknown_rc(self): self.assertCommandFailed( 'No such resource', self.allocation_candidate_list, resources=('UNKNOWN=10',)) class TestAllocationCandidate112(TestAllocationCandidate): VERSION = '1.12' class TestAllocationCandidate116(base.BaseTestCase): VERSION = '1.16' def test_list_limit(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() self.resource_inventory_set( rp1['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') self.resource_inventory_set( rp2['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') unlimited = self.allocation_candidate_list( resources=('MEMORY_MB=1024', 'DISK_GB=80')) self.assertTrue(len(set([row['#'] for row in unlimited])) > 1) limited = self.allocation_candidate_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), limit=1) self.assertEqual(len(set([row['#'] for row in limited])), 1) class TestAllocationCandidate117(base.BaseTestCase): VERSION = '1.17' # NOTE(cdent): The choice of traits here is important. We need to # make sure that we do not overlap with 'test_show_required_trait' # in TestResourceProvider118 which also creates some resource # providers. In a multi-process enviromment, the tests can race. def test_show_required_trait(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() self.resource_inventory_set( rp1['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') self.resource_inventory_set( rp2['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') self.resource_provider_trait_set( rp1['uuid'], 'STORAGE_DISK_SSD', 'HW_NIC_SRIOV') self.resource_provider_trait_set( rp2['uuid'], 'STORAGE_DISK_HDD', 'HW_NIC_SRIOV') rps = self.allocation_candidate_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=('STORAGE_DISK_SSD',)) candidate_dict = {rp['resource provider']: rp for rp in rps} self.assertIn(rp1['uuid'], candidate_dict) self.assertNotIn(rp2['uuid'], candidate_dict) self.assertEqual( set(candidate_dict[rp1['uuid']]['traits'].split(',')), set(['STORAGE_DISK_SSD', 'HW_NIC_SRIOV'])) # Prior to version 1.21 use the --aggregate-uuid arg should # be an errror. def test_fail_if_aggregate_uuid_wrong_version(self): self.assertCommandFailed( 'Operation or argument is not supported with version 1.17', self.allocation_candidate_list, resources=('MEMORY_MB=1024', 'DISK_GB=80'), aggregate_uuids=[str(uuid.uuid4())]) # ...so as --member_of option self.assertCommandFailed( 'Operation or argument is not supported with version 1.17', self.allocation_candidate_list, resources=('MEMORY_MB=1024', 'DISK_GB=80'), member_of=[str(uuid.uuid4())]) class TestAllocationCandidate121(base.BaseTestCase): VERSION = '1.21' def test_return_properly_for_aggregate_uuid_request(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() self.resource_inventory_set( rp1['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') self.resource_inventory_set( rp2['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') agg = str(uuid.uuid4()) self.resource_provider_aggregate_set( rp2['uuid'], agg, generation=1) # use --aggregate_uuids option rps, warning = self.allocation_candidate_list( resources=('MEMORY_MB=1024',), aggregate_uuids=[agg, str(uuid.uuid4())], may_print_to_stderr=True) candidate_dict = {rp['resource provider']: rp for rp in rps} self.assertEqual(1, len(candidate_dict)) self.assertIn(rp2['uuid'], candidate_dict) self.assertNotIn(rp1['uuid'], candidate_dict) self.assertIn('The --aggregate-uuid option is deprecated, ' 'please use --member-of instead.', warning) # validate --member_of option works as expected rps = self.allocation_candidate_list( resources=('MEMORY_MB=1024',), member_of=[agg]) candidate_dict = {rp['resource provider']: rp for rp in rps} self.assertEqual(1, len(candidate_dict)) self.assertIn(rp2['uuid'], candidate_dict) self.assertNotIn(rp1['uuid'], candidate_dict) # Specifying forbidden traits weren't available until version 1.22 def test_fail_if_forbidden_trait_wrong_version(self): self.assertCommandFailed( 'Operation or argument is not supported with version 1.21', self.allocation_candidate_list, resources=('MEMORY_MB=1024', 'DISK_GB=80'), forbidden=('STORAGE_DISK_HDD',)) class TestAllocationCandidate122(base.BaseTestCase): VERSION = '1.22' def test_hide_forbidden_trait(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() rp3 = self.resource_provider_create() self.resource_inventory_set( rp1['uuid'], 'MEMORY_MB=1024', 'DISK_GB=256') self.resource_inventory_set( rp2['uuid'], 'MEMORY_MB=1024', 'DISK_GB=256') self.resource_inventory_set( rp3['uuid'], 'MEMORY_MB=1024', 'DISK_GB=256') self.resource_provider_trait_set( rp1['uuid'], 'STORAGE_DISK_SSD', 'HW_CPU_X86_BMI') self.resource_provider_trait_set( rp2['uuid'], 'STORAGE_DISK_HDD', 'HW_CPU_X86_BMI') self.resource_provider_trait_set( rp3['uuid'], 'STORAGE_DISK_HDD', 'HW_CPU_X86_BMI') rps = self.allocation_candidate_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=('HW_CPU_X86_BMI',), forbidden=('STORAGE_DISK_HDD',)) self.assertEqual(1, len(rps)) self.assertEqual(rp1['uuid'], rps[0]['resource provider']) rps = self.allocation_candidate_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=('HW_CPU_X86_BMI',), forbidden=('STORAGE_DISK_SSD',)) uuids = [rp['resource provider'] for rp in rps] self.assertEqual(2, len(uuids)) self.assertNotIn(rp1['uuid'], uuids) self.assertIn(rp2['uuid'], uuids) self.assertIn(rp3['uuid'], uuids) class TestAllocationCandidate124(base.BaseTestCase): VERSION = '1.24' def test_member_of(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() self.resource_inventory_set( rp1['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') self.resource_inventory_set( rp2['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') agg1 = str(uuid.uuid4()) agg2 = str(uuid.uuid4()) agg3 = str(uuid.uuid4()) self.resource_provider_aggregate_set( rp1['uuid'], agg1, agg3, generation=1) self.resource_provider_aggregate_set( rp2['uuid'], agg2, agg3, generation=1) agg1and3 = [agg1, agg3] agg1or3 = [agg1 + ',' + agg3] agg1or3_and_agg2 = [agg1 + ',' + agg3, agg2] rps = self.allocation_candidate_list(resources=('MEMORY_MB=1024',), member_of=agg1and3) candidate_dict = {rp['resource provider']: rp for rp in rps} self.assertEqual(1, len(candidate_dict)) self.assertIn(rp1['uuid'], candidate_dict) rps = self.allocation_candidate_list(resources=('MEMORY_MB=1024',), member_of=agg1or3) candidate_dict = {rp['resource provider']: rp for rp in rps} self.assertEqual(2, len(candidate_dict)) self.assertIn(rp1['uuid'], candidate_dict) self.assertIn(rp2['uuid'], candidate_dict) rps = self.allocation_candidate_list(resources=('MEMORY_MB=1024',), member_of=agg1or3_and_agg2) candidate_dict = {rp['resource provider']: rp for rp in rps} self.assertEqual(1, len(candidate_dict)) self.assertIn(rp2['uuid'], candidate_dict) def test_fail_granular_wrong_version(self): groups = {'1': {'resources': ('VCPU=3',)}} self.assertCommandFailed( 'Operation or argument is not supported with version 1.24', self.allocation_candidate_granular, groups=groups) class TestAllocationCandidate129(base.BaseTestCase): VERSION = '1.29' def setUp(self): super(TestAllocationCandidate129, self).setUp() self.rp1 = self.resource_provider_create() self.rp1_1 = self.resource_provider_create( parent_provider_uuid=self.rp1['uuid']) self.rp1_2 = self.resource_provider_create( parent_provider_uuid=self.rp1['uuid']) self.agg1 = str(uuid.uuid4()) self.agg2 = str(uuid.uuid4()) self.resource_provider_aggregate_set( self.rp1_1['uuid'], self.agg1, generation=0) self.resource_provider_aggregate_set( self.rp1_2['uuid'], self.agg2, generation=0) self.resource_inventory_set(self.rp1['uuid'], 'DISK_GB=512') self.resource_inventory_set( self.rp1_1['uuid'], 'VCPU=8', 'MEMORY_MB=8192') self.resource_inventory_set( self.rp1_2['uuid'], 'VCPU=16', 'MEMORY_MB=8192') self.resource_provider_trait_set(self.rp1_1['uuid'], 'HW_CPU_X86_AVX') self.resource_provider_trait_set(self.rp1_2['uuid'], 'HW_CPU_X86_SSE') def test_granular_one_group(self): groups = { '1': {'resources': ('VCPU=3',)} } rows = self.allocation_candidate_granular(groups=groups) self.assertEqual(2, len(rows)) numbers = {row['#'] for row in rows} self.assertEqual(2, len(numbers)) rps = {row['resource provider'] for row in rows} self.assertEqual(2, len(rps)) self.assertIn(self.rp1_1['uuid'], rps) self.assertIn(self.rp1_2['uuid'], rps) def test_granular_two_groups(self): groups = { '1': {'resources': ('VCPU=3',)}, '2': {'resources': ('VCPU=3',)} } rows = self.allocation_candidate_granular(groups=groups) self.assertEqual(6, len(rows)) numbers = {row['#'] for row in rows} self.assertEqual(4, len(numbers)) rps = {row['resource provider'] for row in rows} self.assertEqual(2, len(rps)) self.assertIn(self.rp1_1['uuid'], rps) self.assertIn(self.rp1_2['uuid'], rps) rows = self.allocation_candidate_granular(groups=groups, group_policy='isolate') self.assertEqual(4, len(rows)) numbers = {row['#'] for row in rows} self.assertEqual(2, len(numbers)) rps = {row['resource provider'] for row in rows} self.assertEqual(2, len(rps)) self.assertIn(self.rp1_1['uuid'], rps) self.assertIn(self.rp1_2['uuid'], rps) rows = self.allocation_candidate_granular(groups=groups, group_policy='isolate', limit=1) self.assertEqual(2, len(rows)) numbers = {row['#'] for row in rows} self.assertEqual(1, len(numbers)) rps = {row['resource provider'] for row in rows} self.assertEqual(2, len(rps)) self.assertIn(self.rp1_1['uuid'], rps) self.assertIn(self.rp1_2['uuid'], rps) def test_granular_traits1(self): groups = { '1': {'resources': ('VCPU=6',)}, '2': {'resources': ('VCPU=10',), 'required': ['HW_CPU_X86_AVX']} } rows = self.allocation_candidate_granular(groups=groups, group_policy='isolate') self.assertEqual(0, len(rows)) def test_granular_traits2(self): groups = { '1': {'resources': ('VCPU=6',)}, '2': {'resources': ('VCPU=10',), 'required': ['HW_CPU_X86_SSE']} } rows = self.allocation_candidate_granular(groups=groups, group_policy='isolate') self.assertEqual(2, len(rows)) numbers = {row['#'] for row in rows} self.assertEqual(1, len(numbers)) rps = {row['resource provider'] for row in rows} self.assertEqual(2, len(rps)) self.assertIn(self.rp1_1['uuid'], rps) self.assertIn(self.rp1_2['uuid'], rps) def test_list_with_any_traits_old_microversion(self): groups = { '': { 'resources': ('DISK_GB=1',), 'required': ('STORAGE_DISK_HDD,STORAGE_DISK_SSD',), }, '1': { 'resources': ('VCPU=1',), 'required': ('HW_CPU_X86_AVX',), } } self.assertCommandFailed( 'Operation or argument is not supported with version 1.29', self.allocation_candidate_granular, groups=groups ) class TestAllocationCandidate139(base.BaseTestCase): VERSION = '1.39' def setUp(self): super(TestAllocationCandidate139, self).setUp() self.rp1 = self.resource_provider_create() self.rp1_1 = self.resource_provider_create( parent_provider_uuid=self.rp1['uuid']) self.rp1_2 = self.resource_provider_create( parent_provider_uuid=self.rp1['uuid']) self.resource_inventory_set(self.rp1['uuid'], 'DISK_GB=512') self.resource_inventory_set( self.rp1_1['uuid'], 'VCPU=8', 'MEMORY_MB=8192') self.resource_inventory_set( self.rp1_2['uuid'], 'VCPU=16', 'MEMORY_MB=8192') self.resource_provider_trait_set(self.rp1['uuid'], 'STORAGE_DISK_HDD') self.resource_provider_trait_set(self.rp1_1['uuid'], 'HW_CPU_X86_AVX') self.resource_provider_trait_set(self.rp1_2['uuid'], 'HW_CPU_X86_SSE') self.rp2 = self.resource_provider_create() self.rp2_1 = self.resource_provider_create( parent_provider_uuid=self.rp2['uuid']) self.rp2_2 = self.resource_provider_create( parent_provider_uuid=self.rp2['uuid']) self.resource_inventory_set(self.rp2['uuid'], 'DISK_GB=512') self.resource_inventory_set( self.rp2_1['uuid'], 'VCPU=8', 'MEMORY_MB=8192') self.resource_inventory_set( self.rp2_2['uuid'], 'VCPU=16', 'MEMORY_MB=8192') self.resource_provider_trait_set(self.rp2['uuid'], 'STORAGE_DISK_SSD') self.resource_provider_trait_set(self.rp2_1['uuid'], 'HW_CPU_X86_AVX') self.resource_provider_trait_set(self.rp2_2['uuid'], 'HW_CPU_X86_SSE') def test_list_with_any_traits(self): # asking for HDD and AVX that is only on the first tree as the second # has SSD instead groups = { '': { 'resources': ('DISK_GB=1',), 'required': ('STORAGE_DISK_HDD',), }, '1': { 'resources': ('VCPU=1',), 'required': ('HW_CPU_X86_AVX',), } } rows = self.allocation_candidate_granular(groups=groups) # we expect one candidate numbers = {row['#'] for row in rows} self.assertEqual(1, len(numbers)) # with two groups satisfied self.assertEqual(2, len(rows)) rps = {row['resource provider'] for row in rows} self.assertEqual({self.rp1['uuid'], self.rp1_1['uuid']}, rps) # extend this by asking for SSD or HDD groups = { '': { 'resources': ('DISK_GB=1',), 'required': ('STORAGE_DISK_HDD,STORAGE_DISK_SSD',), }, '1': { 'resources': ('VCPU=1',), 'required': ('HW_CPU_X86_AVX',), } } rows = self.allocation_candidate_granular(groups=groups) # we expect two candidates now as both tree matches numbers = {row['#'] for row in rows} self.assertEqual(2, len(numbers)) # with two groups satisfied each self.assertEqual(4, len(rows)) rps = {row['resource provider'] for row in rows} self.assertEqual( { self.rp1['uuid'], self.rp1_1['uuid'], self.rp2['uuid'], self.rp2_1['uuid'], }, rps ) # make it crazy by asking for (HDD or SSD) and SSD and not HDD # this basically means SSD but tests all the branches of the client # code # similarly for the granular group ask for (AVX or SSE) and not SSE groups = { '': { 'resources': ('DISK_GB=1',), 'required': ( 'STORAGE_DISK_HDD,STORAGE_DISK_SSD', 'STORAGE_DISK_SSD' ), 'forbidden': ('STORAGE_DISK_HDD',), }, '1': { 'resources': ('VCPU=1',), 'required': ('HW_CPU_X86_AVX,HW_CPU_X86_SSE',), 'forbidden': ('HW_CPU_X86_SSE',), } } rows = self.allocation_candidate_granular(groups=groups) # SSD and AVX means we only the second tree matches with a single # candidate numbers = {row['#'] for row in rows} self.assertEqual(1, len(numbers)) # with two groups satisfied self.assertEqual(2, len(rows)) rps = {row['resource provider'] for row in rows} self.assertEqual({self.rp2['uuid'], self.rp2_1['uuid']}, rps) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/functional/test_inventory.py0000664000175000017500000005503500000000000026156 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 copy import uuid from osc_placement.tests.functional import base class TestInventory(base.BaseTestCase): def setUp(self): super(TestInventory, self).setUp() self.rp = self.resource_provider_create() def test_inventory_show(self): rp_uuid = self.rp['uuid'] updates = { 'min_unit': 1, 'max_unit': 12, 'reserved': 0, 'step_size': 1, 'total': 12, 'allocation_ratio': 16.0, } expected = updates.copy() expected['used'] = 0 args = ['VCPU:%s=%s' % (k, v) for k, v in updates.items()] self.resource_inventory_set(rp_uuid, *args) self.assertEqual( expected, self.resource_inventory_show(rp_uuid, 'VCPU', include_used=True), ) def test_inventory_show_not_found(self): rp_uuid = self.rp['uuid'] exc = self.assertRaises(base.CommandException, self.resource_inventory_show, rp_uuid, 'VCPU') self.assertIn('No inventory of class VCPU for {}'.format(rp_uuid), str(exc)) def test_inventory_list(self): rp_uuid = self.rp['uuid'] updates = { 'min_unit': 1, 'max_unit': 12, 'reserved': 0, 'step_size': 1, 'total': 12, 'allocation_ratio': 16.0, } expected = [updates.copy()] expected[0]['resource_class'] = 'VCPU' expected[0]['used'] = 0 args = ['VCPU:%s=%s' % (k, v) for k, v in updates.items()] self.resource_inventory_set(rp_uuid, *args) self.assertEqual( expected, self.resource_inventory_list(rp_uuid, include_used=True), ) def test_inventory_delete(self): rp_uuid = self.rp['uuid'] self.resource_inventory_set(rp_uuid, 'VCPU=8') self.resource_inventory_delete(rp_uuid, 'VCPU') exc = self.assertRaises(base.CommandException, self.resource_inventory_show, rp_uuid, 'VCPU') self.assertIn('No inventory of class VCPU for {}'.format(rp_uuid), str(exc)) def test_inventory_delete_not_found(self): exc = self.assertRaises(base.CommandException, self.resource_inventory_delete, self.rp['uuid'], 'VCPU') self.assertIn('No inventory of class VCPU found for delete', str(exc)) def test_delete_all_inventories(self): # Negative test to assert command failure because # microversion < 1.5 and --resource-class is not specified. self.assertCommandFailed( base.ARGUMENTS_REQUIRED % '--resource-class', self.resource_inventory_delete, 'fake_uuid') class TestSetInventory(base.BaseTestCase): def test_fail_if_no_rp(self): exc = self.assertRaises( base.CommandException, self.openstack, 'resource provider inventory set') self.assertIn(base.ARGUMENTS_MISSING, str(exc)) def test_set_empty_inventories(self): rp = self.resource_provider_create() self.assertEqual([], self.resource_inventory_set(rp['uuid'])) def test_fail_if_incorrect_resource(self): rp = self.resource_provider_create() # wrong format exc = self.assertRaises(base.CommandException, self.resource_inventory_set, rp['uuid'], 'VCPU') self.assertIn('must have "name=value"', str(exc)) exc = self.assertRaises(base.CommandException, self.resource_inventory_set, rp['uuid'], 'VCPU==') self.assertIn('must have "name=value"', str(exc)) exc = self.assertRaises(base.CommandException, self.resource_inventory_set, rp['uuid'], '=10') self.assertIn('must be not empty', str(exc)) exc = self.assertRaises(base.CommandException, self.resource_inventory_set, rp['uuid'], 'v=') self.assertIn('must be not empty', str(exc)) # unknown class exc = self.assertRaises(base.CommandException, self.resource_inventory_set, rp['uuid'], 'UNKNOWN_CPU=16') self.assertIn('Unknown resource class', str(exc)) # unknown property exc = self.assertRaises(base.CommandException, self.resource_inventory_set, rp['uuid'], 'VCPU:fake=16') self.assertIn('Unknown inventory field', str(exc)) def test_set_multiple_classes(self): rp = self.resource_provider_create() resp = self.resource_inventory_set( rp['uuid'], 'VCPU=8', 'VCPU:max_unit=4', 'MEMORY_MB=1024', 'MEMORY_MB:reserved=256', 'DISK_GB=16', 'DISK_GB:allocation_ratio=1.5', 'DISK_GB:min_unit=2', 'DISK_GB:step_size=2') def check(inventories): self.assertEqual(8, inventories['VCPU']['total']) self.assertEqual(4, inventories['VCPU']['max_unit']) self.assertEqual(1024, inventories['MEMORY_MB']['total']) self.assertEqual(256, inventories['MEMORY_MB']['reserved']) self.assertEqual(16, inventories['DISK_GB']['total']) self.assertEqual(2, inventories['DISK_GB']['min_unit']) self.assertEqual(2, inventories['DISK_GB']['step_size']) self.assertEqual(1.5, inventories['DISK_GB']['allocation_ratio']) check({r['resource_class']: r for r in resp}) resp = self.resource_inventory_list(rp['uuid']) check({r['resource_class']: r for r in resp}) def test_set_known_and_unknown_class(self): rp = self.resource_provider_create() exc = self.assertRaises(base.CommandException, self.resource_inventory_set, rp['uuid'], 'VCPU=8', 'UNKNOWN=4') self.assertIn('Unknown resource class', str(exc)) self.assertEqual([], self.resource_inventory_list(rp['uuid'])) def test_replace_previous_values(self): """Test each new set call replaces previous inventories totally.""" rp = self.resource_provider_create() # set disk inventory first self.resource_inventory_set(rp['uuid'], 'DISK_GB=16') # set memory and vcpu inventories self.resource_inventory_set(rp['uuid'], 'MEMORY_MB=16', 'VCPU=32') resp = self.resource_inventory_list(rp['uuid']) inv = {r['resource_class']: r for r in resp} # no disk inventory as it was overwritten self.assertNotIn('DISK_GB', inv) self.assertIn('VCPU', inv) self.assertIn('MEMORY_MB', inv) def test_delete_via_set(self): rp = self.resource_provider_create() self.resource_inventory_set(rp['uuid'], 'DISK_GB=16') self.resource_inventory_set(rp['uuid']) self.assertEqual([], self.resource_inventory_list(rp['uuid'])) def test_fail_if_incorrect_parameters_set_class_inventory(self): exc = self.assertRaises( base.CommandException, self.openstack, 'resource provider inventory class set') self.assertIn(base.ARGUMENTS_MISSING, str(exc)) exc = self.assertRaises( base.CommandException, self.openstack, 'resource provider inventory class set fake_uuid') self.assertIn(base.ARGUMENTS_MISSING, str(exc)) exc = self.assertRaises( base.CommandException, self.openstack, ('resource provider inventory class set ' 'fake_uuid fake_class --total 5 --unknown 1')) self.assertIn('unrecognized arguments', str(exc)) # Valid RP UUID and resource class, but no inventory field. rp = self.resource_provider_create() exc = self.assertRaises( base.CommandException, self.openstack, 'resource provider inventory class set %s VCPU' % rp['uuid']) self.assertIn(base.ARGUMENTS_REQUIRED % '--total', str(exc)) def test_set_inventory_for_resource_class(self): rp = self.resource_provider_create() self.resource_inventory_set(rp['uuid'], 'MEMORY_MB=16', 'VCPU=32') self.resource_inventory_class_set( rp['uuid'], 'MEMORY_MB', total=128, step_size=16) resp = self.resource_inventory_list(rp['uuid']) inv = {r['resource_class']: r for r in resp} self.assertEqual(128, inv['MEMORY_MB']['total']) self.assertEqual(16, inv['MEMORY_MB']['step_size']) self.assertEqual(32, inv['VCPU']['total']) def test_fail_aggregate_arg_version_handling(self): agg = str(uuid.uuid4()) self.assertCommandFailed( 'Operation or argument is not supported with version 1.0; ' 'requires at least version 1.3', self.resource_inventory_set, agg, 'MEMORY_MB=16', aggregate=True) def test_amend_multiple_classes(self): # Tests that amending previously nonexistent resource class inventories # results in creation of inventory for them # Create a resource provider with no inventory rp = self.resource_provider_create() resp = self.resource_inventory_set( rp['uuid'], 'VCPU=8', 'VCPU:max_unit=4', 'MEMORY_MB=1024', 'MEMORY_MB:reserved=256', 'DISK_GB=16', 'DISK_GB:allocation_ratio=1.5', 'DISK_GB:min_unit=2', 'DISK_GB:step_size=2', amend=True) def check(inventories): self.assertEqual(8, inventories['VCPU']['total']) self.assertEqual(4, inventories['VCPU']['max_unit']) self.assertEqual(1024, inventories['MEMORY_MB']['total']) self.assertEqual(256, inventories['MEMORY_MB']['reserved']) self.assertEqual(16, inventories['DISK_GB']['total']) self.assertEqual(1.5, inventories['DISK_GB']['allocation_ratio']) self.assertEqual(2, inventories['DISK_GB']['min_unit']) self.assertEqual(2, inventories['DISK_GB']['step_size']) inventories = {r['resource_class']: r for r in resp} check(inventories) resp = self.resource_inventory_list(rp['uuid']) inventories = {r['resource_class']: r for r in resp} check(inventories) # Test amending of one resource class inventory resp = self.resource_inventory_set( rp['uuid'], 'VCPU:allocation_ratio=5.0', amend=True) inventories = {r['resource_class']: r for r in resp} check(inventories) self.assertEqual(5.0, inventories['VCPU']['allocation_ratio']) resp = self.resource_inventory_list(rp['uuid']) inventories = {r['resource_class']: r for r in resp} check(inventories) self.assertEqual(5.0, inventories['VCPU']['allocation_ratio']) def test_dry_run(self): rp = self.resource_provider_create() resp = self.resource_inventory_set( rp['uuid'], 'VCPU=8', 'VCPU:max_unit=4', 'MEMORY_MB=1024', 'MEMORY_MB:reserved=256', 'DISK_GB=16', 'DISK_GB:allocation_ratio=1.5', 'DISK_GB:min_unit=2', 'DISK_GB:step_size=2', dry_run=True) def check(inventories): self.assertEqual(8, inventories['VCPU']['total']) self.assertEqual(4, inventories['VCPU']['max_unit']) self.assertEqual(1024, inventories['MEMORY_MB']['total']) self.assertEqual(256, inventories['MEMORY_MB']['reserved']) self.assertEqual(16, inventories['DISK_GB']['total']) self.assertEqual(2, inventories['DISK_GB']['min_unit']) self.assertEqual(2, inventories['DISK_GB']['step_size']) self.assertEqual(1.5, inventories['DISK_GB']['allocation_ratio']) # We expect the return value from the set command to reflect the values # passed to the command (a preview of what would be set if not for # --dry-run) check({r['resource_class']: r for r in resp}) # But we expect the return value from the list command to be empty # since we used --dry-run and didn't actually effect any changes resp = self.resource_inventory_list(rp['uuid']) self.assertEqual([], resp) class TestInventory15(TestInventory): VERSION = '1.5' def test_delete_all_inventories(self): rp = self.resource_provider_create() self.resource_inventory_set(rp['uuid'], 'MEMORY_MB=16', 'VCPU=32') self.resource_inventory_delete(rp['uuid']) self.assertEqual([], self.resource_inventory_list(rp['uuid'])) class TestAggregateInventory(base.BaseTestCase): VERSION = '1.3' def _test_dry_run(self, agg, rps, old_inventories, amend=False): new_resources = ['VCPU:allocation_ratio=5.0', 'MEMORY_MB:allocation_ratio=6.0', 'DISK_GB:allocation_ratio=7.0'] resp = self.resource_inventory_set( agg, *new_resources, aggregate=True, amend=amend, dry_run=True) # Use empty dict to get expected values for full replacement inventories = old_inventories if amend else [{}] * len(old_inventories) # A list of dict keyed by resource class of dict of inventories # Each list element corresponds to one resource provider new_inventories = self._get_expected_inventories( inventories, new_resources) # To compare with actual result, reformat new_inventories to # a dict, keyed by resource provider uuid, of dict of inventories # keyed by resource class expected = {} for rp, inventory in zip(rps, new_inventories): for rc, inv in inventory.items(): inv['resource_provider'] = rp['uuid'] # For full replacement (not amend) case these values should # be set to empty string. for key in ('max_unit', 'min_unit', 'reserved', 'step_size', 'total', 'reserved', 'step_size'): if key not in inv: inv[key] = '' expected[rp['uuid']] = inventory # Reformat raw response (list of inventories) as well resp_dict = collections.defaultdict(dict) for row in resp: resp_dict[row['resource_provider']][row['resource_class']] = row self.assertEqual(expected, resp_dict) # Verify the inventories weren't changed (--dry-run) for i, rp in enumerate(rps): resp = self.resource_inventory_list(rp['uuid']) self.assertDictEqual(old_inventories[i], {r['resource_class']: r for r in resp}) def _get_expected_inventories(self, old_inventories, resources): new_inventories = [] for old_inventory in old_inventories: new_inventory = collections.defaultdict(dict) new_inventory.update(copy.deepcopy(old_inventory)) for resource in resources: rc, keyval = resource.split(':') key, val = keyval.split('=') # Handle allocation ratio which is a float val = float(val) if '.' in val else int(val) new_inventory[rc][key] = val # The resource_class field is added by the osc_placement CLI, # so add it to our expected inventories if 'resource_class' not in new_inventory[rc]: new_inventory[rc]['resource_class'] = rc new_inventories.append(new_inventory) return new_inventories def _setup_two_resource_providers_in_aggregate(self): rps = [] invs = [] inventory2 = ['VCPU=8', 'VCPU:max_unit=4', 'VCPU:allocation_ratio=16.0', 'MEMORY_MB=1024', 'MEMORY_MB:reserved=256', 'MEMORY_MB:allocation_ratio=2.5', 'DISK_GB=16', 'DISK_GB:allocation_ratio=1.5', 'DISK_GB:min_unit=2', 'DISK_GB:step_size=2'] inventory1 = inventory2 + ['VGPU=8', 'VGPU:allocation_ratio=1.0', 'VGPU:min_unit=2', 'VGPU:step_size=2'] for i, inventory in enumerate([inventory1, inventory2]): rps.append(self.resource_provider_create()) resp = self.resource_inventory_set(rps[i]['uuid'], *inventory) # Verify the resource_provider column is not present without # --aggregate self.assertNotIn('resource_provider', resp) invs.append({r['resource_class']: r for r in resp}) # Put both resource providers in the same aggregate agg = str(uuid.uuid4()) for rp in rps: self.resource_provider_aggregate_set(rp['uuid'], agg) return rps, agg, invs def test_fail_if_no_rps_in_aggregate(self): nonexistent_agg = str(uuid.uuid4()) exc = self.assertRaises(base.CommandException, self.resource_inventory_set, nonexistent_agg, 'VCPU=8', aggregate=True) self.assertIn('No resource providers found in aggregate with uuid {}' .format(nonexistent_agg), str(exc)) def test_with_aggregate_one_fails(self): # Set up some existing inventories with two resource providers rps, agg, _invs = self._setup_two_resource_providers_in_aggregate() # Set a custom resource class inventory on the first resource provider self.resource_class_create('CUSTOM_FOO') rp1_uuid = rps[0]['uuid'] rp1_inv = self.resource_inventory_set(rp1_uuid, 'CUSTOM_FOO=1') # Create an allocation for custom resource class on first provider consumer = str(uuid.uuid4()) alloc = 'rp=%s,CUSTOM_FOO=1' % rp1_uuid self.resource_allocation_set(consumer, [alloc]) # Try to set allocation ratio for an aggregate. The first set should # fail because we're not going to set the custom resource class (which # is equivalent to trying to remove it) and removal isn't allowed if # there is an allocation of it present. The second set should succeed new_resources = ['VCPU:allocation_ratio=5.0', 'VCPU:total=8'] exc = self.assertRaises(base.CommandException, self.resource_inventory_set, agg, *new_resources, aggregate=True) self.assertIn('Failed to set inventory for 1 of 2 resource providers.', str(exc)) output = self.output.getvalue() + self.error.getvalue() self.assertIn('Failed to set inventory for resource provider %s:' % rp1_uuid, output) err_txt = ("Inventory for 'CUSTOM_FOO' on resource provider '%s' in " "use." % rp1_uuid) self.assertIn(err_txt, output) # Placement will default the following internally placement_defaults = ['VCPU:max_unit=2147483647', 'VCPU:min_unit=1', 'VCPU:reserved=0', 'VCPU:step_size=1'] # Get expected inventory for the second resource provider (succeeded) new_inventories = self._get_expected_inventories( # Since inventories are expected to be fully replaced, # use empty dict for old inventory [{}], new_resources + placement_defaults) resp = self.resource_inventory_list(rps[1]['uuid']) self.assertDictEqual(new_inventories[0], {r['resource_class']: r for r in resp}) # First resource provider should have remained the same (failed) resp = self.resource_inventory_list(rp1_uuid) self.assertDictEqual({r['resource_class']: r for r in rp1_inv}, {r['resource_class']: r for r in resp}) def _test_with_aggregate(self, amend=False): # Set up some existing inventories with two resource providers rps, agg, old_invs = self._setup_two_resource_providers_in_aggregate() # Verify that --dry-run works properly self._test_dry_run(agg, rps, old_invs, amend=amend) # If we're not amending inventory, set some defaults that placement # will set internally and return when we list inventories later if not amend: old_invs = [] defaults = {'max_unit': 2147483647, 'min_unit': 1, 'reserved': 0, 'step_size': 1} default_inventory = {'VCPU': copy.deepcopy(defaults)} for rp in rps: old_invs.append(default_inventory) # Now, go ahead and update an allocation ratio and verify new_resources = ['VCPU:allocation_ratio=5.0', 'VCPU:total=8'] resp = self.resource_inventory_set(agg, *new_resources, aggregate=True, amend=amend) # Verify the resource_provider column is present with --aggregate for rp in resp: self.assertIn('resource_provider', rp) new_inventories = self._get_expected_inventories(old_invs, new_resources) for i, rp in enumerate(rps): resp = self.resource_inventory_list(rp['uuid']) self.assertDictEqual(new_inventories[i], {r['resource_class']: r for r in resp}) def test_with_aggregate(self): self._test_with_aggregate() def test_amend_with_aggregate(self): self._test_with_aggregate(amend=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/functional/test_plugin.py0000664000175000017500000000144700000000000025415 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import subprocess from oslotest import base class TestPlugin(base.BaseTestCase): def test_parser_options(self): output = subprocess.check_output(['openstack', '--help']) self.assertIn('--os-placement-api-version', output.decode('utf-8')) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/functional/test_resource_class.py0000664000175000017500000000553700000000000027137 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 osc_placement.tests.functional import base CUSTOM_RC = 'CUSTOM_GPU_DEVICE_{}'.format( str(uuid.uuid4()).replace('-', '').upper()) class TestResourceClass(base.BaseTestCase): VERSION = '1.2' def test_list(self): rcs = self.resource_class_list() names = [rc['name'] for rc in rcs] self.assertIn('VCPU', names) self.assertIn('MEMORY_MB', names) self.assertIn('DISK_GB', names) def test_fail_create_if_incorrect_class(self): self.assertCommandFailed('JSON does not validate', self.resource_class_create, 'fake_class') self.assertCommandFailed('JSON does not validate', self.resource_class_create, 'CUSTOM_lower') self.assertCommandFailed('JSON does not validate', self.resource_class_create, 'CUSTOM_GPU.INTEL') def test_create(self): self.resource_class_create(CUSTOM_RC) rcs = self.resource_class_list() names = [rc['name'] for rc in rcs] self.assertIn(CUSTOM_RC, names) self.resource_class_delete(CUSTOM_RC) def test_fail_show_if_unknown_class(self): self.assertCommandFailed('No such resource class', self.resource_class_show, 'UNKNOWN') def test_show(self): rc = self.resource_class_show('VCPU') self.assertEqual('VCPU', rc['name']) def test_fail_delete_unknown_class(self): self.assertCommandFailed('No such resource class', self.resource_class_delete, 'UNKNOWN') def test_fail_delete_standard_class(self): self.assertCommandFailed('Cannot delete standard resource class', self.resource_class_delete, 'VCPU') class TestResourceClass17(base.BaseTestCase): VERSION = '1.7' def test_set_resource_class(self): self.resource_class_create(CUSTOM_RC) self.resource_class_set(CUSTOM_RC) self.resource_class_set(CUSTOM_RC + '1') rcs = self.resource_class_list() names = [rc['name'] for rc in rcs] self.assertIn(CUSTOM_RC, names) self.assertIn(CUSTOM_RC + '1', names) self.resource_class_delete(CUSTOM_RC) self.resource_class_delete(CUSTOM_RC + '1') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/functional/test_resource_provider.py0000664000175000017500000004175200000000000027663 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import operator import uuid from osc_placement.tests.functional import base class TestResourceProvider(base.BaseTestCase): def test_resource_provider_create(self): name = self.rand_name('test_rp_creation') created = self.resource_provider_create(name) self.assertEqual(name, created['name']) retrieved = self.resource_provider_show(created['uuid']) self.assertEqual(created, retrieved) def test_resource_provider_delete(self): created = self.resource_provider_create() before_delete = self.resource_provider_list(uuid=created['uuid']) self.assertEqual([created['uuid']], [rp['uuid'] for rp in before_delete]) self.resource_provider_delete(created['uuid']) after_delete = self.resource_provider_list(uuid=created['uuid']) self.assertEqual([], after_delete) def test_resource_provider_delete_not_found(self): rp_uuid = str(uuid.uuid4()) msg = 'No resource provider with uuid ' + rp_uuid + ' found' exc = self.assertRaises(base.CommandException, self.resource_provider_delete, rp_uuid) self.assertIn(msg, str(exc)) def test_resource_provider_set(self): orig_name = self.rand_name('test_rp_orig_name') created = self.resource_provider_create(name=orig_name) before_update = self.resource_provider_show(created['uuid']) self.assertEqual(orig_name, before_update['name']) self.assertEqual(0, before_update['generation']) new_name = self.rand_name('test_rp_new_name') self.resource_provider_set(created['uuid'], name=new_name) after_update = self.resource_provider_show(created['uuid']) self.assertEqual(new_name, after_update['name']) self.assertEqual(0, after_update['generation']) def test_resource_provider_set_not_found(self): rp_uuid = str(uuid.uuid4()) msg = 'No resource provider with uuid ' + rp_uuid + ' found' exc = self.assertRaises(base.CommandException, self.resource_provider_set, rp_uuid, 'test') self.assertIn(msg, str(exc)) def test_resource_provider_show(self): created = self.resource_provider_create() retrieved = self.resource_provider_show(created['uuid']) self.assertEqual(created, retrieved) def test_resource_provider_show_allocations(self): consumer_uuid = str(uuid.uuid4()) allocs = {consumer_uuid: {'resources': {'VCPU': 2}}} created = self.resource_provider_create() self.resource_inventory_set(created['uuid'], 'VCPU=4', 'VCPU:max_unit=4') self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(created['uuid'])]) expected = dict(created, allocations=allocs, generation=2) retrieved = self.resource_provider_show(created['uuid'], allocations=True) self.assertEqual(expected, retrieved) def test_resource_provider_show_allocations_empty(self): created = self.resource_provider_create() expected = dict(created, allocations={}) retrieved = self.resource_provider_show(created['uuid'], allocations=True) self.assertEqual(expected, retrieved) def test_resource_provider_show_not_found(self): rp_uuid = str(uuid.uuid4()) msg = 'No resource provider with uuid ' + rp_uuid + ' found' exc = self.assertRaises(base.CommandException, self.resource_provider_show, rp_uuid) self.assertIn(msg, str(exc)) def test_resource_provider_list(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() expected_full = sorted([rp1, rp2], key=operator.itemgetter('uuid')) self.assertEqual( expected_full, sorted([rp for rp in self.resource_provider_list() if rp['name'] in (rp1['name'], rp2['name'])], key=operator.itemgetter('uuid')) ) def test_resource_provider_list_by_name(self): rp1 = self.resource_provider_create() self.resource_provider_create() expected_filtered_by_name = [rp1] self.assertEqual( expected_filtered_by_name, [rp for rp in self.resource_provider_list(name=rp1['name'])] ) def test_resource_provider_list_by_uuid(self): rp1 = self.resource_provider_create() self.resource_provider_create() expected_filtered_by_uuid = [rp1] self.assertEqual( expected_filtered_by_uuid, [rp for rp in self.resource_provider_list(uuid=rp1['uuid'])] ) def test_resource_provider_list_empty(self): by_name = self.resource_provider_list(name='some_non_existing_name') self.assertEqual([], by_name) by_uuid = self.resource_provider_list(uuid=str(uuid.uuid4())) self.assertEqual([], by_uuid) def test_fail_if_incorrect_options(self): # aggregate_uuids param is available starting 1.3 self.assertCommandFailed( 'Operation or argument is not supported', self.resource_provider_list, aggregate_uuids=['1']) # resource param is available starting 1.4 self.assertCommandFailed('Operation or argument is not supported', self.resource_provider_list, resources=['1']) class TestResourceProvider14(base.BaseTestCase): VERSION = '1.4' def test_return_empty_list_for_nonexistent_aggregate(self): self.resource_provider_create() agg = str(uuid.uuid4()) rps, warning = self.resource_provider_list(aggregate_uuids=[agg], may_print_to_stderr=True) self.assertEqual([], rps) self.assertIn('The --aggregate-uuid option is deprecated, ' 'please use --member-of instead.', warning) # validate --member_of option works as expected rps = self.resource_provider_list(member_of=[agg]) self.assertEqual([], rps) def test_return_properly_for_aggregate_uuid_request(self): self.resource_provider_create() rp2 = self.resource_provider_create() agg = str(uuid.uuid4()) self.resource_provider_aggregate_set(rp2['uuid'], agg) rps, warning = self.resource_provider_list( aggregate_uuids=[agg, str(uuid.uuid4())], may_print_to_stderr=True) self.assertEqual(1, len(rps)) self.assertEqual(rp2['uuid'], rps[0]['uuid']) self.assertIn('The --aggregate-uuid option is deprecated, ' 'please use --member-of instead.', warning) # validate --member_of option works as expected rps = self.resource_provider_list(member_of=[agg]) self.assertEqual(1, len(rps)) self.assertEqual(rp2['uuid'], rps[0]['uuid']) def test_return_empty_list_if_no_resource(self): rp = self.resource_provider_create() self.assertEqual([], self.resource_provider_list( resources=['MEMORY_MB=256'], uuid=rp['uuid'])) def test_return_properly_for_resource_request(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() self.resource_inventory_set(rp1['uuid'], 'PCI_DEVICE=8') self.resource_inventory_set(rp2['uuid'], 'PCI_DEVICE=16') rps = self.resource_provider_list(resources=['PCI_DEVICE=16']) self.assertEqual(1, len(rps)) self.assertEqual(rp2['uuid'], rps[0]['uuid']) class TestResourceProvider114(base.BaseTestCase): VERSION = '1.14' def test_resource_provider_create(self): created = self.resource_provider_create() self.assertIn('root_provider_uuid', created) self.assertIn('parent_provider_uuid', created) def test_resource_provider_set(self): created = self.resource_provider_create() updated = self.resource_provider_set( created['uuid'], name='some_new_name') self.assertIn('root_provider_uuid', updated) self.assertIn('parent_provider_uuid', updated) def test_resource_provider_show(self): created = self.resource_provider_create() retrieved = self.resource_provider_show(created['uuid']) self.assertIn('root_provider_uuid', retrieved) self.assertIn('parent_provider_uuid', retrieved) def test_resource_provider_list(self): self.resource_provider_create() retrieved = self.resource_provider_list()[0] self.assertIn('root_provider_uuid', retrieved) self.assertIn('parent_provider_uuid', retrieved) def test_resource_provider_create_with_parent(self): parent = self.resource_provider_create() child = self.resource_provider_create( parent_provider_uuid=parent['uuid']) self.assertEqual(child['parent_provider_uuid'], parent['uuid']) def test_resource_provider_create_then_set_parent(self): parent = self.resource_provider_create() wannabe_child = self.resource_provider_create() child = self.resource_provider_set( wannabe_child['uuid'], name='mandatory_name_1', parent_provider_uuid=parent['uuid']) self.assertEqual(child['parent_provider_uuid'], parent['uuid']) def test_resource_provider_set_reparent(self): parent1 = self.resource_provider_create() parent2 = self.resource_provider_create() child = self.resource_provider_create( parent_provider_uuid=parent1['uuid']) exc = self.assertRaises( base.CommandException, self.resource_provider_set, child['uuid'], name='mandatory_name_2', parent_provider_uuid=parent2['uuid']) self.assertIn('HTTP 400', str(exc)) def test_resource_provider_list_in_tree(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create( parent_provider_uuid=rp1['uuid']) rp3 = self.resource_provider_create( parent_provider_uuid=rp1['uuid']) self.resource_provider_create() # not in-tree retrieved = self.resource_provider_list(in_tree=rp2['uuid']) self.assertEqual( set([rp['uuid'] for rp in retrieved]), set([rp1['uuid'], rp2['uuid'], rp3['uuid']]) ) def test_resource_provider_delete_parent(self): parent = self.resource_provider_create() self.resource_provider_create(parent_provider_uuid=parent['uuid']) exc = self.assertRaises( base.CommandException, self.resource_provider_delete, parent['uuid']) self.assertIn('HTTP 409', str(exc)) class TestResourceProvider118(base.BaseTestCase): VERSION = '1.18' # NOTE(cdent): The choice of traits here is important. We need to # make sure that we do not overlap with 'test_show_required_trait' # in TestAllocationCandidate117 which also creates some resource # providers. In a multi-process enviromment, the tests can race. def test_show_required_trait(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() self.resource_inventory_set( rp1['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') self.resource_inventory_set( rp2['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') self.resource_provider_trait_set( rp1['uuid'], 'STORAGE_DISK_SSD', 'HW_NIC_SRIOV_MULTIQUEUE') self.resource_provider_trait_set( rp2['uuid'], 'STORAGE_DISK_HDD', 'HW_NIC_SRIOV_MULTIQUEUE') rps = self.resource_provider_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=('HW_NIC_SRIOV_MULTIQUEUE',)) uuids = [rp['uuid'] for rp in rps] self.assertEqual(2, len(rps)) self.assertIn(rp1['uuid'], uuids) self.assertIn(rp2['uuid'], uuids) # Narrow the results and check multiple args. rps = self.resource_provider_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=('STORAGE_DISK_HDD', 'HW_NIC_SRIOV_MULTIQUEUE',)) self.assertEqual(1, len(rps)) self.assertEqual(rp2['uuid'], rps[0]['uuid']) # Specifying forbidden traits weren't available until version 1.22 def test_fail_if_forbidden_trait_wrong_version(self): self.assertCommandFailed( 'Operation or argument is not supported with version 1.18', self.resource_provider_list, resources=('MEMORY_MB=1024', 'DISK_GB=80'), forbidden=('STORAGE_DISK_HDD',)) class TestResourceProvider122(base.BaseTestCase): VERSION = '1.22' def test_hide_forbidden_trait(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() rp3 = self.resource_provider_create() self.resource_inventory_set( rp1['uuid'], 'MEMORY_MB=1024', 'DISK_GB=256') self.resource_inventory_set( rp2['uuid'], 'MEMORY_MB=1024', 'DISK_GB=256') self.resource_inventory_set( rp3['uuid'], 'MEMORY_MB=1024', 'DISK_GB=256') self.resource_provider_trait_set( rp1['uuid'], 'STORAGE_DISK_SSD', 'HW_CPU_X86_VMX') self.resource_provider_trait_set( rp2['uuid'], 'STORAGE_DISK_HDD', 'HW_CPU_X86_VMX') self.resource_provider_trait_set( rp3['uuid'], 'STORAGE_DISK_HDD', 'HW_CPU_X86_VMX') rps = self.resource_provider_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=('HW_CPU_X86_VMX',), forbidden=('STORAGE_DISK_HDD',)) self.assertEqual(1, len(rps)) self.assertEqual(rp1['uuid'], rps[0]['uuid']) rps = self.resource_provider_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=('HW_CPU_X86_VMX',), forbidden=('STORAGE_DISK_SSD',)) uuids = [rp['uuid'] for rp in rps] self.assertEqual(2, len(uuids)) self.assertNotIn(rp1['uuid'], uuids) self.assertIn(rp2['uuid'], uuids) self.assertIn(rp3['uuid'], uuids) def test_list_required_trait_any_trait_old_microversion(self): self.assertCommandFailed( 'Operation or argument is not supported with version 1.22', self.resource_provider_list, resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=( 'STORAGE_DISK_HDD,STORAGE_DISK_SSD', 'HW_NIC_SRIOV_MULTIQUEUE'), ) class TestResourceProvider139(base.BaseTestCase): VERSION = '1.39' def test_list_required_trait_any_trait(self): rp1 = self.resource_provider_create() rp2 = self.resource_provider_create() self.resource_inventory_set( rp1['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') self.resource_inventory_set( rp2['uuid'], 'MEMORY_MB=8192', 'DISK_GB=512') self.resource_provider_trait_set( rp1['uuid'], 'STORAGE_DISK_SSD', 'HW_NIC_SRIOV_MULTIQUEUE') self.resource_provider_trait_set( rp2['uuid'], 'STORAGE_DISK_HDD', 'HW_NIC_SRIOV_MULTIQUEUE') rps = self.resource_provider_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=('HW_NIC_SRIOV_MULTIQUEUE',)) self.assertEqual( {rp1['uuid'], rp2['uuid']}, {rp['uuid'] for rp in rps}) # Narrow the results and check multiple args. rps = self.resource_provider_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=('STORAGE_DISK_HDD', 'HW_NIC_SRIOV_MULTIQUEUE',)) self.assertEqual({rp2['uuid']}, {rp['uuid'] for rp in rps}) # Query for (HDD or SSD) and MULTIQUEUE and see that both RP returned # again rps = self.resource_provider_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=( 'STORAGE_DISK_HDD,STORAGE_DISK_SSD', 'HW_NIC_SRIOV_MULTIQUEUE') ) self.assertEqual( {rp1['uuid'], rp2['uuid']}, {rp['uuid'] for rp in rps}) # Query for (HDD or SSD) and MULTIQUEUE and !SSD and see that one of # the RPs are filtered rps = self.resource_provider_list( resources=('MEMORY_MB=1024', 'DISK_GB=80'), required=( 'STORAGE_DISK_HDD,STORAGE_DISK_SSD', 'HW_NIC_SRIOV_MULTIQUEUE'), forbidden=('STORAGE_DISK_SSD',), ) self.assertEqual({rp2['uuid']}, {rp['uuid'] for rp in rps}) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/functional/test_trait.py0000664000175000017500000000772600000000000025250 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 osc_placement.tests.functional import base TRAIT = 'CUSTOM_FAKE_HW_GPU_CLASS_{}'.format( str(uuid.uuid4()).replace('-', '').upper()) class TestTrait(base.BaseTestCase): VERSION = '1.6' def test_list_traits(self): self.assertTrue(len(self.trait_list()) > 0) def test_list_associated_traits(self): self.trait_create(TRAIT) rp = self.resource_provider_create() self.resource_provider_trait_set(rp['uuid'], TRAIT) self.assertIn(TRAIT, {t['name'] for t in self.trait_list(associated=True)}) def test_list_traits_startswith(self): self.trait_create(TRAIT) rp = self.resource_provider_create() self.resource_provider_trait_set(rp['uuid'], TRAIT) traits = {t['name'] for t in self.trait_list( name='startswith:' + TRAIT)} self.assertEqual(1, len(traits)) self.assertIn(TRAIT, traits) def test_list_traits_startswith_unknown_trait(self): traits = {t['name'] for t in self.trait_list( name='startswith:CUSTOM_FOO')} self.assertEqual(0, len(traits)) def test_list_traits_in(self): self.trait_create(TRAIT) rp = self.resource_provider_create() self.resource_provider_trait_set(rp['uuid'], TRAIT) traits = {t['name'] for t in self.trait_list( name='in:' + TRAIT)} self.assertEqual(1, len(traits)) self.assertIn(TRAIT, traits) def test_list_traits_in_unknown_trait(self): traits = {t['name'] for t in self.trait_list(name='in:CUSTOM_FOO')} self.assertEqual(0, len(traits)) def test_show_trait(self): self.trait_create(TRAIT) self.assertEqual({'name': TRAIT}, self.trait_show(TRAIT)) def test_fail_show_unknown_trait(self): self.assertCommandFailed('HTTP 404', self.trait_show, 'UNKNOWN') def test_set_multiple_traits(self): self.trait_create(TRAIT + '1') self.trait_create(TRAIT + '2') rp = self.resource_provider_create() self.resource_provider_trait_set(rp['uuid'], TRAIT + '1', TRAIT + '2') traits = {t['name'] for t in self.resource_provider_trait_list( rp['uuid'])} self.assertEqual(2, len(traits)) def test_set_known_and_unknown_traits(self): self.trait_create(TRAIT) rp = self.resource_provider_create() self.assertCommandFailed( 'No such trait', self.resource_provider_trait_set, rp['uuid'], TRAIT, TRAIT + '1') self.assertEqual([], self.resource_provider_trait_list(rp['uuid'])) def test_delete_traits_from_provider(self): self.trait_create(TRAIT) rp = self.resource_provider_create() self.resource_provider_trait_set(rp['uuid'], TRAIT) traits = {t['name'] for t in self.resource_provider_trait_list( rp['uuid'])} self.assertEqual(1, len(traits)) self.assertIn(TRAIT, traits) self.resource_provider_trait_delete(rp['uuid']) traits = {t['name'] for t in self.resource_provider_trait_list( rp['uuid'])} self.assertEqual(0, len(traits)) def test_delete_trait(self): self.trait_create(TRAIT) self.trait_delete(TRAIT) self.assertCommandFailed('HTTP 404', self.trait_show, TRAIT) def test_fail_rp_trait_list_unknown_uuid(self): self.assertCommandFailed( 'No resource provider', self.resource_provider_trait_list, 123) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/functional/test_usage.py0000664000175000017500000000707400000000000025225 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import operator import uuid from osc_placement.tests.functional import base class TestUsage(base.BaseTestCase): def test_usage_show(self): consumer_uuid = str(uuid.uuid4()) rp = self.resource_provider_create() self.resource_inventory_set( rp['uuid'], 'VCPU=4', 'VCPU:max_unit=4', 'MEMORY_MB=1024', 'MEMORY_MB:max_unit=1024') self.assertEqual([{'resource_class': 'MEMORY_MB', 'usage': 0}, {'resource_class': 'VCPU', 'usage': 0}], sorted(self.resource_provider_show_usage(rp['uuid']), key=operator.itemgetter('resource_class'))) self.resource_allocation_set( consumer_uuid, ['rp={},VCPU=2'.format(rp['uuid']), 'rp={},MEMORY_MB=512'.format(rp['uuid'])] ) self.assertEqual([{'resource_class': 'MEMORY_MB', 'usage': 512}, {'resource_class': 'VCPU', 'usage': 2}], sorted(self.resource_provider_show_usage(rp['uuid']), key=operator.itemgetter('resource_class'))) def test_usage_not_found(self): rp_uuid = str(uuid.uuid4()) exc = self.assertRaises(base.CommandException, self.resource_provider_show_usage, rp_uuid) self.assertIn( 'No resource provider with uuid {} found'.format(rp_uuid), str(exc) ) def test_usage_empty(self): rp = self.resource_provider_create() self.assertEqual([], self.resource_provider_show_usage(rp['uuid'])) class TestResourceUsage(base.BaseTestCase): VERSION = '1.9' def test_usage_by_project_id_user_id(self): c1 = str(uuid.uuid4()) c2 = str(uuid.uuid4()) c3 = str(uuid.uuid4()) p1 = str(uuid.uuid4()) p2 = str(uuid.uuid4()) u1 = str(uuid.uuid4()) u2 = str(uuid.uuid4()) rp = self.resource_provider_create() self.resource_inventory_set(rp['uuid'], 'VCPU=16') self.resource_allocation_set( c1, ['rp={},VCPU=2'.format(rp['uuid'])], project_id=p1, user_id=u1) self.resource_allocation_set( c2, ['rp={},VCPU=4'.format(rp['uuid'])], project_id=p2, user_id=u1) self.resource_allocation_set( c3, ['rp={},VCPU=6'.format(rp['uuid'])], project_id=p1, user_id=u2) # Show usage on the resource provider for all consumers. self.assertEqual( 12, self.resource_provider_show_usage(uuid=rp['uuid'])[0]['usage']) # Show usage for project p1. self.assertEqual( 8, self.resource_show_usage(project_id=p1)[0]['usage']) # Show usage for project p1 and user u1. self.assertEqual( 2, self.resource_show_usage( project_id=p1, user_id=u1)[0]['usage']) # Show usage for project p2. self.assertEqual( 4, self.resource_show_usage(project_id=p2)[0]['usage']) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740697287.4091735 osc_placement-4.6.0/osc_placement/tests/unit/0000775000175000017500000000000000000000000021315 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/unit/__init__.py0000664000175000017500000000000000000000000023414 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/unit/test_allocation.py0000664000175000017500000000532500000000000025060 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 osc_lib import exceptions from oslotest import base from osc_placement.resources import allocation class TestAllocation(base.BaseTestCase): def test_parse_allocations(self): rp1 = str(uuid.uuid4()) rp2 = str(uuid.uuid4()) allocations = [ 'rp={},VCPU=4,MEMORY_MB=16324'.format(rp1), 'rp={},VCPU=4,DISK_GB=4096'.format(rp2)] expected = { rp1: {'VCPU': 4, 'MEMORY_MB': 16324}, rp2: {'VCPU': 4, 'DISK_GB': 4096}, } self.assertDictEqual( expected, allocation.parse_allocations(allocations)) def test_merge_allocations(self): rp1 = str(uuid.uuid4()) allocations = [ 'rp={},VCPU=4,MEMORY_MB=16324'.format(rp1), 'rp={},VCPU=4,DISK_GB=4096'.format(rp1)] expected = { rp1: {'VCPU': 4, 'MEMORY_MB': 16324, 'DISK_GB': 4096}} self.assertEqual(expected, allocation.parse_allocations(allocations)) def test_fail_if_cannot_merge_allocations(self): rp1 = str(uuid.uuid4()) allocations = [ 'rp={},VCPU=4,MEMORY_MB=16324'.format(rp1), 'rp={},VCPU=8,DISK_GB=4096'.format(rp1)] ex = self.assertRaises( exceptions.CommandError, allocation.parse_allocations, allocations) self.assertEqual( 'Conflict detected for resource provider %s resource class VCPU' % rp1, str(ex)) def test_fail_if_incorrect_format(self): allocations = ['incorrect_format'] self.assertRaisesRegex( ValueError, 'Incorrect allocation', allocation.parse_allocations, allocations) allocations = ['=,'] self.assertRaisesRegex( ValueError, '2 is required', allocation.parse_allocations, allocations) allocations = ['abc=155'] self.assertRaisesRegex( ValueError, 'Incorrect allocation', allocation.parse_allocations, allocations) allocations = ['abc=155,xyz=999'] self.assertRaisesRegex( ValueError, 'parameter is required', allocation.parse_allocations, allocations) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/unit/test_common.py0000664000175000017500000000375400000000000024227 0ustar00zuulzuul00000000000000# 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 collections import oslotest.base as base import osc_placement.resources.common as common class TestCommon(base.BaseTestCase): def test_encode(self): self.assertEqual(u'привет'.encode('utf-8'), common.encode(u'привет')) def test_encode_custom_encoding(self): self.assertEqual(u'привет'.encode('utf-16'), common.encode(u'привет', 'utf-16')) def test_encode_non_string(self): self.assertEqual(b'bytesvalue', common.encode(b'bytesvalue')) def test_url_with_filters(self): base_url = '/resource_providers' expected = '/resource_providers?name=test&uuid=123456' filters = collections.OrderedDict([('name', 'test'), ('uuid', 123456)]) actual = common.url_with_filters(base_url, filters) self.assertEqual(expected, actual) def test_url_with_filters_empty(self): base_url = '/resource_providers' self.assertEqual(base_url, common.url_with_filters(base_url)) self.assertEqual(base_url, common.url_with_filters(base_url, {})) def test_url_with_filters_unicode_string(self): base_url = '/resource_providers' expected = ('/resource_providers?' 'name=%D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82') actual = common.url_with_filters(base_url, {'name': u'привет'}) self.assertEqual(expected, actual) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/unit/test_http.py0000664000175000017500000001054400000000000023711 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 unittest import mock import keystoneauth1.exceptions.http as ks_exceptions import osc_lib.exceptions as exceptions import oslotest.base as base import requests from osc_placement import http from osc_placement import version from oslo_serialization import jsonutils class FakeResponse(requests.Response): def __init__(self, status_code, content=None, headers=None): super(FakeResponse, self).__init__() self.status_code = status_code if content: self._content = content if headers: self.headers = headers class TestSessionClient(base.BaseTestCase): def test_wrap_http_exceptions(self): def go(): with http._wrap_http_exceptions(): error = { "errors": [ {"status": 404, "detail": ("The resource could not be found.\n\n" "No resource provider with uuid 123 " "found for delete")} ] } response = mock.Mock(content=json.dumps(error)) raise ks_exceptions.NotFound(response=response) exc = self.assertRaises(exceptions.NotFound, go) self.assertEqual(404, exc.http_status) self.assertIn('No resource provider with uuid 123 found', str(exc)) def test_unexpected_response(self): def go(): with http._wrap_http_exceptions(): raise ks_exceptions.InternalServerError() exc = self.assertRaises(ks_exceptions.InternalServerError, go) self.assertEqual(500, exc.http_status) self.assertIn('Internal Server Error (HTTP 500)', str(exc)) def test_session_client_version(self): session = mock.Mock() ks_filter = {'service_type': 'placement', 'region_name': 'mock_region', 'interface': 'mock_interface'} # 1. target to a specific version target_version = '1.23' client = http.SessionClient( session, ks_filter, api_version=target_version) self.assertEqual(client.api_version, target_version) # validate that the server side is not called session.request.assert_not_called() # 2. negotiation succeeds and have the client's highest version target_version = '1' session.request.return_value = FakeResponse(200) client = http.SessionClient( session, ks_filter, api_version=target_version) self.assertEqual(client.api_version, version.MAX_VERSION_NO_GAP) # validate that the server side is called expected_version = 'placement ' + version.MAX_VERSION_NO_GAP expected_headers = {'OpenStack-API-Version': expected_version, 'Accept': 'application/json'} session.request.assert_called_once_with( '/', 'GET', endpoint_filter=ks_filter, headers=expected_headers, raise_exc=False) session.reset_mock() # 3. negotiation fails and get the servers's highest version mock_server_version = '1.10' json_mock = { "errors": [{"status": 406, "title": "Not Acceptable", "min_version": "1.0", "max_version": mock_server_version}] } session.request.return_value = FakeResponse( 406, content=jsonutils.dump_as_bytes(json_mock)) client = http.SessionClient( session, ks_filter, api_version=target_version) self.assertEqual(client.api_version, mock_server_version) # validate that the server side is called session.request.assert_called_once_with( '/', 'GET', endpoint_filter=ks_filter, headers=expected_headers, raise_exc=False) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/unit/test_plugin.py0000664000175000017500000000307000000000000024224 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse from unittest import mock from oslotest import base import osc_placement.plugin as plugin class TestPlugin(base.BaseTestCase): def test_build_option_parser(self): parser = plugin.build_option_parser(argparse.ArgumentParser()) args = parser.parse_args(['--os-placement-api-version=1.0']) self.assertEqual('1.0', args.os_placement_api_version) args = parser.parse_args(['--os-placement-api-version', '1.0']) self.assertEqual('1.0', args.os_placement_api_version) @mock.patch('osc_placement.http.SessionClient') def test_make_client(self, mock_session_client): instance = mock.Mock(_api_version={'placement': '1.0'}) plugin.make_client(instance) mock_session_client.assert_called_with( session=instance.session, ks_filter={ 'service_type': 'placement', 'region_name': instance._region_name, 'interface': instance.interface }, api_version='1.0' ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/tests/unit/test_version.py0000664000175000017500000001241500000000000024416 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES 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 oslotest.base as base from osc_placement import version class TestVersion(base.BaseTestCase): def test_compare(self): self.assertTrue(version._compare('1.0', version.gt('0.9'))) self.assertTrue(version._compare('1.0', version.ge('0.9'))) self.assertTrue(version._compare('1.0', version.ge('1.0'))) self.assertTrue(version._compare('1.0', version.eq('1.0'))) self.assertTrue(version._compare('1.0', version.le('1.0'))) self.assertTrue(version._compare('1.0', version.le('1.1'))) self.assertTrue(version._compare('1.0', version.lt('1.1'))) self.assertTrue( version._compare('1.1', version.gt('1.0'), version.lt('1.2'))) self.assertTrue( version._compare( '0.3', version.eq('0.2'), version.eq('0.3'), op=any)) # Test error message msg = 'Operation or argument is not supported with version 1.0; ' self.assertEqual((msg + 'requires version greater than 1.0'), version._compare('1.0', version.gt('1.0'))) self.assertEqual((msg + 'requires at least version 1.1'), version._compare('1.0', version.ge('1.1'))) self.assertEqual((msg + 'requires version 1.1'), version._compare('1.0', version.eq('1.1'))) self.assertEqual((msg + 'requires at most version 0.9'), version._compare('1.0', version.le('0.9'))) self.assertEqual((msg + 'requires version less than 0.9'), version._compare('1.0', version.lt('0.9'))) self.assertRaises( ValueError, version._compare, 'abc', version.le('1.1')) self.assertRaises( ValueError, version._compare, '1.0', version.le('.0')) self.assertRaises( ValueError, version._compare, '1', version.le('2')) ex = self.assertRaises( ValueError, version.compare, '1.0', version.ge('1.1')) self.assertEqual( 'Operation or argument is not supported with version 1.0; ' 'requires at least version 1.1', str(ex)) ex = self.assertRaises( ValueError, version.compare, '1.0', version.eq('1.1'), version.eq('1.5'), op=any) self.assertEqual( 'Operation or argument is not supported with version 1.0; ' 'requires version 1.1, or requires version 1.5', str(ex)) def test_compare_with_exc(self): self.assertTrue(version.compare('1.05', version.gt('1.4'))) self.assertFalse(version.compare('1.3', version.gt('1.4'), exc=False)) self.assertRaisesRegex( ValueError, 'Operation or argument is not supported', version.compare, '3.1', version.gt('3.2')) def test_check_decorator(self): fake_api = mock.Mock() fake_api_dec = version.check(version.gt('2.11'))(fake_api) obj = mock.Mock() obj.app.client_manager.placement.api_version = '2.12' fake_api_dec(obj, 1, 2, 3) fake_api.assert_called_once_with(obj, 1, 2, 3) fake_api.reset_mock() obj.app.client_manager.placement.api_version = '2.10' self.assertRaisesRegex( ValueError, 'Operation or argument is not supported', fake_api_dec, obj, 1, 2, 3) fake_api.assert_not_called() def test_check_mixin(self): class Test(version.CheckerMixin): app = mock.Mock() app.client_manager.placement.api_version = '1.2' t = Test() self.assertTrue(t.compare_version(version.le('1.3'))) self.assertTrue(t.check_version(version.ge('1.0'))) self.assertRaisesRegex( ValueError, 'Operation or argument is not supported', t.check_version, version.lt('1.2')) def test_max_version_consistency(self): def _convert_to_tuple(str): return tuple(map(int, str.split("."))) versions = [ _convert_to_tuple(ver) for ver in version.SUPPORTED_MICROVERSIONS] max_ver = _convert_to_tuple(version.MAX_VERSION_NO_GAP) there_is_gap = False for i in range(len(versions) - 1): j = i + 1 if versions[j][1] - versions[i][1] != 1: there_is_gap = True self.assertEqual(max_ver, versions[i]) break if not there_is_gap: self.assertEqual(max_ver, versions[-1]) def test_get_version_returns_max_no_gap_when_no_session(self): obj = mock.Mock() obj.app.client_manager.session = None ret = version.get_version(obj) self.assertEqual(version.MAX_VERSION_NO_GAP, ret) obj.app.client_manager.placement.api_version.assert_not_called() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/osc_placement/version.py0000664000175000017500000001157700000000000021246 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import functools import operator import re NEGOTIATE_VERSIONS = [ '1', # Added for auto choice for appropriate version ] SUPPORTED_MICROVERSIONS = [ '1.0', '1.1', '1.2', '1.3', '1.4', '1.5', '1.6', '1.7', '1.8', '1.9', '1.10', '1.11', '1.12', '1.13', # unused '1.14', '1.15', # unused '1.16', '1.17', '1.18', '1.19', '1.20', # unused '1.21', '1.22', '1.23', # unused '1.24', '1.25', '1.26', # unused '1.27', # unused '1.28', # Added for provider allocation (un)set (Ussuri) '1.29', '1.37', # unused '1.38', # Added for consumer types (Xena) '1.39', # Added any-traits support (Yoga) ] SUPPORTED_VERSIONS = SUPPORTED_MICROVERSIONS + NEGOTIATE_VERSIONS # The max microversion lower than which are all supported by this client. # This is used to automatically pick up the microversion to use. Change this # when you add a microversion to the `_SUPPORTED_VERSIONS` without a gap. # TestVersion.test_max_version_consistency checks its consistency. MAX_VERSION_NO_GAP = '1.29' @functools.total_ordering class _Version: _version_re = re.compile(r'^(\d) \. (\d+)$', re.VERBOSE | re.ASCII) def __init__(self, version): match = self._version_re.match(version) if not match: raise ValueError('invalid version number %s' % version) major, minor = match.group(1, 2) self.version = (int(major), int(minor)) def __str__(self): return '.'.join(str(v) for v in self.version) def __eq__(self, other): return self.version == other.version def __lt__(self, other): return self.version < other.version def _op(func, b, msg): return lambda a: func(_Version(a), _Version(b)) or msg def lt(b): msg = 'requires version less than %s' % b return _op(operator.lt, b, msg) def le(b): msg = 'requires at most version %s' % b return _op(operator.le, b, msg) def eq(b): msg = 'requires version %s' % b return _op(operator.eq, b, msg) def ne(b): msg = 'can not use version %s' % b return _op(operator.ne, b, msg) def ge(b): msg = 'requires at least version %s' % b return _op(operator.ge, b, msg) def gt(b): msg = 'requires version greater than %s' % b return _op(operator.gt, b, msg) def _compare(ver, *predicates, **kwargs): func = kwargs.get('op', all) if func(p(ver) is True for p in predicates): return True # construct an error message if the requirement not satisfied err_msg = 'Operation or argument is not supported with version %s; ' % ver err_detail = [p(ver) for p in predicates if p(ver) is not True] logic = ', and ' if func is all else ', or ' return err_msg + logic.join(err_detail) def compare(ver, *predicates, **kwargs): """Validate version satisfies provided predicates. kwargs['exc'] - boolean whether exception should be raised kwargs['op'] - (all, any) how predicates should be checked Examples: compare('1.1', version.gt('1.2'), exc=False) - False compare('1.1', version.eq('1.0'), version.eq('1.1'), op=any) - True """ exc = kwargs.get('exc', True) result = _compare(ver, *predicates, **kwargs) if result is not True: if exc: raise ValueError(result) return False return True def check(*predicates, **check_kwargs): """Decorator for command object method. See `compare` """ def wrapped(func): def inner(self, *args, **kwargs): compare(get_version(self), *predicates, **check_kwargs) return func(self, *args, **kwargs) return inner return wrapped def get_version(obj): """Extract version from a command object.""" try: if obj.app.client_manager.session is None: return MAX_VERSION_NO_GAP version = obj.app.client_manager.placement.api_version except AttributeError: # resource does not have api_version attr when docs are generated # so let's use the minimal one version = SUPPORTED_VERSIONS[0] return version class CheckerMixin(object): def check_version(self, *predicates, **kwargs): return compare(get_version(self), *predicates, **kwargs) def compare_version(self, *predicates, **kwargs): return compare(get_version(self), *predicates, exc=False, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740697287.4211738 osc_placement-4.6.0/osc_placement.egg-info/0000775000175000017500000000000000000000000020666 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697287.0 osc_placement-4.6.0/osc_placement.egg-info/PKG-INFO0000644000175000017500000000347100000000000021766 0ustar00zuulzuul00000000000000Metadata-Version: 2.1 Name: osc-placement Version: 4.6.0 Summary: OpenStackClient plugin for the Placement service Home-page: https://docs.openstack.org/osc-placement/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.9 Classifier: Programming Language :: Python :: 3.10 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Requires-Python: >=3.9 License-File: LICENSE Requires-Dist: pbr>=2.0.0 Requires-Dist: keystoneauth1>=3.3.0 Requires-Dist: osc-lib>=1.2.0 Requires-Dist: oslo.utils>=3.37.0 ============= osc-placement ============= .. image:: https://governance.openstack.org/tc/badges/osc-placement.svg :target: https://governance.openstack.org/tc/reference/tags/index.html OpenStackClient plugin for the Placement service This is an OpenStackClient plugin, that provides CLI for the Placement service. Python API binding is not implemented - Placement API consumers are encouraged to use the REST API directly, CLI is provided only for convenience of users. * Free software: Apache license * Documentation: https://docs.openstack.org/osc-placement/latest/index.html * Source: https://opendev.org/openstack/osc-placement * Bugs: https://launchpad.net/placement * Release notes: https://docs.openstack.org/releasenotes/osc-placement/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697287.0 osc_placement-4.6.0/osc_placement.egg-info/SOURCES.txt0000664000175000017500000001035600000000000022557 0ustar00zuulzuul00000000000000.coveragerc .mailmap .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE MANIFEST.in README.rst pyproject.toml requirements.txt setup.cfg setup.py test-requirements.txt tox.ini doc/requirements.txt doc/source/conf.py doc/source/index.rst doc/source/cli/index.rst doc/source/contributor/index.rst doc/source/install/index.rst doc/source/user/index.rst osc_placement/__init__.py osc_placement/http.py osc_placement/plugin.py osc_placement/version.py osc_placement.egg-info/PKG-INFO osc_placement.egg-info/SOURCES.txt osc_placement.egg-info/dependency_links.txt osc_placement.egg-info/entry_points.txt osc_placement.egg-info/not-zip-safe osc_placement.egg-info/pbr.json osc_placement.egg-info/requires.txt osc_placement.egg-info/top_level.txt osc_placement/resources/__init__.py osc_placement/resources/aggregate.py osc_placement/resources/allocation.py osc_placement/resources/allocation_candidate.py osc_placement/resources/common.py osc_placement/resources/inventory.py osc_placement/resources/resource_class.py osc_placement/resources/resource_provider.py osc_placement/resources/trait.py osc_placement/resources/usage.py osc_placement/tests/__init__.py osc_placement/tests/functional/__init__.py osc_placement/tests/functional/base.py osc_placement/tests/functional/test_aggregate.py osc_placement/tests/functional/test_allocation.py osc_placement/tests/functional/test_allocation_candidate.py osc_placement/tests/functional/test_inventory.py osc_placement/tests/functional/test_plugin.py osc_placement/tests/functional/test_resource_class.py osc_placement/tests/functional/test_resource_provider.py osc_placement/tests/functional/test_trait.py osc_placement/tests/functional/test_usage.py osc_placement/tests/unit/__init__.py osc_placement/tests/unit/test_allocation.py osc_placement/tests/unit/test_common.py osc_placement/tests/unit/test_http.py osc_placement/tests/unit/test_plugin.py osc_placement/tests/unit/test_version.py releasenotes/notes/.placeholder releasenotes/notes/allocation-unset-beb0d904c8bc4228.yaml releasenotes/notes/allocation-unset-resource-3ff87787eca13f18.yaml releasenotes/notes/commands-v1.0.0-894ea659825b3757.yaml releasenotes/notes/drop-python-2-df59500ad303a56c.yaml releasenotes/notes/microversion-1.10-03ab71969921a0e4.yaml releasenotes/notes/microversion-1.14-support-nested-resource-providers-296961cc93ef30e8.yaml releasenotes/notes/microversion-1.16-alloc-candidates-limit-8310675ecc99a82a.yaml releasenotes/notes/microversion-1.17-alloc-candidates-required-traits-57378c735d0beeb4.yaml releasenotes/notes/microversion-1.18-resource-provider-required-traits-2ff846221bb297b9.yaml releasenotes/notes/microversion-1.19-resource-provider-aggregate-generation-c276739ec1cbc549.yaml releasenotes/notes/microversion-1.21-allocation-candidates-aggregate-3460414fa6819b7f.yaml releasenotes/notes/microversion-1.22-forbidden-traits-bc7acaf3006829a5.yaml releasenotes/notes/microversion-1.24-member-of-fbabd395a0048e87.yaml releasenotes/notes/microversion-1.25-granular-requests-f10936c700dee06f.yaml releasenotes/notes/microversion-1.28-alloc-consumer-gen-83cef07e274a1a1d.yaml releasenotes/notes/microversion-1.3-and-1.4-becd8058c9dd9ad8.yaml releasenotes/notes/microversion-1.39-any-traits-da32e4ef7c9ac6b7.yaml releasenotes/notes/microversion-1.5-0c6342c887669b8e.yaml releasenotes/notes/microversion-1.6-54a85ef9ae79f15d.yaml releasenotes/notes/microversion-1.7-6be2dadd0b27910f.yaml releasenotes/notes/microversion-1.8-1.9-db26e40571292353.yaml releasenotes/notes/resource-provider-inventory-set-aggregate-5f2239dd2685b636.yaml releasenotes/notes/resource-provider-inventory-set-amend-2349ea7a7dd5a43a.yaml releasenotes/notes/resource-provider-inventory-set-dryrun-18cf5e3a62f00938.yaml releasenotes/notes/show-usage-in-inventory-31eb87a6d243fc5a.yaml releasenotes/source/2023.1.rst releasenotes/source/2023.2.rst releasenotes/source/2024.1.rst releasenotes/source/2024.2.rst releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/wallaby.rst releasenotes/source/xena.rst releasenotes/source/yoga.rst releasenotes/source/zed.rst././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697287.0 osc_placement-4.6.0/osc_placement.egg-info/dependency_links.txt0000664000175000017500000000000100000000000024734 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697287.0 osc_placement-4.6.0/osc_placement.egg-info/entry_points.txt0000664000175000017500000000503700000000000024171 0ustar00zuulzuul00000000000000[openstack.cli.extension] placement = osc_placement.plugin [openstack.placement.v1] allocation_candidate_list = osc_placement.resources.allocation_candidate:ListAllocationCandidate resource_class_create = osc_placement.resources.resource_class:CreateResourceClass resource_class_delete = osc_placement.resources.resource_class:DeleteResourceClass resource_class_list = osc_placement.resources.resource_class:ListResourceClass resource_class_set = osc_placement.resources.resource_class:SetResourceClass resource_class_show = osc_placement.resources.resource_class:ShowResourceClass resource_provider_aggregate_list = osc_placement.resources.aggregate:ListAggregate resource_provider_aggregate_set = osc_placement.resources.aggregate:SetAggregate resource_provider_allocation_delete = osc_placement.resources.allocation:DeleteAllocation resource_provider_allocation_set = osc_placement.resources.allocation:SetAllocation resource_provider_allocation_show = osc_placement.resources.allocation:ShowAllocation resource_provider_allocation_unset = osc_placement.resources.allocation:UnsetAllocation resource_provider_create = osc_placement.resources.resource_provider:CreateResourceProvider resource_provider_delete = osc_placement.resources.resource_provider:DeleteResourceProvider resource_provider_inventory_class_set = osc_placement.resources.inventory:SetClassInventory resource_provider_inventory_delete = osc_placement.resources.inventory:DeleteInventory resource_provider_inventory_list = osc_placement.resources.inventory:ListInventory resource_provider_inventory_set = osc_placement.resources.inventory:SetInventory resource_provider_inventory_show = osc_placement.resources.inventory:ShowInventory resource_provider_list = osc_placement.resources.resource_provider:ListResourceProvider resource_provider_set = osc_placement.resources.resource_provider:SetResourceProvider resource_provider_show = osc_placement.resources.resource_provider:ShowResourceProvider resource_provider_trait_delete = osc_placement.resources.trait:DeleteResourceProviderTrait resource_provider_trait_list = osc_placement.resources.trait:ListResourceProviderTrait resource_provider_trait_set = osc_placement.resources.trait:SetResourceProviderTrait resource_provider_usage_show = osc_placement.resources.usage:ShowUsage resource_usage_show = osc_placement.resources.usage:ResourceShowUsage trait_create = osc_placement.resources.trait:CreateTrait trait_delete = osc_placement.resources.trait:DeleteTrait trait_list = osc_placement.resources.trait:ListTrait trait_show = osc_placement.resources.trait:ShowTrait ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697287.0 osc_placement-4.6.0/osc_placement.egg-info/not-zip-safe0000664000175000017500000000000100000000000023114 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697287.0 osc_placement-4.6.0/osc_placement.egg-info/pbr.json0000664000175000017500000000005600000000000022345 0ustar00zuulzuul00000000000000{"git_version": "454e48d", "is_release": true}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697287.0 osc_placement-4.6.0/osc_placement.egg-info/requires.txt0000664000175000017500000000010200000000000023257 0ustar00zuulzuul00000000000000pbr>=2.0.0 keystoneauth1>=3.3.0 osc-lib>=1.2.0 oslo.utils>=3.37.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697287.0 osc_placement-4.6.0/osc_placement.egg-info/top_level.txt0000664000175000017500000000001600000000000023415 0ustar00zuulzuul00000000000000osc_placement ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/pyproject.toml0000664000175000017500000000014400000000000017273 0ustar00zuulzuul00000000000000[build-system] requires = ["pbr>=5.7.0", "setuptools>=64.0.0", "wheel"] build-backend = "pbr.build" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740697287.3891726 osc_placement-4.6.0/releasenotes/0000775000175000017500000000000000000000000017051 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740697287.4131734 osc_placement-4.6.0/releasenotes/notes/0000775000175000017500000000000000000000000020201 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/.placeholder0000664000175000017500000000000000000000000022452 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/allocation-unset-beb0d904c8bc4228.yaml0000664000175000017500000000077300000000000026644 0ustar00zuulzuul00000000000000--- features: - | A new ``openstack resource provider allocation unset`` command has been added which allows removing allocations against specific resource providers for the given consumer. This can be useful when a consumer has allocations against more than one resource provider and ``openstack resource provider allocation delete`` is undesirable as it removes all allocations for the consumer. The new unset command requires ``--os-placement-api-version 1.12`` or greater. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/allocation-unset-resource-3ff87787eca13f18.yaml0000664000175000017500000000132600000000000030432 0ustar00zuulzuul00000000000000--- features: - | The ``openstack resource provider allocation unset`` command now supports ``--resource-class`` option, which accepts string of a resource class. This will remove allocations for the given resource class from all the providers. If ``--provider`` option is also specified, allocations to remove will be limited to the given resource class of the given resource provider. example1:: # remove VGPU allocation from provider P for this consumer. allocation unset --provider P --resource-class VGPU example2:: # remove VGPU allocations from all providers for this consumer. allocation unset --resource-class VGPU ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/commands-v1.0.0-894ea659825b3757.yaml0000664000175000017500000000235200000000000025533 0ustar00zuulzuul00000000000000--- prelude: | This is the first major release for the *osc-placement* plugin and includes a minimal set of command line support for the `Placement API`_. .. _Placement API: https://docs.openstack.org/nova/latest/user/placement.html features: - | The set of new commands in this release includes:: $ openstack resource -h Command "resource" matches: resource class create resource class delete resource class list resource class show resource provider aggregate list resource provider aggregate set resource provider allocation delete resource provider allocation set resource provider allocation show resource provider create resource provider delete resource provider inventory class set resource provider inventory delete resource provider inventory list resource provider inventory set resource provider inventory show resource provider list resource provider set resource provider show resource provider usage show See the `Usage`_ documentation for more details and examples. .. _Usage: https://docs.openstack.org/osc-placement/latest/user/index.html././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/drop-python-2-df59500ad303a56c.yaml0000664000175000017500000000017300000000000025714 0ustar00zuulzuul00000000000000--- upgrade: - | Python 2.7 support has been dropped. The minimum version of Python now supported is Python 3.6. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.10-03ab71969921a0e4.yaml0000664000175000017500000000070700000000000026236 0ustar00zuulzuul00000000000000--- features: - | The ``openstack allocation candidate list`` command is available starting from microversion `1.10`_. See the command documentation for `allocation candidate list`_ for more details. .. _1.10: https://docs.openstack.org/nova/latest/user/placement.html#allocation-candidates-maximum-in-pike .. _allocation candidate list: https://docs.openstack.org/osc-placement/latest/cli/index.html#allocation-candidate-list ././@PaxHeader0000000000000000000000000000022000000000000011447 xustar0000000000000000122 path=osc_placement-4.6.0/releasenotes/notes/microversion-1.14-support-nested-resource-providers-296961cc93ef30e8.yaml 22 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.14-support-nested-resource-providers-296961cc90000664000175000017500000000076600000000000033244 0ustar00zuulzuul00000000000000--- features: - | Support is added for the `1.14`_ placement API microversion by adding the ``root_provider_uuid`` and ``parent_provider_uuid`` to the output of resource provider list/show/create/set commands. Also resource provider create/set commands now have a new option ``--parent-provider ``. And ``resource provider list`` has a new option ``--in-tree ``. .. _1.14: https://docs.openstack.org/nova/latest/user/placement.html#add-nested-resource-providers ././@PaxHeader0000000000000000000000000000020500000000000011452 xustar0000000000000000111 path=osc_placement-4.6.0/releasenotes/notes/microversion-1.16-alloc-candidates-limit-8310675ecc99a82a.yaml 22 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.16-alloc-candidates-limit-8310675ecc99a82a.yam0000664000175000017500000000043300000000000032460 0ustar00zuulzuul00000000000000--- features: - | Support is added for the `1.16`_ placement API microversion by adding the ``--limit`` option to the ``openstack allocation candidate list`` command. .. _1.16: https://docs.openstack.org/nova/latest/user/placement.html#limit-allocation-candidates ././@PaxHeader0000000000000000000000000000021700000000000011455 xustar0000000000000000121 path=osc_placement-4.6.0/releasenotes/notes/microversion-1.17-alloc-candidates-required-traits-57378c735d0beeb4.yaml 22 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.17-alloc-candidates-required-traits-57378c735d0000664000175000017500000000051000000000000032773 0ustar00zuulzuul00000000000000--- features: - | Support is added for the `1.17`_ placement API microversion by adding the ``--required`` option to the ``openstack allocation candidate list`` command. .. _1.17: https://docs.openstack.org/nova/latest/user/placement.html#add-required-parameter-to-the-allocation-candidates-maximum-in-queens ././@PaxHeader0000000000000000000000000000022000000000000011447 xustar0000000000000000122 path=osc_placement-4.6.0/releasenotes/notes/microversion-1.18-resource-provider-required-traits-2ff846221bb297b9.yaml 22 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.18-resource-provider-required-traits-2ff8462210000664000175000017500000000052500000000000033173 0ustar00zuulzuul00000000000000--- features: - | Support is added for the `1.18`_ placement API microversion by adding the ``--required`` option to the ``openstack resource provider list`` command. .. _1.18: https://docs.openstack.org/placement/latest/placement-api-microversion-history.html#support-required-traits-queryparam-on-get-resource-providers ././@PaxHeader0000000000000000000000000000022500000000000011454 xustar0000000000000000127 path=osc_placement-4.6.0/releasenotes/notes/microversion-1.19-resource-provider-aggregate-generation-c276739ec1cbc549.yaml 22 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.19-resource-provider-aggregate-generation-c2760000664000175000017500000000055100000000000033443 0ustar00zuulzuul00000000000000--- features: - | Support is added for the `1.19`_ placement API microversion by adding the ``--generation`` option to the ``openstack resource provider aggregate set`` command. .. _1.19: https://docs.openstack.org/placement/latest/placement-api-microversion-history.html#include-generation-and-conflict-detection-in-provider-aggregates-apis ././@PaxHeader0000000000000000000000000000021600000000000011454 xustar0000000000000000120 path=osc_placement-4.6.0/releasenotes/notes/microversion-1.21-allocation-candidates-aggregate-3460414fa6819b7f.yaml 22 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.21-allocation-candidates-aggregate-3460414fa680000664000175000017500000000113600000000000032677 0ustar00zuulzuul00000000000000--- features: - | The ``openstack allocation candidate list`` command now supports microversion `1.21`_. An ``--aggregate-uuid`` option is added which limits the list of results to include only those resource providers that are in at least one of the aggregates. See the `command documentation`__ for more details. .. _1.21: https://docs.openstack.org/placement/latest/placement-api-microversion-history.html#support-member-of-aggregates-queryparam-on-get-allocation-candidates .. __: https://docs.openstack.org/osc-placement/latest/cli/index.html#allocation-candidate-list ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.22-forbidden-traits-bc7acaf3006829a5.yaml0000664000175000017500000000064300000000000031713 0ustar00zuulzuul00000000000000--- features: - | The `1.22 microversion`_ of placement adds support for excluding resource providers and allocation candidates with specified traits. A forbidden trait may be specified with ``--forbidden`` option. .. _1.22 microversion: https://docs.openstack.org/placement/latest/placement-api-microversion-history.html#support-forbidden-traits-on-resource-providers-and-allocations-candidates ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.24-member-of-fbabd395a0048e87.yaml0000664000175000017500000000132600000000000030335 0ustar00zuulzuul00000000000000--- features: - | The ``openstack allocation candidate list`` and the ``openstack resource provider list`` command now supports ``--member-of`` option, which accepts comma-separated UUIDs of the resource provider aggregates. If this is specified, the returned resource providers must be associated with at least one of the aggregates identified by uuid. This option can be repeated to add(restrict) the condition with ``--os-placement-api-version 1.24`` or greater. deprecations: - | The ``--aggregate-uuid`` option has been deprecated for the ``openstack allocation candidate list`` and the ``openstack resource provider list`` commands. Please use ``--member-of`` option instead. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.25-granular-requests-f10936c700dee06f.yaml0000664000175000017500000000244500000000000032070 0ustar00zuulzuul00000000000000--- features: - | The ``openstack allocation candidate list`` command now supports ``--group`` and ``--group-policy`` option. The ``--group`` option accepts an integer to group granular requests. If specified, following given options of resources, required/forbidden traits, and aggregates are associated to that group and will be satisfied by the same resource provider in the response. ``--group`` can be repeated to get candidates from multiple resource providers in a same resource provider tree. If multiple groups are supplied, the separate groups may or may not be satisfied by the same provider. If you want the groups to be satisfied by different resource providers, set ``--group_policy`` to ``isolate``. For example:: openstack allocation candidate list \ --group 1 --resource VCPU=3 --required HW_CPU_X86_SSE \ --group 2 --resource VCPU=4 \ --group_policy isolate This option is available with ``--os-placement-api-version 1.25`` or greater, but to have placement server be aware of nested providers, use ``--os-placement-api-version 1.29`` or greater. See the `REST API Version History`__ for more details. .. __: https://docs.openstack.org/placement/latest/placement-api-microversion-history.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.28-alloc-consumer-gen-83cef07e274a1a1d.yaml0000664000175000017500000000036100000000000032151 0ustar00zuulzuul00000000000000--- features: - | The ``openstack resource provider allocation set`` command now supports ``--os-placement-api-version 1.28`` where a consumer generation is used which facilitates safe concurrent modification of an allocation. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.3-and-1.4-becd8058c9dd9ad8.yaml0000664000175000017500000000154700000000000027624 0ustar00zuulzuul00000000000000--- features: - | The ``openstack resource provider list`` command now supports microversion `1.3`_ and `1.4`_. Specifically two new options are added to the command: * ``--aggregate-uuid``: List resource providers which are members of at least one of the specified resource provider aggregates. * ``--resource``: List resource providers which have the capacity to serve allocation requests for the given amount of specified resource class. See the `command documentation`__ for more details. .. _1.3: https://docs.openstack.org/nova/latest/user/placement.html#member-of-query-parameter .. _1.4: https://docs.openstack.org/nova/latest/user/placement.html#filter-resource-providers-by-requested-resource-capacity-maximum-in-ocata .. __: https://docs.openstack.org/osc-placement/latest/cli/index.html#resource-provider-list ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.39-any-traits-da32e4ef7c9ac6b7.yaml0000664000175000017500000000056300000000000030732 0ustar00zuulzuul00000000000000--- features: - | Both the ``openstack resource provider list`` and ``openstack allocation candidate list`` command now supports ``--os-placement-api-version 1.39`` where the ``--required`` argument now can contain a comma separated list of trait names to express OR relationship. So ``--required T1,T2 --required T3`` means (T1 or T2) and T3. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.5-0c6342c887669b8e.yaml0000664000175000017500000000145200000000000026204 0ustar00zuulzuul00000000000000--- features: - | The ``openstack resource provider inventory delete`` command now supports microversion `1.5`_. Specifically it is possible to delete all inventories of the specified resource provider. See the `command documentation`__ for more details. .. _1.5: https://docs.openstack.org/nova/latest/user/placement.html#delete-all-inventory-for-a-resource-provider .. __: https://docs.openstack.org/osc-placement/latest/cli/index.html#resource-provider-inventory-delete upgrade: - | The ``resource_class`` positional argument in command ``openstack resource provider inventory delete`` was replaced with the ``--resource-class`` optional argument. The ``--resource-class`` option is still required if using ``--os-placement-api-version`` less than 1.5. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.6-54a85ef9ae79f15d.yaml0000664000175000017500000000112500000000000026423 0ustar00zuulzuul00000000000000--- features: - | The following list of trait related commands was added for microversion `1.6`_: - ``openstack trait list`` - ``openstack trait show`` - ``openstack trait create`` - ``openstack trait delete`` - ``openstack resource provider trait list`` - ``openstack resource provider trait set`` - ``openstack resource provider trait delete`` See the `command documentation`__ for more details. .. _1.6: https://docs.openstack.org/nova/latest/user/placement.html#traits-api .. __: https://docs.openstack.org/osc-placement/latest/cli/index.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.7-6be2dadd0b27910f.yaml0000664000175000017500000000065500000000000026461 0ustar00zuulzuul00000000000000--- features: - | The ``openstack resource class set {name}`` command has been added which requires ``--os-placement-api-version 1.7``. This command is similar to ``openstack resource class create`` except it is idempotent if the resource class already exists. See the `command documentation`__ for more details. .. __: https://docs.openstack.org/osc-placement/latest/cli/index.html#resource-class-set././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/microversion-1.8-1.9-db26e40571292353.yaml0000664000175000017500000000170000000000000026404 0ustar00zuulzuul00000000000000--- features: - | The ``openstack resource provider allocation set`` command now supports microversion `1.8`_. Specifically from 1.8 it is necessary to specify ``--user-id`` and ``--project-id`` arguments when setting allocations. The ``openstack resource usage show`` command is available starting from microversion `1.9`_. It is possible to show usages for a project and user. See the command documentation for `allocation set`_ and `resource usage show`_ for more details. .. _1.8: https://docs.openstack.org/nova/latest/user/placement.html#require-placement-project-id-user-id-in-put-allocations .. _1.9: https://docs.openstack.org/nova/latest/user/placement.html#add-get-usages .. _allocation set: https://docs.openstack.org/osc-placement/latest/cli/index.html#resource-provider-allocation-set .. _resource usage show: https://docs.openstack.org/osc-placement/latest/cli/index.html#resource-usage-show ././@PaxHeader0000000000000000000000000000020600000000000011453 xustar0000000000000000112 path=osc_placement-4.6.0/releasenotes/notes/resource-provider-inventory-set-aggregate-5f2239dd2685b636.yaml 22 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/resource-provider-inventory-set-aggregate-5f2239dd2685b636.ya0000664000175000017500000000073400000000000033055 0ustar00zuulzuul00000000000000--- features: - | A new ``--aggregate`` option has been added to the ``resource provider inventory set`` command which can set resource provider inventory for all resource providers that are members of the specified aggregate. For example, VCPU, MEMORY_MB, and/or DISK_GB allocation ratios can be managed in aggregate to resolve `bug 1804125`_. .. _bug 1804125: https://docs.openstack.org/nova/latest/admin/configuration/schedulers.html#bug-1804125 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/resource-provider-inventory-set-amend-2349ea7a7dd5a43a.yaml0000664000175000017500000000035500000000000032747 0ustar00zuulzuul00000000000000--- features: - | A new ``--amend`` option has been added to the ``resource provider inventory set`` command which can update resource provider inventory without requiring the user to pass a full replacement for inventory. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/resource-provider-inventory-set-dryrun-18cf5e3a62f00938.yaml0000664000175000017500000000031500000000000033047 0ustar00zuulzuul00000000000000--- features: - | A new ``--dry-run`` option has been added to the ``resource provider inventory set`` command which gives the ability to preview changes to inventory without effecting them. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/notes/show-usage-in-inventory-31eb87a6d243fc5a.yaml0000664000175000017500000000036000000000000030077 0ustar00zuulzuul00000000000000--- features: - | The ``openstack resource provider inventory list`` and ``openstack resource provider inventory show`` commands now include a ``used`` column providing summary usage information for the specified resource(s). ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740697287.4211738 osc_placement-4.6.0/releasenotes/source/0000775000175000017500000000000000000000000020351 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/2023.1.rst0000664000175000017500000000020200000000000021622 0ustar00zuulzuul00000000000000=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/2023.2.rst0000664000175000017500000000020200000000000021623 0ustar00zuulzuul00000000000000=========================== 2023.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/2024.1.rst0000664000175000017500000000020200000000000021623 0ustar00zuulzuul00000000000000=========================== 2024.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/2024.2.rst0000664000175000017500000000020200000000000021624 0ustar00zuulzuul00000000000000=========================== 2024.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/conf.py0000664000175000017500000000452500000000000021656 0ustar00zuulzuul00000000000000# -*- 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. # Glance 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 ------------------------------------------------ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # The master toctree document. master_doc = 'index' # General information about the project. project = 'osc_placement Release Notes' copyright = '2016, 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. # # The short X.Y version. # The full version, including alpha/beta/rc tags. release = '' # The short X.Y version. version = '' # -- 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' # openstackdocstheme options openstackdocs_repo_name = 'openstack/osc-placement' openstackdocs_auto_name = False openstackdocs_use_storyboard = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/index.rst0000664000175000017500000000045100000000000022212 0ustar00zuulzuul00000000000000============================================ osc_placement Release Notes ============================================ .. toctree:: :maxdepth: 1 unreleased 2024.2 2024.1 2023.2 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky queens ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/queens.rst0000664000175000017500000000022300000000000022400 0ustar00zuulzuul00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/rocky.rst0000664000175000017500000000022100000000000022225 0ustar00zuulzuul00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/stein.rst0000664000175000017500000000022100000000000022220 0ustar00zuulzuul00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/train.rst0000664000175000017500000000017600000000000022224 0ustar00zuulzuul00000000000000========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/unreleased.rst0000664000175000017500000000016000000000000023227 0ustar00zuulzuul00000000000000============================== Current Series Release Notes ============================== .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/ussuri.rst0000664000175000017500000000020200000000000022427 0ustar00zuulzuul00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/victoria.rst0000664000175000017500000000022000000000000022715 0ustar00zuulzuul00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: unmaintained/victoria ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/wallaby.rst0000664000175000017500000000021400000000000022533 0ustar00zuulzuul00000000000000============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: unmaintained/wallaby ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/xena.rst0000664000175000017500000000020000000000000022026 0ustar00zuulzuul00000000000000========================= Xena Series Release Notes ========================= .. release-notes:: :branch: unmaintained/xena ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/yoga.rst0000664000175000017500000000020000000000000022032 0ustar00zuulzuul00000000000000========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: unmaintained/yoga ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/releasenotes/source/zed.rst0000664000175000017500000000017400000000000021667 0ustar00zuulzuul00000000000000======================== Zed Series Release Notes ======================== .. release-notes:: :branch: unmaintained/zed ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/requirements.txt0000664000175000017500000000016700000000000017650 0ustar00zuulzuul00000000000000pbr>=2.0.0 # Apache-2.0 keystoneauth1>=3.3.0 # Apache-2.0 osc-lib>=1.2.0 # Apache-2.0 oslo.utils>=3.37.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1740697287.4211738 osc_placement-4.6.0/setup.cfg0000664000175000017500000000677400000000000016217 0ustar00zuulzuul00000000000000[metadata] name = osc-placement summary = OpenStackClient plugin for the Placement service description_file = README.rst author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/osc-placement/latest/ python_requires = >=3.9 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 :: Implementation :: CPython Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 Programming Language :: Python :: 3.9 Programming Language :: Python :: 3.10 Programming Language :: Python :: 3.11 Programming Language :: Python :: 3.12 [files] packages = osc_placement [entry_points] openstack.cli.extension = placement = osc_placement.plugin openstack.placement.v1 = resource_provider_allocation_set = osc_placement.resources.allocation:SetAllocation resource_provider_allocation_unset = osc_placement.resources.allocation:UnsetAllocation resource_provider_allocation_show = osc_placement.resources.allocation:ShowAllocation resource_provider_allocation_delete = osc_placement.resources.allocation:DeleteAllocation resource_provider_create = osc_placement.resources.resource_provider:CreateResourceProvider resource_provider_list = osc_placement.resources.resource_provider:ListResourceProvider resource_provider_show = osc_placement.resources.resource_provider:ShowResourceProvider resource_provider_set = osc_placement.resources.resource_provider:SetResourceProvider resource_provider_delete = osc_placement.resources.resource_provider:DeleteResourceProvider resource_provider_usage_show = osc_placement.resources.usage:ShowUsage resource_usage_show = osc_placement.resources.usage:ResourceShowUsage resource_provider_inventory_set = osc_placement.resources.inventory:SetInventory resource_provider_inventory_class_set = osc_placement.resources.inventory:SetClassInventory resource_provider_inventory_list = osc_placement.resources.inventory:ListInventory resource_provider_inventory_show = osc_placement.resources.inventory:ShowInventory resource_provider_inventory_delete = osc_placement.resources.inventory:DeleteInventory resource_provider_aggregate_list = osc_placement.resources.aggregate:ListAggregate resource_provider_aggregate_set = osc_placement.resources.aggregate:SetAggregate resource_class_list = osc_placement.resources.resource_class:ListResourceClass resource_class_create = osc_placement.resources.resource_class:CreateResourceClass resource_class_set = osc_placement.resources.resource_class:SetResourceClass resource_class_show = osc_placement.resources.resource_class:ShowResourceClass resource_class_delete = osc_placement.resources.resource_class:DeleteResourceClass trait_list = osc_placement.resources.trait:ListTrait trait_show = osc_placement.resources.trait:ShowTrait trait_create = osc_placement.resources.trait:CreateTrait trait_delete = osc_placement.resources.trait:DeleteTrait resource_provider_trait_list = osc_placement.resources.trait:ListResourceProviderTrait resource_provider_trait_set = osc_placement.resources.trait:SetResourceProviderTrait resource_provider_trait_delete = osc_placement.resources.trait:DeleteResourceProviderTrait allocation_candidate_list = osc_placement.resources.allocation_candidate:ListAllocationCandidate [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/setup.py0000664000175000017500000000127100000000000016073 0ustar00zuulzuul00000000000000# 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>=2.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/test-requirements.txt0000664000175000017500000000031100000000000020614 0ustar00zuulzuul00000000000000hacking>=6.1.0,<6.2.0 # Apache-2.0 coverage>=4.0 # Apache-2.0 oslotest>=1.10.0 # Apache-2.0 python-openstackclient>=3.3.0 # Apache-2.0 stestr>=1.0.0 # Apache-2.0 wsgi-intercept>=1.7.0 # MIT License ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1740697242.0 osc_placement-4.6.0/tox.ini0000664000175000017500000000737500000000000015707 0ustar00zuulzuul00000000000000[tox] minversion = 3.18.0 envlist = py3,functional,pep8 [testenv] usedevelop = true allowlist_externals = rm setenv = PYTHONDONTWRITEBYTECODE=1 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} # NOTE(cdent): Do not set envdir here as it confuses tox-siblings. # gate functional jobs, which use the 'functional' path when # copying files. [testenv:functional] description = Run functional tests using python3. # As osc-placement functional tests import the PlacementFixture from the placement # repository these tests are, by default, set up to run with openstack-placement # from pypi. In the gate, Zuul will use the installed version of placement (stable # branch version on stable gate run) OR the version of placement the Depends-On in # the commit message suggests. If you want to run the tests with latest master from # the placement repo, modify the dep line to point at master, example: # deps = # {[testenv]deps} # git+https://opendev.org/openstack/placement#egg=openstack-placement # If you want to run the test locally with an un-merged placement change, # modify the dep line to point to your dependency or pip install placement # into the appropriate tox virtualenv. # NOTE: We express the requirement here instead of test-requirements # because we do not want placement present during unit tests. deps = {[testenv]deps} openstack-placement>=1.0.0 commands = stestr --test-path=./osc_placement/tests/functional run {posargs} [testenv:functional-py39] description = Run functional tests using python3.9. deps = {[testenv:functional]deps} commands = {[testenv:functional]commands} [testenv:functional-py310] description = Run functional tests using python3.10. deps = {[testenv:functional]deps} commands = {[testenv:functional]commands} [testenv:functional-py311] description = Run functional tests using python3.11. deps = {[testenv:functional]deps} commands = {[testenv:functional]commands} [testenv:functional-py312] description = Run functional tests using python3.12. deps = {[testenv:functional]deps} commands = {[testenv:functional]commands} [testenv:pep8] description = Run style checks. commands = flake8 {posargs} [testenv:cover] description = Run unit tests with coverage enabled. setenv = {[testenv]setenv} PYTHON=coverage run --source osc_placement --parallel-mode commands = coverage erase stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml coverage report [testenv:docs] description = Build main documentation. deps = -r{toxinidir}/doc/requirements.txt -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} commands = rm -rf doc/build sphinx-build -W -b html -d doc/build/doctrees doc/source doc/build/html [testenv:pdf-docs] description = Build PDF documentation. deps = {[testenv:docs]deps} allowlist_externals = make commands = sphinx-build -W -b latex doc/source doc/build/pdf make -C doc/build/pdf [testenv:releasenotes] description = Build release notes. deps = {[testenv:docs]deps} commands = rm -rf releasenotes/build sphinx-build -W -b html -d releasenotes/build/doctrees releasenotes/source releasenotes/build/html [testenv:venv] commands = {posargs} # The docs requirements are included here for creating release notes, e.g.: # tox -e venv -- reno new deps = {[testenv]deps} -r{toxinidir}/doc/requirements.txt [testenv:debug] commands = oslo_debug_helper {posargs} [flake8] # E123, E125 skipped as they are invalid PEP-8. # W503 line break before binary operator show-source = true ignore = E123,E125,W503 builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,build