diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 09a9172bd2..c76b800593 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,13 +1,13 @@ --- repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace - id: mixed-line-ending args: ['--fix', 'lf'] exclude: '.*\.(svg)$' - - id: check-byte-order-marker + - id: fix-byte-order-marker - id: check-executables-have-shebangs - id: check-merge-conflict - id: debug-statements @@ -15,27 +15,14 @@ repos: files: .*\.(yaml|yml)$ args: ['--unsafe'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.11.8 + rev: v0.15.18 hooks: - - id: ruff + - id: ruff-check args: ['--fix', '--unsafe-fixes'] - id: ruff-format - repo: https://opendev.org/openstack/hacking - rev: 7.0.0 + rev: 8.1.0 hooks: - id: hacking additional_dependencies: [] exclude: '^(doc|releasenotes)/.*$' - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.15.0 - hooks: - - id: mypy - additional_dependencies: - - types-requests - # keep this in-sync with '[mypy] exclude' in 'setup.cfg' - exclude: | - (?x)( - doc/.* - | examples/.* - | releasenotes/.* - ) diff --git a/.zuul.yaml b/.zuul.yaml index 74ca67d571..256d5dc364 100644 --- a/.zuul.yaml +++ b/.zuul.yaml @@ -1,4 +1,5 @@ --- + - job: name: osc-tox-unit-tips parent: openstack-tox @@ -6,7 +7,7 @@ Run unit tests for OpenStackClient with master branch of important libs. Takes advantage of the base tox job's install-siblings feature. - irrelevant-files: &common-irrelevant-files + irrelevant-files: - ^.*\.rst$ - ^doc/.*$ - ^releasenotes/.*$ @@ -23,8 +24,8 @@ zuul_work_dir: src/opendev.org/openstack/python-openstackclient - job: - name: osc-tox-py310-tips - parent: openstack-tox-py310 + name: osc-tox-py311-tips + parent: openstack-tox-py311 description: | Run unit tests for OpenStackClient with master branch of important libs. @@ -43,8 +44,8 @@ zuul_work_dir: src/opendev.org/openstack/python-openstackclient - job: - name: osc-tox-py313-tips - parent: openstack-tox-py313 + name: osc-tox-py314-tips + parent: openstack-tox-py314 description: | Run unit tests for OpenStackClient with master branch of important libs. @@ -97,7 +98,7 @@ ovn-northd: false ovs-vswitchd: false ovsdb-server: false - q-ovn-metadata-agent: false + q-ovn-agent: false # Neutron services q-agt: true q-dhcp: true @@ -155,12 +156,12 @@ name: osc-tox-unit-tips check: jobs: - - osc-tox-py310-tips - - osc-tox-py313-tips + - osc-tox-py311-tips + - osc-tox-py314-tips gate: jobs: - - osc-tox-py310-tips - - osc-tox-py313-tips + - osc-tox-py311-tips + - osc-tox-py314-tips - project: templates: @@ -168,11 +169,14 @@ - osc-tox-unit-tips - openstack-cover-jobs - openstack-python3-jobs + - openstack-python3-next-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 check: jobs: + - openstackclient-check-plugins: + voting: true - osc-build-image: voting: false - osc-functional-devstack diff --git a/bindep.txt b/bindep.txt index 83ebf2d43f..8402431aed 100644 --- a/bindep.txt +++ b/bindep.txt @@ -8,4 +8,4 @@ libffi-dev [compile test platform:dpkg] libssl-dev [compile test platform:dpkg] python3-dev [compile test platform:dpkg] python3-devel [compile test platform:rpm] -libpcre3-dev [test platform:dpkg] +libpcre2-dev [test platform:dpkg] diff --git a/doc/Makefile b/doc/Makefile deleted file mode 100644 index 2cdd0f5cd9..0000000000 --- a/doc/Makefile +++ /dev/null @@ -1,136 +0,0 @@ -# Makefile for Sphinx documentation -# - -# You can set these variables from the command line. -SPHINXOPTS = -SPHINXBUILD = sphinx-build -PAPER = -BUILDDIR = build - -# Internal variables. -PAPEROPT_a4 = -D latex_paper_size=a4 -PAPEROPT_letter = -D latex_paper_size=letter -ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source - -.PHONY: help clean html pdf dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - @echo " pdf to make pdf with rst2pdf" - @echo " dirhtml to make HTML files named index.html in directories" - @echo " singlehtml to make a single large HTML file" - @echo " pickle to make pickle files" - @echo " json to make JSON files" - @echo " htmlhelp to make HTML files and a HTML help project" - @echo " qthelp to make HTML files and a qthelp project" - @echo " devhelp to make HTML files and a Devhelp project" - @echo " epub to make an epub" - @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" - @echo " latexpdf to make LaTeX files and run them through pdflatex" - @echo " text to make text files" - @echo " man to make manual pages" - @echo " changes to make an overview of all changed/added/deprecated items" - @echo " linkcheck to check all external links for integrity" - @echo " doctest to run all doctests embedded in the documentation (if enabled)" - -clean: - -rm -rf $(BUILDDIR)/* - -html: - $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." - -pdf: - $(SPHINXBUILD) -b pdf $(ALLSPHINXOPTS) $(BUILDDIR)/pdf - @echo - @echo "Build finished. The PDFs are in $(BUILDDIR)/pdf." - -dirhtml: - $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." - -singlehtml: - $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml - @echo - @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." - -pickle: - $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle - @echo - @echo "Build finished; now you can process the pickle files." - -json: - $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json - @echo - @echo "Build finished; now you can process the JSON files." - -htmlhelp: - $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp - @echo - @echo "Build finished; now you can run HTML Help Workshop with the" \ - ".hhp project file in $(BUILDDIR)/htmlhelp." - -qthelp: - $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp - @echo - @echo "Build finished; now you can run "qcollectiongenerator" with the" \ - ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/NebulaDocs.qhcp" - @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/NebulaDocs.qhc" - -devhelp: - $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp - @echo - @echo "Build finished." - @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/NebulaDocs" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/NebulaDocs" - @echo "# devhelp" - -epub: - $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub - @echo - @echo "Build finished. The epub file is in $(BUILDDIR)/epub." - -latex: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo - @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." - @echo "Run \`make' in that directory to run these through (pdf)latex" \ - "(use \`make latexpdf' here to do that automatically)." - -latexpdf: - $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex - @echo "Running LaTeX files through pdflatex..." - make -C $(BUILDDIR)/latex all-pdf - @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." - -text: - $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text - @echo - @echo "Build finished. The text files are in $(BUILDDIR)/text." - -man: - $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man - @echo - @echo "Build finished. The manual pages are in $(BUILDDIR)/man." - -changes: - $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes - @echo - @echo "The overview file is in $(BUILDDIR)/changes." - -linkcheck: - $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck - @echo - @echo "Link check complete; look for any errors in the above output " \ - "or in $(BUILDDIR)/linkcheck/output.txt." - -doctest: - $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest - @echo "Testing of doctests in the sources finished, look at the " \ - "results in $(BUILDDIR)/doctest/output.txt." diff --git a/doc/requirements.txt b/doc/requirements.txt index 79e3ded4f6..05a9bfa87c 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -4,7 +4,7 @@ sphinx>=2.0.0,!=2.1.0 # BSD sphinxcontrib-apidoc>=0.2.0 # BSD # redirect tests in docs -whereto>=0.4.0 # Apache-2.0 +whereto>=0.5.0 # Apache-2.0 # Install these to generate sphinx autodocs aodhclient>=0.9.0 # Apache-2.0 diff --git a/doc/source/_extra/.htaccess b/doc/source/_extra/.htaccess index 8c0a005425..ab7dceecf9 100644 --- a/doc/source/_extra/.htaccess +++ b/doc/source/_extra/.htaccess @@ -14,3 +14,13 @@ redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/plugin-commands.html$ /py # - image pages were split into -v1 and -v2 # The unversioned page is hidden but contains links to the versioned pages so links in the wild redirect somewhere sane. redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(ec2-credentials|endpoint|image|project|role|service|token|user).html$ /python-openstackclient/$1/cli/_hidden/$2.html + +# Redirect docs from their previous flat structure to the new service/version structure +redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(availability-zone|command|complete|configuration|extension|limits|module|project-cleanup|quota|versions)\.html$ /python-openstackclient/$1/cli/command-objects/common/index.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(aggregate|compute-agent|compute-service|console-connection|console-log|console-url|flavor|host|hypervisor-stats|hypervisor|keypair|server-backup|server-event|server-group|server-image|server-migration|server|usage)\.html$ /python-openstackclient/$1/cli/command-objects/compute/v2/index.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(ec2-credentials-v2|endpoint-v2|project-v2|role-v2|service-v2|token-v2|user-v2)\.html$ /python-openstackclient/$1/cli/command-objects/identity/v2/index.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(access-rules|access-token|application-credentials|catalog|consumer|credential|domain|ec2-credentials-v3|endpoint-v3|endpoint_group|federation-domain-project|federation-protocol|group|identity-provider|implied_role|limit|mapping|policy|project-v3|region|registered-limit|request-token|role-assignment|role-v3|service-provider|service-v3|token-v3|trust|user-v3)\.html$ /python-openstackclient/$1/cli/command-objects/identity/v3/index.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(image-member|image-task|image-v2)\.html$ /python-openstackclient/$1/cli/command-objects/image/v2/index.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(address-group|address-scope|default-security-group-rule|floating-ip-port-forwarding|floating-ip|ip-availability|local-ip-association|local-ip|network-agent|network-auto-allocated-topology|network-flavor-profile|network-flavor|network-l3-conntrack-helper|network-meter-rule|network-meter|network-qos-policy|network-qos-rule-type|network-qos-rule|network-rbac|network-segment|network-service-provider|network-trunk|network|network_segment_range|port|router-ndp-proxy|router|security-group-rule|security-group|subnet-pool|subnet)\.html$ /python-openstackclient/$1/cli/command-objects/network/v2/index.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(container|object-store-account|object)\.html$ /python-openstackclient/$1/cli/command-objects/object-store/v1/index.html +redirectmatch 301 ^/python-openstackclient/([^/]+)/cli/command-objects/(block-storage-cleanup|block-storage-cluster|block-storage-log-level|block-storage-manage|block-storage-resource-filter|consistency-group-snapshot|consistency-group|volume-attachment|volume-backend|volume-backup|volume-group-snapshot|volume-group-type|volume-group|volume-host|volume-message|volume-qos|volume-service|volume-snapshot|volume-transfer-request|volume-type|volume)\.html$ /python-openstackclient/$1/cli/command-objects/volume/v3/index.html diff --git a/doc/source/cli/_hidden/ec2-credentials.rst b/doc/source/cli/_hidden/ec2-credentials.rst deleted file mode 100644 index c54459d877..0000000000 --- a/doc/source/cli/_hidden/ec2-credentials.rst +++ /dev/null @@ -1,13 +0,0 @@ -=============== -ec2 credentials -=============== - -.. NOTE(efried): This page is hidden from the main TOC; it's here so links in - the wild redirect somewhere sane, because previously identity v2 and v3 were - combined in a single page. - -.. toctree:: - :maxdepth: 2 - - ../command-objects/ec2-credentials-v2 - ../command-objects/ec2-credentials-v3 diff --git a/doc/source/cli/_hidden/endpoint.rst b/doc/source/cli/_hidden/endpoint.rst deleted file mode 100644 index 744e3badcb..0000000000 --- a/doc/source/cli/_hidden/endpoint.rst +++ /dev/null @@ -1,13 +0,0 @@ -======== -endpoint -======== - -.. NOTE(efried): This page is hidden from the main TOC; it's here so links in - the wild redirect somewhere sane, because previously identity v2 and v3 were - combined in a single page. - -.. toctree:: - :maxdepth: 2 - - ../command-objects/endpoint-v2 - ../command-objects/endpoint-v3 diff --git a/doc/source/cli/_hidden/image.rst b/doc/source/cli/_hidden/image.rst deleted file mode 100644 index 06919e7ab9..0000000000 --- a/doc/source/cli/_hidden/image.rst +++ /dev/null @@ -1,13 +0,0 @@ -===== -image -===== - -.. NOTE(efried): This page is hidden from the main TOC; it's here so links in - the wild redirect somewhere sane, because previously image v2 and v3 were - combined in a single page. - -.. toctree:: - :maxdepth: 2 - - ../command-objects/image-v1 - ../command-objects/image-v2 diff --git a/doc/source/cli/_hidden/project.rst b/doc/source/cli/_hidden/project.rst deleted file mode 100644 index 209a3129df..0000000000 --- a/doc/source/cli/_hidden/project.rst +++ /dev/null @@ -1,13 +0,0 @@ -======= -project -======= - -.. NOTE(efried): This page is hidden from the main TOC; it's here so links in - the wild redirect somewhere sane, because previously identity v2 and v3 were - combined in a single page. - -.. toctree:: - :maxdepth: 2 - - ../command-objects/project-v2 - ../command-objects/project-v3 diff --git a/doc/source/cli/_hidden/role.rst b/doc/source/cli/_hidden/role.rst deleted file mode 100644 index c85f48146c..0000000000 --- a/doc/source/cli/_hidden/role.rst +++ /dev/null @@ -1,13 +0,0 @@ -==== -role -==== - -.. NOTE(efried): This page is hidden from the main TOC; it's here so links in - the wild redirect somewhere sane, because previously identity v2 and v3 were - combined in a single page. - -.. toctree:: - :maxdepth: 2 - - ../command-objects/role-v2 - ../command-objects/role-v3 diff --git a/doc/source/cli/_hidden/service.rst b/doc/source/cli/_hidden/service.rst deleted file mode 100644 index 8b33638610..0000000000 --- a/doc/source/cli/_hidden/service.rst +++ /dev/null @@ -1,13 +0,0 @@ -======= -service -======= - -.. NOTE(efried): This page is hidden from the main TOC; it's here so links in - the wild redirect somewhere sane, because previously identity v2 and v3 were - combined in a single page. - -.. toctree:: - :maxdepth: 2 - - ../command-objects/service-v2 - ../command-objects/service-v3 diff --git a/doc/source/cli/_hidden/token.rst b/doc/source/cli/_hidden/token.rst deleted file mode 100644 index 6ebf801b76..0000000000 --- a/doc/source/cli/_hidden/token.rst +++ /dev/null @@ -1,13 +0,0 @@ -===== -token -===== - -.. NOTE(efried): This page is hidden from the main TOC; it's here so links in - the wild redirect somewhere sane, because previously identity v2 and v3 were - combined in a single page. - -.. toctree:: - :maxdepth: 2 - - ../command-objects/token-v2 - ../command-objects/token-v3 diff --git a/doc/source/cli/_hidden/user.rst b/doc/source/cli/_hidden/user.rst deleted file mode 100644 index 34eb59954f..0000000000 --- a/doc/source/cli/_hidden/user.rst +++ /dev/null @@ -1,13 +0,0 @@ -==== -user -==== - -.. NOTE(efried): This page is hidden from the main TOC; it's here so links in - the wild redirect somewhere sane, because previously identity v2 and v3 were - combined in a single page. - -.. toctree:: - :maxdepth: 2 - - ../command-objects/user-v2 - ../command-objects/user-v3 diff --git a/doc/source/cli/authentication.rst b/doc/source/cli/authentication.rst index 8ed318f526..69cd7009e0 100644 --- a/doc/source/cli/authentication.rst +++ b/doc/source/cli/authentication.rst @@ -4,10 +4,8 @@ Authentication ============== -OpenStackClient leverages `python-keystoneclient`_ authentication -plugins to support a number of different authentication methods. - -.. _`python-keystoneclient`: https://docs.openstack.org/python-keystoneclient/latest/using-sessions.html#sharing-authentication-plugins +OpenStackClient leverages *keystoneauth* authentication plugins to support a +number of different authentication methods. Authentication Process ---------------------- @@ -15,7 +13,8 @@ Authentication Process The user provides some number of authentication credential options. If an authentication type is not provided (``--os-auth-type``), the authentication options are examined to determine if one of the default -types can be used. If no match is found an error is reported and OSC exits. +types can be used. If no match is found an error is reported and +OpenStackClient exits. Note that the authentication call to the Identity service has not yet occurred. It is deferred until the last possible moment in order to @@ -25,7 +24,7 @@ processing detects an invalid command. Authentication Plugins ---------------------- -The Keystone client library implements the base set of plugins. Additional +The *keystoneauth* library implements the base set of plugins. Additional plugins may be available from the Keystone project or other sources. There are at least three authentication types that are always available: @@ -33,15 +32,15 @@ There are at least three authentication types that are always available: * **Password**: A project, username and password are used to identify the user. An optional domain may also be included. This is the most common type and is the default any time a username is supplied. An authentication - URL for the Identity service is also required. [Required: ``--os-auth-url``, - ``--os-project-name``, ``--os-username``; Optional: ``--os-password``] + URL for the Identity service is also required. + * **Token**: This is slightly different from the usual token authentication - in that a token and an authentication - URL are supplied and the plugin retrieves a new token. - [Required: ``--os-auth-url``, ``--os-token``] + in that a token and an authentication URL are supplied and the plugin + retrieves a new token. + * **Others**: Other authentication plugins such as SAML, Kerberos, and OAuth1.0 - are under development and also supported. To use them, they must be selected - by supplying the ``--os-auth-type`` option. + are also supported. To use them, they must be selected by supplying the + ``--os-auth-type`` option. Detailed Process ---------------- @@ -295,6 +294,15 @@ or, using environment variables: $ TOKEN=$(openstack token issue -f value -c id) +.. note:: + + The above examples assume you require a project-scoped token. You can omit + the project-related configuration if your user has a default project ID set. + Conversely, if requesting domain-scoped or system-scoped, you should update + these examples accordingly. If the user does not have a default project + configured and no scoping information is provided, the resulting token will + be unscoped. + ``v3totp`` ~~~~~~~~~~ diff --git a/doc/source/cli/backwards-incompatible.rst b/doc/source/cli/backwards-incompatible.rst index 3fbe65fae9..652ca6c29f 100644 --- a/doc/source/cli/backwards-incompatible.rst +++ b/doc/source/cli/backwards-incompatible.rst @@ -16,6 +16,18 @@ from this backwards incompatible change handling. Backwards Incompatible Changes ============================== +Release 10.0 +------------ + +1. Nova network support was removed from the networking commands. + + As Neutron has been the preferred networking solution in + Openstack for close to ten years, the old Nova network + code was finally removed. All networking API calls are still + supported, but they will only use the Neutron API endpoint. + + * Commit: https://review.opendev.org/981613 + Release 4.0 ----------- diff --git a/doc/source/cli/command-list.rst b/doc/source/cli/command-list.rst index 9044f69389..b920f60c84 100644 --- a/doc/source/cli/command-list.rst +++ b/doc/source/cli/command-list.rst @@ -8,4 +8,24 @@ Command List :glob: :maxdepth: 2 - command-objects/* + command-objects/common/index + command-objects/compute/v2/index + command-objects/identity/v2/index + command-objects/identity/v3/index + command-objects/image/v1/index + command-objects/image/v2/index + command-objects/network/v2/index + command-objects/object-store/v1/index + command-objects/share/v2/index + command-objects/volume/v2/index + command-objects/volume/v3/index + +.. toctree:: + :hidden: + + command-objects/identity/index + command-objects/image/index + command-objects/network/index + command-objects/object-store/index + command-objects/share/index + command-objects/volume/index diff --git a/doc/source/cli/command-objects/access-rules.rst b/doc/source/cli/command-objects/access-rules.rst deleted file mode 100644 index 6e811fc7df..0000000000 --- a/doc/source/cli/command-objects/access-rules.rst +++ /dev/null @@ -1,19 +0,0 @@ -=========== -access rule -=========== - -Identity v3 - -Access rules are fine-grained permissions for application credentials. An access -rule comprises of a service type, a request path, and a request method. Access -rules may only be created as attributes of application credentials, but they may -be viewed and deleted independently. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: access rule delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: access rule list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: access rule show diff --git a/doc/source/cli/command-objects/access-token.rst b/doc/source/cli/command-objects/access-token.rst deleted file mode 100644 index 7ef9167eb3..0000000000 --- a/doc/source/cli/command-objects/access-token.rst +++ /dev/null @@ -1,10 +0,0 @@ -============ -access token -============ - -An **access token** is used by the Identity service's OS-OAUTH1 extension. It -is used by the **consumer** to request new Identity API tokens on behalf of the -authorizing **user**. Applicable to Identity v3. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: access token create diff --git a/doc/source/cli/command-objects/address-group.rst b/doc/source/cli/command-objects/address-group.rst deleted file mode 100644 index c1ff6f8858..0000000000 --- a/doc/source/cli/command-objects/address-group.rst +++ /dev/null @@ -1,12 +0,0 @@ -============= -address group -============= - -An **address group** is a group of IPv4 or IPv6 address blocks which could be -referenced as a remote source or destination when creating a security group -rule. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: address group * diff --git a/doc/source/cli/command-objects/address-scope.rst b/doc/source/cli/command-objects/address-scope.rst deleted file mode 100644 index 38141d5c97..0000000000 --- a/doc/source/cli/command-objects/address-scope.rst +++ /dev/null @@ -1,11 +0,0 @@ -============= -address scope -============= - -An **address scope** is a scope of IPv4 or IPv6 addresses that belongs -to a given project and may be shared between projects. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: address scope * diff --git a/doc/source/cli/command-objects/aggregate.rst b/doc/source/cli/command-objects/aggregate.rst deleted file mode 100644 index 0f62ce8f43..0000000000 --- a/doc/source/cli/command-objects/aggregate.rst +++ /dev/null @@ -1,11 +0,0 @@ -========= -aggregate -========= - -Host aggregates provide a mechanism to group hosts according to certain -criteria. - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: aggregate * diff --git a/doc/source/cli/command-objects/application-credentials.rst b/doc/source/cli/command-objects/application-credentials.rst deleted file mode 100644 index cef3ef0dfc..0000000000 --- a/doc/source/cli/command-objects/application-credentials.rst +++ /dev/null @@ -1,13 +0,0 @@ -====================== -application credential -====================== - -Identity v3 - -With application credentials, a user can grant their applications limited -access to their cloud resources. Once created, users can authenticate with an -application credential by using the ``v3applicationcredential`` auth type. - - -.. autoprogram-cliff:: openstack.identity.v3 - :command: application credential * diff --git a/doc/source/cli/command-objects/availability-zone.rst b/doc/source/cli/command-objects/availability-zone.rst deleted file mode 100644 index bdc64f1537..0000000000 --- a/doc/source/cli/command-objects/availability-zone.rst +++ /dev/null @@ -1,11 +0,0 @@ -================= -availability zone -================= - -An **availability zone** is a logical partition of cloud block storage, -compute and network services. - -Block Storage v2, Compute v2, Network v2 - -.. autoprogram-cliff:: openstack.common - :command: availability zone list diff --git a/doc/source/cli/command-objects/block-storage-cleanup.rst b/doc/source/cli/command-objects/block-storage-cleanup.rst deleted file mode 100644 index 6a593c1187..0000000000 --- a/doc/source/cli/command-objects/block-storage-cleanup.rst +++ /dev/null @@ -1,8 +0,0 @@ -============= -block storage -============= - -Block Storage v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: block storage cleanup diff --git a/doc/source/cli/command-objects/block-storage-cluster.rst b/doc/source/cli/command-objects/block-storage-cluster.rst deleted file mode 100644 index 318419e079..0000000000 --- a/doc/source/cli/command-objects/block-storage-cluster.rst +++ /dev/null @@ -1,8 +0,0 @@ -===================== -block storage cluster -===================== - -Block Storage v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: block storage cluster * diff --git a/doc/source/cli/command-objects/block-storage-log-level.rst b/doc/source/cli/command-objects/block-storage-log-level.rst deleted file mode 100644 index 17241a0edf..0000000000 --- a/doc/source/cli/command-objects/block-storage-log-level.rst +++ /dev/null @@ -1,8 +0,0 @@ -======================= -Block Storage Log Level -======================= - -Block Storage v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: block storage log level * diff --git a/doc/source/cli/command-objects/block-storage-manage.rst b/doc/source/cli/command-objects/block-storage-manage.rst deleted file mode 100644 index a1cff1ad67..0000000000 --- a/doc/source/cli/command-objects/block-storage-manage.rst +++ /dev/null @@ -1,11 +0,0 @@ -==================== -Block Storage Manage -==================== - -Block Storage v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: block storage volume manageable list - -.. autoprogram-cliff:: openstack.volume.v3 - :command: block storage snapshot manageable list diff --git a/doc/source/cli/command-objects/block-storage-resource-filter.rst b/doc/source/cli/command-objects/block-storage-resource-filter.rst deleted file mode 100644 index 3e4c33ba5f..0000000000 --- a/doc/source/cli/command-objects/block-storage-resource-filter.rst +++ /dev/null @@ -1,8 +0,0 @@ -============================= -block storage resource filter -============================= - -Block Storage v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: block storage resource filter * diff --git a/doc/source/cli/command-objects/catalog.rst b/doc/source/cli/command-objects/catalog.rst deleted file mode 100644 index 84cd160a0b..0000000000 --- a/doc/source/cli/command-objects/catalog.rst +++ /dev/null @@ -1,9 +0,0 @@ -======= -catalog -======= - -A **catalog** lists OpenStack services that are available on the cloud. -Applicable to Identity v2 and v3 - -.. autoprogram-cliff:: openstack.identity.v3 - :command: catalog * diff --git a/doc/source/cli/command-objects/command.rst b/doc/source/cli/command-objects/command.rst deleted file mode 100644 index 5afc4940ab..0000000000 --- a/doc/source/cli/command-objects/command.rst +++ /dev/null @@ -1,10 +0,0 @@ -======= -command -======= - -Internal - -Installed commands in the OSC process. - -.. autoprogram-cliff:: openstack.cli - :command: command * diff --git a/doc/source/cli/command-objects/common/index.rst b/doc/source/cli/command-objects/common/index.rst new file mode 100644 index 0000000000..f5882006bf --- /dev/null +++ b/doc/source/cli/command-objects/common/index.rst @@ -0,0 +1,117 @@ +=============== +Common Commands +=============== + + +availability zone +----------------- + +An **availability zone** is a logical partition of cloud block storage, +compute and network services. + +.. autoprogram-cliff:: openstack.common + :command: availability zone list + + +command +------- + +Installed commands in the OSC process. + +.. autoprogram-cliff:: openstack.cli + :command: command * + + +complete +-------- + +The ``complete`` command is inherited from the `python-cliff` library, it can +be used to generate a bash-completion script. Currently, the command will +generate a script for bash versions 3 or 4. The bash-completion script is +printed directly to standard out. + +Typical usage for this command is:: + + openstack complete | sudo tee /etc/bash_completion.d/osc.bash_completion > /dev/null + +It is highly recommended to install ``python-openstackclient`` from a package +(``apt-get`` or ``yum``). In some distributions the package ``bash-completion`` is shipped +as dependency, and the `openstack complete` command will be run as a post-install action, +however not every distribution include this dependency and you might need to install +``bash-completion`` package to enable autocomplete feature. + +complete +-------- + +print bash completion command + +.. program:: complete +.. code:: bash + + openstack complete + + +configuration +------------- + +.. _configuration-show: + +.. autoprogram-cliff:: openstack.common + :command: configuration show + + +extension +--------- + +Many OpenStack server APIs include API extensions that enable +additional functionality. + +.. autoprogram-cliff:: openstack.common + :command: extension * + + +limits +------ + +The Compute and Block Storage APIs have resource usage limits. + +.. autoprogram-cliff:: openstack.common + :command: limits * + + +module +------ + +Installed Python modules in the OSC process. + +.. autoprogram-cliff:: openstack.cli + :command: module * + + +project cleanup +--------------- + +Clean resources associated with a specific project based on OpenStackSDK +implementation + +.. autoprogram-cliff:: openstack.common + :command: project cleanup + + +quota +----- + +Resource quotas appear in multiple APIs, OpenStackClient presents them as a +single object with multiple properties. + +.. autoprogram-cliff:: openstack.common + :command: quota * + + +versions +-------- + +Get a list of every version of every service in a given cloud. + +.. autoprogram-cliff:: openstack.common + :command: versions show diff --git a/doc/source/cli/command-objects/complete.rst b/doc/source/cli/command-objects/complete.rst deleted file mode 100644 index 165d5d07d5..0000000000 --- a/doc/source/cli/command-objects/complete.rst +++ /dev/null @@ -1,28 +0,0 @@ -======== -complete -======== - -The ``complete`` command is inherited from the `python-cliff` library, it can -be used to generate a bash-completion script. Currently, the command will -generate a script for bash versions 3 or 4. The bash-completion script is -printed directly to standard out. - -Typical usage for this command is:: - - openstack complete | sudo tee /etc/bash_completion.d/osc.bash_completion > /dev/null - -It is highly recommended to install ``python-openstackclient`` from a package -(``apt-get`` or ``yum``). In some distributions the package ``bash-completion`` is shipped -as dependency, and the `openstack complete` command will be run as a post-install action, -however not every distribution include this dependency and you might need to install -``bash-completion`` package to enable autocomplete feature. - -complete --------- - -print bash completion command - -.. program:: complete -.. code:: bash - - openstack complete diff --git a/doc/source/cli/command-objects/compute-agent.rst b/doc/source/cli/command-objects/compute-agent.rst deleted file mode 100644 index 89d7492879..0000000000 --- a/doc/source/cli/command-objects/compute-agent.rst +++ /dev/null @@ -1,8 +0,0 @@ -============= -compute agent -============= - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: compute agent * diff --git a/doc/source/cli/command-objects/compute-service.rst b/doc/source/cli/command-objects/compute-service.rst deleted file mode 100644 index ac54786e5b..0000000000 --- a/doc/source/cli/command-objects/compute-service.rst +++ /dev/null @@ -1,8 +0,0 @@ -=============== -compute service -=============== - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: compute service * diff --git a/doc/source/cli/command-objects/compute/v2/index.rst b/doc/source/cli/command-objects/compute/v2/index.rst new file mode 100644 index 0000000000..dddfef2ece --- /dev/null +++ b/doc/source/cli/command-objects/compute/v2/index.rst @@ -0,0 +1,262 @@ +=================== +Compute v2 Commands +=================== + + +aggregate +--------- + +Host aggregates provide a mechanism to group hosts according to certain +criteria. + +.. autoprogram-cliff:: openstack.compute.v2 + :command: aggregate * + + +compute agent +------------- + +.. autoprogram-cliff:: openstack.compute.v2 + :command: compute agent * + + +compute service +--------------- + +.. autoprogram-cliff:: openstack.compute.v2 + :command: compute service * + + +console connection +------------------ + +Server console connection information + +.. autoprogram-cliff:: openstack.compute.v2 + :command: console connection show + + +console log +----------- + +Server console text dump + +.. autoprogram-cliff:: openstack.compute.v2 + :command: console log * + + +console url +----------- + +Server remote console URL + +.. autoprogram-cliff:: openstack.compute.v2 + :command: console url * + + +flavor +------ + +.. autoprogram-cliff:: openstack.compute.v2 + :command: flavor * + + +host +---- + +The physical computer running a hypervisor. + +.. autoprogram-cliff:: openstack.compute.v2 + :command: host * + + +hypervisor stats +---------------- + +.. autoprogram-cliff:: openstack.compute.v2 + :command: hypervisor stats * + + +hypervisor +---------- + +.. NOTE(efried): have to list these out one by one; 'hypervisor *' pulls in + ... stats. + +.. autoprogram-cliff:: openstack.compute.v2 + :command: hypervisor list + +.. autoprogram-cliff:: openstack.compute.v2 + :command: hypervisor show + + +keypair +------- + +The badly named keypair is really the public key of an OpenSSH key pair to be +used for access to created servers. You can also create a private key for +access to a created server by not passing any argument to the keypair create +command. + +.. autoprogram-cliff:: openstack.compute.v2 + :command: keypair * + + +server backup +------------- + +A server backup is a disk image created in the Image store from a running server +instance. The backup command manages the number of archival copies to retain. + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server backup create + + +server event +------------ + +Server event are event record for server operations. They consist of: type +(create, delete, reboot and so on), result (success, error), start time, finish +time and so on. These are important for server maintenance. + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server event * + + +server group +------------ + +Server groups provide a mechanism to group servers according to certain policy. + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server group * + + +server image +------------ + +A server image is a disk image created from a running server instance. The +image is created in the Image store. + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server image create + + +server migration +---------------- + +A server migration provides a way to move an instance from one +host to another. There are four types of migration operation +supported: live migration, cold migration, resize and evacuation. + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server migration * + + +server share +------------ + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server share * + + +server volume +------------- + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server volume * + + +server +------ + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server add * + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server create + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server evacuate + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server delete + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server dump create + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server list + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server lock + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server migrate* + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server pause + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server reboot + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server rebuild + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server remove * + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server rescue + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server resize* + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server restore + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server resume + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server set + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server shelve + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server show + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server ssh + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server start + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server stop + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server suspend + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server unlock + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server unpause + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server unrescue + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server unset + +.. autoprogram-cliff:: openstack.compute.v2 + :command: server unshelve + + +usage +----- + +.. autoprogram-cliff:: openstack.compute.v2 + :command: usage * diff --git a/doc/source/cli/command-objects/configuration.rst b/doc/source/cli/command-objects/configuration.rst deleted file mode 100644 index 22fd13e43d..0000000000 --- a/doc/source/cli/command-objects/configuration.rst +++ /dev/null @@ -1,10 +0,0 @@ -============= -configuration -============= - -Available for all services - -.. _configuration-show: - -.. autoprogram-cliff:: openstack.common - :command: configuration show diff --git a/doc/source/cli/command-objects/consistency-group-snapshot.rst b/doc/source/cli/command-objects/consistency-group-snapshot.rst deleted file mode 100644 index 51241685fd..0000000000 --- a/doc/source/cli/command-objects/consistency-group-snapshot.rst +++ /dev/null @@ -1,17 +0,0 @@ -========================== -consistency group snapshot -========================== - -Block Storage v2, v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: consistency group snapshot create - -.. autoprogram-cliff:: openstack.volume.v3 - :command: consistency group snapshot delete - -.. autoprogram-cliff:: openstack.volume.v3 - :command: consistency group snapshot list - -.. autoprogram-cliff:: openstack.volume.v3 - :command: consistency group snapshot show diff --git a/doc/source/cli/command-objects/consistency-group.rst b/doc/source/cli/command-objects/consistency-group.rst deleted file mode 100644 index 9ff207ff79..0000000000 --- a/doc/source/cli/command-objects/consistency-group.rst +++ /dev/null @@ -1,26 +0,0 @@ -================= -consistency group -================= - -Block Storage v2, v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: consistency group add volume - -.. autoprogram-cliff:: openstack.volume.v3 - :command: consistency group create - -.. autoprogram-cliff:: openstack.volume.v3 - :command: consistency group delete - -.. autoprogram-cliff:: openstack.volume.v3 - :command: consistency group list - -.. autoprogram-cliff:: openstack.volume.v3 - :command: consistency group remove volume - -.. autoprogram-cliff:: openstack.volume.v3 - :command: consistency group set - -.. autoprogram-cliff:: openstack.volume.v3 - :command: consistency group show diff --git a/doc/source/cli/command-objects/console-connection.rst b/doc/source/cli/command-objects/console-connection.rst deleted file mode 100644 index c3358050fb..0000000000 --- a/doc/source/cli/command-objects/console-connection.rst +++ /dev/null @@ -1,10 +0,0 @@ -================== -console connection -================== - -Server console connection information - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: console connection show diff --git a/doc/source/cli/command-objects/console-log.rst b/doc/source/cli/command-objects/console-log.rst deleted file mode 100644 index 46ef370d84..0000000000 --- a/doc/source/cli/command-objects/console-log.rst +++ /dev/null @@ -1,10 +0,0 @@ -=========== -console log -=========== - -Server console text dump - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: console log * diff --git a/doc/source/cli/command-objects/console-url.rst b/doc/source/cli/command-objects/console-url.rst deleted file mode 100644 index 001ccc56c7..0000000000 --- a/doc/source/cli/command-objects/console-url.rst +++ /dev/null @@ -1,10 +0,0 @@ -=========== -console url -=========== - -Server remote console URL - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: console url * diff --git a/doc/source/cli/command-objects/consumer.rst b/doc/source/cli/command-objects/consumer.rst deleted file mode 100644 index 17cfc0c996..0000000000 --- a/doc/source/cli/command-objects/consumer.rst +++ /dev/null @@ -1,10 +0,0 @@ -======== -consumer -======== - -An **consumer** is used by the Identity service's OS-OAUTH1 extension. It -is used to create a **request token** and **access token**. Applicable to -Identity v3. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: consumer * diff --git a/doc/source/cli/command-objects/container.rst b/doc/source/cli/command-objects/container.rst deleted file mode 100644 index 83deeb8156..0000000000 --- a/doc/source/cli/command-objects/container.rst +++ /dev/null @@ -1,26 +0,0 @@ -========= -container -========= - -A **container** defines a namespace for **objects**. Applies to Object Storage v1 - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: container create - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: container delete - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: container list - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: container save - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: container set - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: container show - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: container unset diff --git a/doc/source/cli/command-objects/credential.rst b/doc/source/cli/command-objects/credential.rst deleted file mode 100644 index f490f1cfee..0000000000 --- a/doc/source/cli/command-objects/credential.rst +++ /dev/null @@ -1,8 +0,0 @@ -========== -credential -========== - -Identity v3 - -.. autoprogram-cliff:: openstack.identity.v3 - :command: credential * diff --git a/doc/source/cli/command-objects/default-security-group-rule.rst b/doc/source/cli/command-objects/default-security-group-rule.rst deleted file mode 100644 index 1905614bfc..0000000000 --- a/doc/source/cli/command-objects/default-security-group-rule.rst +++ /dev/null @@ -1,11 +0,0 @@ -=========================== -default security group rule -=========================== - -A **default security group rule** specifies the template of the security group -rules which will be used by neutron to create rules in every new security group. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: default security group rule * diff --git a/doc/source/cli/command-objects/domain.rst b/doc/source/cli/command-objects/domain.rst deleted file mode 100644 index 81a193497b..0000000000 --- a/doc/source/cli/command-objects/domain.rst +++ /dev/null @@ -1,22 +0,0 @@ -====== -domain -====== - -A **domain** is a collection of **users**, **groups**, and **projects**. Each -**group** and **project** is owned by exactly one **domain**. Applies to -Identity v3. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: domain create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: domain delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: domain list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: domain set - -.. autoprogram-cliff:: openstack.identity.v3 - :command: domain show diff --git a/doc/source/cli/command-objects/ec2-credentials-v2.rst b/doc/source/cli/command-objects/ec2-credentials-v2.rst deleted file mode 100644 index ace3d84ec5..0000000000 --- a/doc/source/cli/command-objects/ec2-credentials-v2.rst +++ /dev/null @@ -1,6 +0,0 @@ -============================= -ec2 credentials (Identity v2) -============================= - -.. autoprogram-cliff:: openstack.identity.v2 - :command: ec2 credentials * diff --git a/doc/source/cli/command-objects/ec2-credentials-v3.rst b/doc/source/cli/command-objects/ec2-credentials-v3.rst deleted file mode 100644 index 761d428050..0000000000 --- a/doc/source/cli/command-objects/ec2-credentials-v3.rst +++ /dev/null @@ -1,6 +0,0 @@ -============================= -ec2 credentials (Identity v3) -============================= - -.. autoprogram-cliff:: openstack.identity.v3 - :command: ec2 credentials * diff --git a/doc/source/cli/command-objects/endpoint-v2.rst b/doc/source/cli/command-objects/endpoint-v2.rst deleted file mode 100644 index 7badfd4080..0000000000 --- a/doc/source/cli/command-objects/endpoint-v2.rst +++ /dev/null @@ -1,6 +0,0 @@ -====================== -endpoint (Identity v2) -====================== - -.. autoprogram-cliff:: openstack.identity.v2 - :command: endpoint * diff --git a/doc/source/cli/command-objects/endpoint-v3.rst b/doc/source/cli/command-objects/endpoint-v3.rst deleted file mode 100644 index f12063fb0c..0000000000 --- a/doc/source/cli/command-objects/endpoint-v3.rst +++ /dev/null @@ -1,24 +0,0 @@ -====================== -endpoint (Identity v3) -====================== - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint add project - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint remove project - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint set - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint show diff --git a/doc/source/cli/command-objects/endpoint_group.rst b/doc/source/cli/command-objects/endpoint_group.rst deleted file mode 100644 index b0d988e17c..0000000000 --- a/doc/source/cli/command-objects/endpoint_group.rst +++ /dev/null @@ -1,10 +0,0 @@ -============== -endpoint group -============== - -A **endpoint group** is used to create groups of endpoints that then -can be used to filter the endpoints that are available to a project. -Applicable to Identity v3 - -.. autoprogram-cliff:: openstack.identity.v3 - :command: endpoint group * diff --git a/doc/source/cli/command-objects/extension.rst b/doc/source/cli/command-objects/extension.rst deleted file mode 100644 index 1002a5fff0..0000000000 --- a/doc/source/cli/command-objects/extension.rst +++ /dev/null @@ -1,10 +0,0 @@ -========= -extension -========= - -Many OpenStack server APIs include API extensions that enable -additional functionality. - - -.. autoprogram-cliff:: openstack.common - :command: extension * diff --git a/doc/source/cli/command-objects/federation-domain-project.rst b/doc/source/cli/command-objects/federation-domain-project.rst deleted file mode 100644 index 68db5705b0..0000000000 --- a/doc/source/cli/command-objects/federation-domain-project.rst +++ /dev/null @@ -1,11 +0,0 @@ -========================= -federation domain/project -========================= - -Identity v3 - -.. autoprogram-cliff:: openstack.identity.v3 - :command: federation domain * - -.. autoprogram-cliff:: openstack.identity.v3 - :command: federation project * diff --git a/doc/source/cli/command-objects/federation-protocol.rst b/doc/source/cli/command-objects/federation-protocol.rst deleted file mode 100644 index 81b3f9fcd3..0000000000 --- a/doc/source/cli/command-objects/federation-protocol.rst +++ /dev/null @@ -1,10 +0,0 @@ -=================== -federation protocol -=================== - -A **federation protocol** is used by the Identity service's OS-FEDERATION -extension. It is used by **identity providers** and **mappings**. Applicable to -Identity v3. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: federation protocol * diff --git a/doc/source/cli/command-objects/flavor.rst b/doc/source/cli/command-objects/flavor.rst deleted file mode 100644 index ee09b3ace7..0000000000 --- a/doc/source/cli/command-objects/flavor.rst +++ /dev/null @@ -1,8 +0,0 @@ -====== -flavor -====== - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: flavor * diff --git a/doc/source/cli/command-objects/floating-ip-pool.rst b/doc/source/cli/command-objects/floating-ip-pool.rst deleted file mode 100644 index ab6f83a993..0000000000 --- a/doc/source/cli/command-objects/floating-ip-pool.rst +++ /dev/null @@ -1,8 +0,0 @@ -================ -floating ip pool -================ - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: floating ip pool * diff --git a/doc/source/cli/command-objects/floating-ip-port-forwarding.rst b/doc/source/cli/command-objects/floating-ip-port-forwarding.rst deleted file mode 100644 index 052ea0cc7f..0000000000 --- a/doc/source/cli/command-objects/floating-ip-port-forwarding.rst +++ /dev/null @@ -1,9 +0,0 @@ -=========================== -floating ip port forwarding -=========================== - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: floating ip port forwarding * - diff --git a/doc/source/cli/command-objects/floating-ip.rst b/doc/source/cli/command-objects/floating-ip.rst deleted file mode 100644 index c781a1edb4..0000000000 --- a/doc/source/cli/command-objects/floating-ip.rst +++ /dev/null @@ -1,26 +0,0 @@ -=========== -floating ip -=========== - -Network v2 - -.. NOTE(efried): have to list these out one by one; 'floating ip' pulls in - ... pool and ... port forwarding. - -.. autoprogram-cliff:: openstack.network.v2 - :command: floating ip create - -.. autoprogram-cliff:: openstack.network.v2 - :command: floating ip delete - -.. autoprogram-cliff:: openstack.network.v2 - :command: floating ip list - -.. autoprogram-cliff:: openstack.network.v2 - :command: floating ip set - -.. autoprogram-cliff:: openstack.network.v2 - :command: floating ip show - -.. autoprogram-cliff:: openstack.network.v2 - :command: floating ip unset diff --git a/doc/source/cli/command-objects/group.rst b/doc/source/cli/command-objects/group.rst deleted file mode 100644 index a1071069db..0000000000 --- a/doc/source/cli/command-objects/group.rst +++ /dev/null @@ -1,8 +0,0 @@ -===== -group -===== - -Identity v3 - -.. autoprogram-cliff:: openstack.identity.v3 - :command: group * diff --git a/doc/source/cli/command-objects/host.rst b/doc/source/cli/command-objects/host.rst deleted file mode 100644 index acd5a287fe..0000000000 --- a/doc/source/cli/command-objects/host.rst +++ /dev/null @@ -1,10 +0,0 @@ -==== -host -==== - -Compute v2 - -The physical computer running a hypervisor. - -.. autoprogram-cliff:: openstack.compute.v2 - :command: host * diff --git a/doc/source/cli/command-objects/hypervisor-stats.rst b/doc/source/cli/command-objects/hypervisor-stats.rst deleted file mode 100644 index 1f5768f298..0000000000 --- a/doc/source/cli/command-objects/hypervisor-stats.rst +++ /dev/null @@ -1,8 +0,0 @@ -================ -hypervisor stats -================ - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: hypervisor stats * diff --git a/doc/source/cli/command-objects/hypervisor.rst b/doc/source/cli/command-objects/hypervisor.rst deleted file mode 100644 index 9ae82bf122..0000000000 --- a/doc/source/cli/command-objects/hypervisor.rst +++ /dev/null @@ -1,14 +0,0 @@ -========== -hypervisor -========== - -Compute v2 - -.. NOTE(efried): have to list these out one by one; 'hypervisor *' pulls in - ... stats. - -.. autoprogram-cliff:: openstack.compute.v2 - :command: hypervisor list - -.. autoprogram-cliff:: openstack.compute.v2 - :command: hypervisor show diff --git a/doc/source/cli/command-objects/identity-provider.rst b/doc/source/cli/command-objects/identity-provider.rst deleted file mode 100644 index ed85cb7ac8..0000000000 --- a/doc/source/cli/command-objects/identity-provider.rst +++ /dev/null @@ -1,10 +0,0 @@ -================= -identity provider -================= - -An **identity provider** is used by the Identity service's OS-FEDERATION -extension. It is used by **federation protocols** and **mappings**. Applicable -to Identity v3. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: identity provider * diff --git a/doc/source/cli/command-objects/identity/index.rst b/doc/source/cli/command-objects/identity/index.rst new file mode 100644 index 0000000000..4057faae35 --- /dev/null +++ b/doc/source/cli/command-objects/identity/index.rst @@ -0,0 +1,9 @@ +================= +Identity Commands +================= + +.. toctree:: + :maxdepth: 1 + + v2/index + v3/index diff --git a/doc/source/cli/command-objects/identity/v2/index.rst b/doc/source/cli/command-objects/identity/v2/index.rst new file mode 100644 index 0000000000..c87ae49bc4 --- /dev/null +++ b/doc/source/cli/command-objects/identity/v2/index.rst @@ -0,0 +1,59 @@ +==================== +Identity v2 Commands +==================== + + +catalog +------- + +.. autoprogram-cliff:: openstack.identity.v2 + :command: catalog * + + +ec2 credentials (Identity v2) +----------------------------- + +.. autoprogram-cliff:: openstack.identity.v2 + :command: ec2 credentials * + + +endpoint (Identity v2) +---------------------- + +.. autoprogram-cliff:: openstack.identity.v2 + :command: endpoint * + + +project (Identity v2) +--------------------- + +.. autoprogram-cliff:: openstack.identity.v2 + :command: project * + + +role (Identity v2) +------------------ + +.. autoprogram-cliff:: openstack.identity.v2 + :command: role * + + +service (Identity v2) +--------------------- + +.. autoprogram-cliff:: openstack.identity.v2 + :command: service * + + +token (Identity v2) +------------------- + +.. autoprogram-cliff:: openstack.identity.v2 + :command: token * + + +user (Identity v2) +------------------ + +.. autoprogram-cliff:: openstack.identity.v2 + :command: user * diff --git a/doc/source/cli/command-objects/identity/v3/index.rst b/doc/source/cli/command-objects/identity/v3/index.rst new file mode 100644 index 0000000000..b4e1a0b6ac --- /dev/null +++ b/doc/source/cli/command-objects/identity/v3/index.rst @@ -0,0 +1,311 @@ +==================== +Identity v3 Commands +==================== + + +access rule +----------- + +Access rules are fine-grained permissions for application credentials. An access +rule comprises of a service type, a request path, and a request method. Access +rules may only be created as attributes of application credentials, but they may +be viewed and deleted independently. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: access rule delete + +.. autoprogram-cliff:: openstack.identity.v3 + :command: access rule list + +.. autoprogram-cliff:: openstack.identity.v3 + :command: access rule show + + +access token +------------ + +An **access token** is used by the Identity service's OS-OAUTH1 extension. It +is used by the **consumer** to request new Identity API tokens on behalf of the +authorizing **user**. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: access token create + + +application credential +---------------------- + +With application credentials, a user can grant their applications limited +access to their cloud resources. Once created, users can authenticate with an +application credential by using the ``v3applicationcredential`` auth type. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: application credential * + + +catalog +------- + +A **catalog** lists OpenStack services that are available on the cloud. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: catalog * + + +consumer +-------- + +An **consumer** is used by the Identity service's OS-OAUTH1 extension. It +is used to create a **request token** and **access token**. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: consumer * + + +credential +---------- + +.. autoprogram-cliff:: openstack.identity.v3 + :command: credential * + + +domain +------ + +A **domain** is a collection of **users**, **groups**, and **projects**. Each +**group** and **project** is owned by exactly one **domain**. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: domain create + +.. autoprogram-cliff:: openstack.identity.v3 + :command: domain delete + +.. autoprogram-cliff:: openstack.identity.v3 + :command: domain list + +.. autoprogram-cliff:: openstack.identity.v3 + :command: domain set + +.. autoprogram-cliff:: openstack.identity.v3 + :command: domain show + + +ec2 credentials (Identity v3) +----------------------------- + +.. autoprogram-cliff:: openstack.identity.v3 + :command: ec2 credentials * + + +endpoint group +-------------- + +A **endpoint group** is used to create groups of endpoints that then +can be used to filter the endpoints that are available to a project. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint group * + + +endpoint (Identity v3) +---------------------- + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint add project + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint create + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint delete + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint list + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint remove project + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint set + +.. autoprogram-cliff:: openstack.identity.v3 + :command: endpoint show + + +federation domain/project +------------------------- + +.. autoprogram-cliff:: openstack.identity.v3 + :command: federation domain * + +.. autoprogram-cliff:: openstack.identity.v3 + :command: federation project * + + +federation protocol +------------------- + +A **federation protocol** is used by the Identity service's OS-FEDERATION +extension. It is used by **identity providers** and **mappings**. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: federation protocol * + + +group +----- + +.. autoprogram-cliff:: openstack.identity.v3 + :command: group * + + +identity provider +----------------- + +An **identity provider** is used by the Identity service's OS-FEDERATION +extension. It is used by **federation protocols** and **mappings**. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: identity provider * + + +implied role +------------ + +.. autoprogram-cliff:: openstack.identity.v3 + :command: implied role * + + +limit +----- + +Limits are used to specify project-specific limits thresholds of resources. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: limit * + + +mapping +------- + +A **mapping** is used by the Identity service's OS-FEDERATION +extension. It is used by **federation protocols** and **identity providers**. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: mapping * + + +policy +------ + +A **policy** is an arbitrarily serialized policy engine rule set to be consumed +by a remote service. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: policy * + + +project (Identity v3) +--------------------- + +.. autoprogram-cliff:: openstack.identity.v3 + :command: project * + + +region +------ + +A **region** is a general division of an OpenStack deployment. You can associate +zero or more sub-regions with a region to create a tree-like structured +hierarchy. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: region * + + +registered limit +---------------- + +Registered limits are used to define default limits for resources within a +deployment. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: registered limit * + + +request token +------------- + +A **request token** is used by the Identity service's OS-OAUTH1 extension. It +is used by the **consumer** to request **access tokens**. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: request token * + + +role assignment +--------------- + +.. autoprogram-cliff:: openstack.identity.v3 + :command: role assignment list + + +role (Identity v3) +------------------ + +.. autoprogram-cliff:: openstack.identity.v3 + :command: role * + + +service provider +---------------- + +A **service provider** is used by the Identity service's OS-FEDERATION +extension. It is used by to register another OpenStack Identity service. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: service provider * + + +service (Identity v3) +--------------------- + +.. autoprogram-cliff:: openstack.identity.v3 + :command: service create + +.. autoprogram-cliff:: openstack.identity.v3 + :command: service delete + +.. autoprogram-cliff:: openstack.identity.v3 + :command: service list + +.. autoprogram-cliff:: openstack.identity.v3 + :command: service show + +.. autoprogram-cliff:: openstack.identity.v3 + :command: service set + + +token (Identity v3) +------------------- + +.. autoprogram-cliff:: openstack.identity.v3 + :command: token * + + +trust +----- + +A **trust** provide project-specific role delegation between users, with +optional impersonation. Requires the OS-TRUST extension. + +.. autoprogram-cliff:: openstack.identity.v3 + :command: trust * + + +user (Identity v3) +------------------ + +.. autoprogram-cliff:: openstack.identity.v3 + :command: user * diff --git a/doc/source/cli/command-objects/image-member.rst b/doc/source/cli/command-objects/image-member.rst deleted file mode 100644 index 4b48991cb3..0000000000 --- a/doc/source/cli/command-objects/image-member.rst +++ /dev/null @@ -1,14 +0,0 @@ -============ -image member -============ - -Image v2 - -.. autoprogram-cliff:: openstack.image.v2 - :command: image add project - -.. autoprogram-cliff:: openstack.image.v2 - :command: image remove project - -.. autoprogram-cliff:: openstack.image.v2 - :command: image member list diff --git a/doc/source/cli/command-objects/image-metadef.rst b/doc/source/cli/command-objects/image-metadef.rst deleted file mode 100644 index ed36d95f75..0000000000 --- a/doc/source/cli/command-objects/image-metadef.rst +++ /dev/null @@ -1,50 +0,0 @@ -============= -image metadef -============= - -Image v2 - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef namespace create - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef namespace delete - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef namespace list - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef namespace set - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef namespace show - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef resource type list - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef object create - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef object show - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef object list - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef object delete - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef object update - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef object property show - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef property create - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef property list - -.. autoprogram-cliff:: openstack.image.v2 - :command: image metadef property show diff --git a/doc/source/cli/command-objects/image-task.rst b/doc/source/cli/command-objects/image-task.rst deleted file mode 100644 index 0021192042..0000000000 --- a/doc/source/cli/command-objects/image-task.rst +++ /dev/null @@ -1,11 +0,0 @@ -========== -image task -========== - -Image v2 - -.. autoprogram-cliff:: openstack.image.v2 - :command: image task list - -.. autoprogram-cliff:: openstack.image.v2 - :command: image task show diff --git a/doc/source/cli/command-objects/image-v2.rst b/doc/source/cli/command-objects/image-v2.rst deleted file mode 100644 index 4a2a4d0f90..0000000000 --- a/doc/source/cli/command-objects/image-v2.rst +++ /dev/null @@ -1,32 +0,0 @@ -===== -image -===== - -Image v2 - -.. autoprogram-cliff:: openstack.image.v2 - :command: image create - -.. autoprogram-cliff:: openstack.image.v2 - :command: image delete - -.. autoprogram-cliff:: openstack.image.v2 - :command: image list - -.. autoprogram-cliff:: openstack.image.v2 - :command: image save - -.. autoprogram-cliff:: openstack.image.v2 - :command: image set - -.. autoprogram-cliff:: openstack.image.v2 - :command: image unset - -.. autoprogram-cliff:: openstack.image.v2 - :command: image show - -.. autoprogram-cliff:: openstack.image.v2 - :command: image stage - -.. autoprogram-cliff:: openstack.image.v2 - :command: image import diff --git a/doc/source/cli/command-objects/image/index.rst b/doc/source/cli/command-objects/image/index.rst new file mode 100644 index 0000000000..12470e5d73 --- /dev/null +++ b/doc/source/cli/command-objects/image/index.rst @@ -0,0 +1,9 @@ +============== +Image Commands +============== + +.. toctree:: + :maxdepth: 1 + + v1/index + v2/index diff --git a/doc/source/cli/command-objects/image-v1.rst b/doc/source/cli/command-objects/image/v1/index.rst similarity index 52% rename from doc/source/cli/command-objects/image-v1.rst rename to doc/source/cli/command-objects/image/v1/index.rst index 4f7edc4322..fc93124405 100644 --- a/doc/source/cli/command-objects/image-v1.rst +++ b/doc/source/cli/command-objects/image/v1/index.rst @@ -1,6 +1,10 @@ -======== +================= +Image v1 Commands +================= + + image v1 -======== +-------- .. autoprogram-cliff:: openstack.image.v1 :command: image * diff --git a/doc/source/cli/command-objects/image/v2/index.rst b/doc/source/cli/command-objects/image/v2/index.rst new file mode 100644 index 0000000000..d98957acef --- /dev/null +++ b/doc/source/cli/command-objects/image/v2/index.rst @@ -0,0 +1,157 @@ +================= +Image v2 Commands +================= + + +image member +------------ + +.. autoprogram-cliff:: openstack.image.v2 + :command: image add project + +.. autoprogram-cliff:: openstack.image.v2 + :command: image remove project + +.. autoprogram-cliff:: openstack.image.v2 + :command: image member list + +.. autoprogram-cliff:: openstack.image.v2 + :command: image member get + + +image metadef +------------- + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef namespace create + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef namespace delete + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef namespace list + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef namespace set + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef namespace show + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef resource type list + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef object create + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef object show + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef object list + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef object delete + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef object update + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef object property show + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef property create + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef property list + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef property show + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef property delete + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef property set + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef namespace unset + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef resource type association create + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef resource type association delete + +.. autoprogram-cliff:: openstack.image.v2 + :command: image metadef resource type association list + + +image task +---------- + +.. autoprogram-cliff:: openstack.image.v2 + :command: image task list + +.. autoprogram-cliff:: openstack.image.v2 + :command: image task show + + +image +----- + +.. autoprogram-cliff:: openstack.image.v2 + :command: image create + +.. autoprogram-cliff:: openstack.image.v2 + :command: image delete + +.. autoprogram-cliff:: openstack.image.v2 + :command: image list + +.. autoprogram-cliff:: openstack.image.v2 + :command: image save + +.. autoprogram-cliff:: openstack.image.v2 + :command: image set + +.. autoprogram-cliff:: openstack.image.v2 + :command: image unset + +.. autoprogram-cliff:: openstack.image.v2 + :command: image show + +.. autoprogram-cliff:: openstack.image.v2 + :command: image stage + +.. autoprogram-cliff:: openstack.image.v2 + :command: image import + + +image import info +----------------- + +.. autoprogram-cliff:: openstack.image.v2 + :command: image import info + + +image stores +------------ + +.. autoprogram-cliff:: openstack.image.v2 + :command: image stores list + + +cached image +------------ + +.. autoprogram-cliff:: openstack.image.v2 + :command: cached image list + +.. autoprogram-cliff:: openstack.image.v2 + :command: cached image queue + +.. autoprogram-cliff:: openstack.image.v2 + :command: cached image delete + +.. autoprogram-cliff:: openstack.image.v2 + :command: cached image clear diff --git a/doc/source/cli/command-objects/implied_role.rst b/doc/source/cli/command-objects/implied_role.rst deleted file mode 100644 index 09532c0b02..0000000000 --- a/doc/source/cli/command-objects/implied_role.rst +++ /dev/null @@ -1,8 +0,0 @@ -============ -implied role -============ - -Identity v3 - -.. autoprogram-cliff:: openstack.identity.v3 - :command: implied role * diff --git a/doc/source/cli/command-objects/ip-availability.rst b/doc/source/cli/command-objects/ip-availability.rst deleted file mode 100644 index 94e60459b1..0000000000 --- a/doc/source/cli/command-objects/ip-availability.rst +++ /dev/null @@ -1,8 +0,0 @@ -=============== -ip availability -=============== - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: ip availability * diff --git a/doc/source/cli/command-objects/keypair.rst b/doc/source/cli/command-objects/keypair.rst deleted file mode 100644 index f8bce3756b..0000000000 --- a/doc/source/cli/command-objects/keypair.rst +++ /dev/null @@ -1,13 +0,0 @@ -======= -keypair -======= - -The badly named keypair is really the public key of an OpenSSH key pair to be -used for access to created servers. You can also create a private key for -access to a created server by not passing any argument to the keypair create -command. - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: keypair * diff --git a/doc/source/cli/command-objects/limit.rst b/doc/source/cli/command-objects/limit.rst deleted file mode 100644 index 784d0cb423..0000000000 --- a/doc/source/cli/command-objects/limit.rst +++ /dev/null @@ -1,10 +0,0 @@ -===== -limit -===== - -Identity v3 - -Limits are used to specify project-specific limits thresholds of resources. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: limit * diff --git a/doc/source/cli/command-objects/limits.rst b/doc/source/cli/command-objects/limits.rst deleted file mode 100644 index 11d53802c6..0000000000 --- a/doc/source/cli/command-objects/limits.rst +++ /dev/null @@ -1,11 +0,0 @@ -====== -limits -====== - -The Compute and Block Storage APIs have resource usage limits. - -Block Storage v2, v3; Compute v2 - - -.. autoprogram-cliff:: openstack.common - :command: limits * diff --git a/doc/source/cli/command-objects/local-ip-association.rst b/doc/source/cli/command-objects/local-ip-association.rst deleted file mode 100644 index 824ee4d0e6..0000000000 --- a/doc/source/cli/command-objects/local-ip-association.rst +++ /dev/null @@ -1,11 +0,0 @@ -============================================= -Local IP Associations (local_ip_associations) -============================================= - -The resource lets users assign Local IPs to user Ports. -This is a sub-resource of the Local IP resource. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: local ip association * diff --git a/doc/source/cli/command-objects/local-ip.rst b/doc/source/cli/command-objects/local-ip.rst deleted file mode 100644 index c8c5ab470d..0000000000 --- a/doc/source/cli/command-objects/local-ip.rst +++ /dev/null @@ -1,12 +0,0 @@ -===================== -Local IPs (local_ips) -===================== - -Extension that allows users to create a virtual IP that can later be assigned -to multiple ports/VMs (similar to anycast IP) and is guaranteed to only be -reachable within the same physical server/node boundaries - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: local ip * diff --git a/doc/source/cli/command-objects/mapping.rst b/doc/source/cli/command-objects/mapping.rst deleted file mode 100644 index 5653b52bb9..0000000000 --- a/doc/source/cli/command-objects/mapping.rst +++ /dev/null @@ -1,10 +0,0 @@ -======= -mapping -======= - -A **mapping** is used by the Identity service's OS-FEDERATION -extension. It is used by **federation protocols** and **identity providers**. -Applicable to Identity v3. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: mapping * \ No newline at end of file diff --git a/doc/source/cli/command-objects/module.rst b/doc/source/cli/command-objects/module.rst deleted file mode 100644 index 82269f472d..0000000000 --- a/doc/source/cli/command-objects/module.rst +++ /dev/null @@ -1,10 +0,0 @@ -====== -module -====== - -Internal - -Installed Python modules in the OSC process. - -.. autoprogram-cliff:: openstack.cli - :command: module * diff --git a/doc/source/cli/command-objects/network-agent.rst b/doc/source/cli/command-objects/network-agent.rst deleted file mode 100644 index 7e721db1ea..0000000000 --- a/doc/source/cli/command-objects/network-agent.rst +++ /dev/null @@ -1,14 +0,0 @@ -============= -network agent -============= - -A **network agent** is an agent that handles various tasks used to -implement virtual networks. These agents include neutron-dhcp-agent, -neutron-l3-agent, neutron-metering-agent, and neutron-lbaas-agent, -among others. The agent is available when the alive status of the -agent is "True". - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: network agent * diff --git a/doc/source/cli/command-objects/network-auto-allocated-topology.rst b/doc/source/cli/command-objects/network-auto-allocated-topology.rst deleted file mode 100644 index 436836484a..0000000000 --- a/doc/source/cli/command-objects/network-auto-allocated-topology.rst +++ /dev/null @@ -1,14 +0,0 @@ -=============================== -network auto allocated topology -=============================== - -An **auto allocated topology** allows admins to quickly set up external -connectivity for end-users. Only one auto allocated topology is allowed per -project. For more information on how to set up the resources required -for auto allocated topology review :neutron-doc:`the documentation -`. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: network auto allocated topology * diff --git a/doc/source/cli/command-objects/network-flavor-profile.rst b/doc/source/cli/command-objects/network-flavor-profile.rst deleted file mode 100644 index c5e9aa0c13..0000000000 --- a/doc/source/cli/command-objects/network-flavor-profile.rst +++ /dev/null @@ -1,15 +0,0 @@ -====================== -network flavor profile -====================== - -A **network flavor profile** allows administrators to create, delete, list, -show and update network service profile, which details a framework to enable -operators to configure and users to select from different abstract -representations of a service implementation in the Networking service. -It decouples the logical configuration from its instantiation enabling -operators to create user options according to deployment needs. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: network flavor profile * diff --git a/doc/source/cli/command-objects/network-flavor.rst b/doc/source/cli/command-objects/network-flavor.rst deleted file mode 100644 index 6fe7504a0d..0000000000 --- a/doc/source/cli/command-objects/network-flavor.rst +++ /dev/null @@ -1,33 +0,0 @@ -============== -network flavor -============== - -A **network flavor** extension allows the user selection of operator-curated -flavors during resource creations. It allows administrators to create network -service flavors. - -Network v2 - -.. NOTE(efried): have to list these out one by one; 'network flavor' pulls in - ... profile *. - -.. autoprogram-cliff:: openstack.network.v2 - :command: network flavor add profile - -.. autoprogram-cliff:: openstack.network.v2 - :command: network flavor create - -.. autoprogram-cliff:: openstack.network.v2 - :command: network flavor delete - -.. autoprogram-cliff:: openstack.network.v2 - :command: network flavor list - -.. autoprogram-cliff:: openstack.network.v2 - :command: network flavor remove profile - -.. autoprogram-cliff:: openstack.network.v2 - :command: network flavor set - -.. autoprogram-cliff:: openstack.network.v2 - :command: network flavor show diff --git a/doc/source/cli/command-objects/network-l3-conntrack-helper.rst b/doc/source/cli/command-objects/network-l3-conntrack-helper.rst deleted file mode 100644 index badbab6337..0000000000 --- a/doc/source/cli/command-objects/network-l3-conntrack-helper.rst +++ /dev/null @@ -1,8 +0,0 @@ -=========================== -network l3 conntrack helper -=========================== - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: network l3 conntrack helper * diff --git a/doc/source/cli/command-objects/network-meter-rule.rst b/doc/source/cli/command-objects/network-meter-rule.rst deleted file mode 100644 index 616c4bc516..0000000000 --- a/doc/source/cli/command-objects/network-meter-rule.rst +++ /dev/null @@ -1,13 +0,0 @@ -================== -network meter rule -================== - -A **meter rule** sets the rule for -a meter to measure traffic for a specific IP range. -The following uses **meter** and requires the L3 -metering extension. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: network meter rule * diff --git a/doc/source/cli/command-objects/network-meter.rst b/doc/source/cli/command-objects/network-meter.rst deleted file mode 100644 index 5752e6f419..0000000000 --- a/doc/source/cli/command-objects/network-meter.rst +++ /dev/null @@ -1,24 +0,0 @@ -============= -network meter -============= - -A **network meter** allows operators to measure -traffic for a specific IP range. The following commands -are specific to the L3 metering extension. - -Network v2 - -.. NOTE(efried): have to list these out one by one; 'network meter *' pulls in - ... rule *. - -.. autoprogram-cliff:: openstack.network.v2 - :command: network meter create - -.. autoprogram-cliff:: openstack.network.v2 - :command: network meter delete - -.. autoprogram-cliff:: openstack.network.v2 - :command: network meter list - -.. autoprogram-cliff:: openstack.network.v2 - :command: network meter show diff --git a/doc/source/cli/command-objects/network-qos-policy.rst b/doc/source/cli/command-objects/network-qos-policy.rst deleted file mode 100644 index af7a104070..0000000000 --- a/doc/source/cli/command-objects/network-qos-policy.rst +++ /dev/null @@ -1,11 +0,0 @@ -================== -network qos policy -================== - -A **Network QoS policy** groups a number of Network QoS rules, applied to a -network or a port. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: network qos policy * diff --git a/doc/source/cli/command-objects/network-qos-rule-type.rst b/doc/source/cli/command-objects/network-qos-rule-type.rst deleted file mode 100644 index 197a86002f..0000000000 --- a/doc/source/cli/command-objects/network-qos-rule-type.rst +++ /dev/null @@ -1,11 +0,0 @@ -===================== -network qos rule type -===================== - -A **Network QoS rule type** is a specific Network QoS rule type available to be -used. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: network qos rule type * diff --git a/doc/source/cli/command-objects/network-qos-rule.rst b/doc/source/cli/command-objects/network-qos-rule.rst deleted file mode 100644 index 8b715c03ab..0000000000 --- a/doc/source/cli/command-objects/network-qos-rule.rst +++ /dev/null @@ -1,28 +0,0 @@ -================ -network qos rule -================ - -A **Network QoS rule** specifies a rule defined in a Network QoS policy; its -type is defined by the parameter 'type'. Can be assigned, within a Network QoS -policy, to a port or a network. Each Network QoS policy can contain several -rules, each of them - -Network v2 - -.. NOTE(efried): have to list these out one by one; 'network qos rule *' pulls - network qos rule type *. - -.. autoprogram-cliff:: openstack.network.v2 - :command: network qos rule create - -.. autoprogram-cliff:: openstack.network.v2 - :command: network qos rule delete - -.. autoprogram-cliff:: openstack.network.v2 - :command: network qos rule list - -.. autoprogram-cliff:: openstack.network.v2 - :command: network qos rule set - -.. autoprogram-cliff:: openstack.network.v2 - :command: network qos rule show diff --git a/doc/source/cli/command-objects/network-rbac.rst b/doc/source/cli/command-objects/network-rbac.rst deleted file mode 100644 index d9e48554d3..0000000000 --- a/doc/source/cli/command-objects/network-rbac.rst +++ /dev/null @@ -1,12 +0,0 @@ -============ -network rbac -============ - -A **network rbac** is a Role-Based Access Control (RBAC) policy for -network resources. It enables both operators and users to grant access -to network resources for specific projects. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: network rbac * diff --git a/doc/source/cli/command-objects/network-segment.rst b/doc/source/cli/command-objects/network-segment.rst deleted file mode 100644 index 2e9143801d..0000000000 --- a/doc/source/cli/command-objects/network-segment.rst +++ /dev/null @@ -1,28 +0,0 @@ -=============== -network segment -=============== - -A **network segment** is an isolated Layer 2 segment within a network. -A network may contain multiple network segments. Depending on the -network configuration, Layer 2 connectivity between network segments -within a network may not be guaranteed. - -Network v2 - -.. NOTE(efried): have to list these out one by one; 'network segment *' pulls - ... range *. - -.. autoprogram-cliff:: openstack.network.v2 - :command: network segment create - -.. autoprogram-cliff:: openstack.network.v2 - :command: network segment delete - -.. autoprogram-cliff:: openstack.network.v2 - :command: network segment list - -.. autoprogram-cliff:: openstack.network.v2 - :command: network segment set - -.. autoprogram-cliff:: openstack.network.v2 - :command: network segment show diff --git a/doc/source/cli/command-objects/network-service-provider.rst b/doc/source/cli/command-objects/network-service-provider.rst deleted file mode 100644 index 52d0288dd0..0000000000 --- a/doc/source/cli/command-objects/network-service-provider.rst +++ /dev/null @@ -1,13 +0,0 @@ -======================== -network service provider -======================== - -A **network service provider** is a particular driver that implements a -networking service - -Network v2 - -.. _network_service_provider_list: - -.. autoprogram-cliff:: openstack.network.v2 - :command: network service provider list diff --git a/doc/source/cli/command-objects/network-trunk.rst b/doc/source/cli/command-objects/network-trunk.rst deleted file mode 100644 index 98fd4b0c4b..0000000000 --- a/doc/source/cli/command-objects/network-trunk.rst +++ /dev/null @@ -1,16 +0,0 @@ -============= -network trunk -============= - -A **network trunk** is a container to group logical ports from different -networks and provide a single trunked vNIC for servers. It consists of -one parent port which is a regular VIF and multiple subports which allow -the server to connect to more networks. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: network subport list - -.. autoprogram-cliff:: openstack.network.v2 - :command: network trunk * diff --git a/doc/source/cli/command-objects/network.rst b/doc/source/cli/command-objects/network.rst deleted file mode 100644 index 626b11a928..0000000000 --- a/doc/source/cli/command-objects/network.rst +++ /dev/null @@ -1,33 +0,0 @@ -======= -network -======= - -A **network** is an isolated Layer 2 networking segment. There are two types -of networks, project and provider networks. Project networks are fully isolated -and are not shared with other projects. Provider networks map to existing -physical networks in the data center and provide external network access for -servers and other resources. Only an OpenStack administrator can create -provider networks. Networks can be connected via routers. - -Compute v2, Network v2 - -.. NOTE(efried): have to list these out one by one; 'network *' pulls in - ... flavor *, ... qos policy *, etc. - -.. autoprogram-cliff:: openstack.network.v2 - :command: network create - -.. autoprogram-cliff:: openstack.network.v2 - :command: network delete - -.. autoprogram-cliff:: openstack.network.v2 - :command: network list - -.. autoprogram-cliff:: openstack.network.v2 - :command: network set - -.. autoprogram-cliff:: openstack.network.v2 - :command: network show - -.. autoprogram-cliff:: openstack.network.v2 - :command: network unset diff --git a/doc/source/cli/command-objects/network/index.rst b/doc/source/cli/command-objects/network/index.rst new file mode 100644 index 0000000000..34a3f06c1e --- /dev/null +++ b/doc/source/cli/command-objects/network/index.rst @@ -0,0 +1,12 @@ +================ +Network Commands +================ + +.. toctree:: + :maxdepth: 1 + + v2/index + v2/bgpvpn/index + v2/dynamic-routing/index + v2/fwaas/index + v2/taas/index diff --git a/doc/source/cli/command-objects/network/v2/bgpvpn/index.rst b/doc/source/cli/command-objects/network/v2/bgpvpn/index.rst new file mode 100644 index 0000000000..e1ef51a478 --- /dev/null +++ b/doc/source/cli/command-objects/network/v2/bgpvpn/index.rst @@ -0,0 +1,46 @@ +========================== +Network v2 BGPVPN Commands +========================== + + +bgpvpn +------ + +.. autoprogram-cliff:: openstack.network.v2.bgpvpn + :command: bgpvpn create + +.. autoprogram-cliff:: openstack.network.v2.bgpvpn + :command: bgpvpn delete + +.. autoprogram-cliff:: openstack.network.v2.bgpvpn + :command: bgpvpn list + +.. autoprogram-cliff:: openstack.network.v2.bgpvpn + :command: bgpvpn set + +.. autoprogram-cliff:: openstack.network.v2.bgpvpn + :command: bgpvpn show + +.. autoprogram-cliff:: openstack.network.v2.bgpvpn + :command: bgpvpn unset + + +bgpvpn network association +--------------------------- + +.. autoprogram-cliff:: openstack.network.v2.bgpvpn + :command: bgpvpn network association * + + +bgpvpn router association +-------------------------- + +.. autoprogram-cliff:: openstack.network.v2.bgpvpn + :command: bgpvpn router association * + + +bgpvpn port association +------------------------ + +.. autoprogram-cliff:: openstack.network.v2.bgpvpn + :command: bgpvpn port association * diff --git a/doc/source/cli/command-objects/network/v2/dynamic-routing/index.rst b/doc/source/cli/command-objects/network/v2/dynamic-routing/index.rst new file mode 100644 index 0000000000..f44373e805 --- /dev/null +++ b/doc/source/cli/command-objects/network/v2/dynamic-routing/index.rst @@ -0,0 +1,24 @@ +=================================== +Network v2 Dynamic Routing Commands +=================================== + + +bgp peer +-------- + +.. autoprogram-cliff:: openstack.network.v2.dynamic_routing + :command: bgp peer * + + +bgp speaker +----------- + +.. autoprogram-cliff:: openstack.network.v2.dynamic_routing + :command: bgp speaker * + + +bgp dragent +----------- + +.. autoprogram-cliff:: openstack.network.v2.dynamic_routing + :command: bgp dragent * diff --git a/doc/source/cli/command-objects/network/v2/fwaas/index.rst b/doc/source/cli/command-objects/network/v2/fwaas/index.rst new file mode 100644 index 0000000000..891e5d0cc9 --- /dev/null +++ b/doc/source/cli/command-objects/network/v2/fwaas/index.rst @@ -0,0 +1,42 @@ +========================= +Network v2 FWaaS Commands +========================= + + +firewall group +-------------- + +.. NOTE(efried): have to list these out one by one; 'firewall group *' pulls in + ... policy * and ... rule *. + +.. autoprogram-cliff:: openstack.network.v2.fwaas + :command: firewall group create + +.. autoprogram-cliff:: openstack.network.v2.fwaas + :command: firewall group delete + +.. autoprogram-cliff:: openstack.network.v2.fwaas + :command: firewall group list + +.. autoprogram-cliff:: openstack.network.v2.fwaas + :command: firewall group set + +.. autoprogram-cliff:: openstack.network.v2.fwaas + :command: firewall group show + +.. autoprogram-cliff:: openstack.network.v2.fwaas + :command: firewall group unset + + +firewall group policy +--------------------- + +.. autoprogram-cliff:: openstack.network.v2.fwaas + :command: firewall group policy * + + +firewall group rule +------------------- + +.. autoprogram-cliff:: openstack.network.v2.fwaas + :command: firewall group rule * diff --git a/doc/source/cli/command-objects/network/v2/index.rst b/doc/source/cli/command-objects/network/v2/index.rst new file mode 100644 index 0000000000..1fbfb86f67 --- /dev/null +++ b/doc/source/cli/command-objects/network/v2/index.rst @@ -0,0 +1,491 @@ +=================== +Network v2 Commands +=================== + + +address group +------------- + +An **address group** is a group of IPv4 or IPv6 address blocks which could be +referenced as a remote source or destination when creating a security group +rule. + +.. autoprogram-cliff:: openstack.network.v2 + :command: address group * + + +address scope +------------- + +An **address scope** is a scope of IPv4 or IPv6 addresses that belongs +to a given project and may be shared between projects. + +.. autoprogram-cliff:: openstack.network.v2 + :command: address scope * + + +default security group rule +--------------------------- + +A **default security group rule** specifies the template of the security group +rules which will be used by neutron to create rules in every new security group. + +.. autoprogram-cliff:: openstack.network.v2 + :command: default security group rule * + + +floating ip port forwarding +--------------------------- + +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip port forwarding * + + +floating ip +----------- + +.. NOTE(efried): have to list these out one by one; 'floating ip' pulls in + ... pool and ... port forwarding. + +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip create + +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip delete + +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip list + +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip set + +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip show + +.. autoprogram-cliff:: openstack.network.v2 + :command: floating ip unset + + +ip availability +--------------- + +.. autoprogram-cliff:: openstack.network.v2 + :command: ip availability * + + +Local IP Associations (local_ip_associations) +--------------------------------------------- + +The resource lets users assign Local IPs to user Ports. +This is a sub-resource of the Local IP resource. + +.. autoprogram-cliff:: openstack.network.v2 + :command: local ip association * + + +Local IPs (local_ips) +--------------------- + +Extension that allows users to create a virtual IP that can later be assigned +to multiple ports/VMs (similar to anycast IP) and is guaranteed to only be +reachable within the same physical server/node boundaries + +.. autoprogram-cliff:: openstack.network.v2 + :command: local ip * + + +network agent +------------- + +A **network agent** is an agent that handles various tasks used to +implement virtual networks. These agents include neutron-dhcp-agent, +neutron-l3-agent, neutron-metering-agent, and neutron-lbaas-agent, +among others. The agent is available when the alive status of the +agent is "True". + +.. autoprogram-cliff:: openstack.network.v2 + :command: network agent * + + +network auto allocated topology +------------------------------- + +An **auto allocated topology** allows admins to quickly set up external +connectivity for end-users. Only one auto allocated topology is allowed per +project. For more information on how to set up the resources required +for auto allocated topology review :neutron-doc:`the documentation +`. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network auto allocated topology * + + +network flavor profile +---------------------- + +A **network flavor profile** allows administrators to create, delete, list, +show and update network service profile, which details a framework to enable +operators to configure and users to select from different abstract +representations of a service implementation in the Networking service. +It decouples the logical configuration from its instantiation enabling +operators to create user options according to deployment needs. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor profile * + + +network flavor +-------------- + +A **network flavor** extension allows the user selection of operator-curated +flavors during resource creations. It allows administrators to create network +service flavors. + + +.. NOTE(efried): have to list these out one by one; 'network flavor' pulls in + ... profile *. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor add profile + +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor create + +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor delete + +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor list + +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor remove profile + +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor set + +.. autoprogram-cliff:: openstack.network.v2 + :command: network flavor show + + +network l3 conntrack helper +--------------------------- + +.. autoprogram-cliff:: openstack.network.v2 + :command: network l3 conntrack helper * + + +network meter rule +------------------ + +A **meter rule** sets the rule for +a meter to measure traffic for a specific IP range. +The following uses **meter** and requires the L3 +metering extension. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network meter rule * + + +network meter +------------- + +A **network meter** allows operators to measure +traffic for a specific IP range. The following commands +are specific to the L3 metering extension. + + +.. NOTE(efried): have to list these out one by one; 'network meter *' pulls in + ... rule *. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network meter create + +.. autoprogram-cliff:: openstack.network.v2 + :command: network meter delete + +.. autoprogram-cliff:: openstack.network.v2 + :command: network meter list + +.. autoprogram-cliff:: openstack.network.v2 + :command: network meter show + + +network qos policy +------------------ + +A **Network QoS policy** groups a number of Network QoS rules, applied to a +network or a port. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos policy * + + +network qos rule type +--------------------- + +A **Network QoS rule type** is a specific Network QoS rule type available to be +used. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule type * + + +network qos rule +---------------- + +A **Network QoS rule** specifies a rule defined in a Network QoS policy; its +type is defined by the parameter 'type'. Can be assigned, within a Network QoS +policy, to a port or a network. Each Network QoS policy can contain several +rules, each of them + + +.. NOTE(efried): have to list these out one by one; 'network qos rule *' pulls + network qos rule type *. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule create + +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule delete + +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule list + +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule set + +.. autoprogram-cliff:: openstack.network.v2 + :command: network qos rule show + + +network rbac +------------ + +A **network rbac** is a Role-Based Access Control (RBAC) policy for +network resources. It enables both operators and users to grant access +to network resources for specific projects. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network rbac * + + +network segment range +--------------------- + +A **network segment range** is a resource for tenant network segment +allocation. +A network segment range exposes the segment range management to be administered +via the Neutron API. In addition, it introduces the ability for the +administrator to control the segment ranges globally or on a per-tenant basis. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment range * + + +network segment +--------------- + +A **network segment** is an isolated Layer 2 segment within a network. +A network may contain multiple network segments. Depending on the +network configuration, Layer 2 connectivity between network segments +within a network may not be guaranteed. + + +.. NOTE(efried): have to list these out one by one; 'network segment *' pulls + ... range *. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment create + +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment delete + +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment list + +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment set + +.. autoprogram-cliff:: openstack.network.v2 + :command: network segment show + + +network service provider +------------------------ + +A **network service provider** is a particular driver that implements a +networking service + + +.. _network_service_provider_list: + +.. autoprogram-cliff:: openstack.network.v2 + :command: network service provider list + + +network trunk +------------- + +A **network trunk** is a container to group logical ports from different +networks and provide a single trunked vNIC for servers. It consists of +one parent port which is a regular VIF and multiple subports which allow +the server to connect to more networks. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network subport list + +.. autoprogram-cliff:: openstack.network.v2 + :command: network trunk * + + +network +------- + +A **network** is an isolated Layer 2 networking segment. There are two types +of networks, project and provider networks. Project networks are fully isolated +and are not shared with other projects. Provider networks map to existing +physical networks in the data center and provide external network access for +servers and other resources. Only an OpenStack administrator can create +provider networks. Networks can be connected via routers. + + +.. NOTE(efried): have to list these out one by one; 'network *' pulls in + ... flavor *, ... qos policy *, etc. + +.. autoprogram-cliff:: openstack.network.v2 + :command: network create + +.. autoprogram-cliff:: openstack.network.v2 + :command: network delete + +.. autoprogram-cliff:: openstack.network.v2 + :command: network list + +.. autoprogram-cliff:: openstack.network.v2 + :command: network set + +.. autoprogram-cliff:: openstack.network.v2 + :command: network show + +.. autoprogram-cliff:: openstack.network.v2 + :command: network unset + + +port +---- + +A **port** is a connection point for attaching a single device, such as the +NIC of a server, to a network. The port also describes the associated network +configuration, such as the MAC and IP addresses to be used on that port. + +.. autoprogram-cliff:: openstack.network.v2 + :command: port * + + +router ndp proxy +---------------- + +An **NDP proxy** publishes a internal IPv6 address to public network. With the +**NDP proxy**, the IPv6 address can be accessed from external. It is similar +to **Floating IP** of IPv4 in functionality. + +.. autoprogram-cliff:: openstack.network.v2 + :command: router ndp proxy * + + +router +------ + +A **router** is a logical component that forwards data packets between +networks. It also provides Layer 3 and NAT forwarding to provide external +network access for servers on project networks. + +.. autoprogram-cliff:: openstack.network.v2 + :command: router * + + +security group default statefulness +----------------------------------- + +.. autoprogram-cliff:: openstack.network.v2 + :command: security group default statefulness * + + +security group rule +------------------- + +A **security group rule** specifies the network access rules for servers +and other resources on the network. + +.. autoprogram-cliff:: openstack.network.v2 + :command: security group rule * + + +security group +-------------- + +A **security group** acts as a virtual firewall for servers and other +resources on a network. It is a container for security group rules +which specify the network access rules. + + +.. NOTE(efried): have to list these out one by one; 'security group *' pulls in + ... rule *. + +.. autoprogram-cliff:: openstack.network.v2 + :command: security group create + +.. autoprogram-cliff:: openstack.network.v2 + :command: security group delete + +.. autoprogram-cliff:: openstack.network.v2 + :command: security group list + +.. autoprogram-cliff:: openstack.network.v2 + :command: security group set + +.. autoprogram-cliff:: openstack.network.v2 + :command: security group show + +.. autoprogram-cliff:: openstack.network.v2 + :command: security group unset + + +subnet pool +----------- + +A **subnet pool** contains a collection of prefixes in CIDR notation +that are available for IP address allocation. + +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet pool * + + +subnet +------ + +A **subnet** is a block of IP addresses and associated configuration state. +Subnets are used to allocate IP addresses when new ports are created on a +network. + + +.. NOTE(efried): have to list these out one by one; 'subnet *' pulls in + subnet pool *. + +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet create + +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet delete + +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet list + +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet set + +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet show + +.. autoprogram-cliff:: openstack.network.v2 + :command: subnet unset diff --git a/doc/source/cli/command-objects/network/v2/taas/index.rst b/doc/source/cli/command-objects/network/v2/taas/index.rst new file mode 100644 index 0000000000..034d4e526f --- /dev/null +++ b/doc/source/cli/command-objects/network/v2/taas/index.rst @@ -0,0 +1,24 @@ +======================== +Network v2 TaaS Commands +======================== + + +tap flow +-------- + +.. autoprogram-cliff:: openstack.network.v2.taas + :command: tap flow * + + +tap mirror +---------- + +.. autoprogram-cliff:: openstack.network.v2.taas + :command: tap mirror * + + +tap service +----------- + +.. autoprogram-cliff:: openstack.network.v2.taas + :command: tap service * diff --git a/doc/source/cli/command-objects/network_segment_range.rst b/doc/source/cli/command-objects/network_segment_range.rst deleted file mode 100644 index f40007f385..0000000000 --- a/doc/source/cli/command-objects/network_segment_range.rst +++ /dev/null @@ -1,14 +0,0 @@ -===================== -network segment range -===================== - -A **network segment range** is a resource for tenant network segment -allocation. -A network segment range exposes the segment range management to be administered -via the Neutron API. In addition, it introduces the ability for the -administrator to control the segment ranges globally or on a per-tenant basis. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: network segment range * diff --git a/doc/source/cli/command-objects/object-store-account.rst b/doc/source/cli/command-objects/object-store-account.rst deleted file mode 100644 index 6beb63aff8..0000000000 --- a/doc/source/cli/command-objects/object-store-account.rst +++ /dev/null @@ -1,15 +0,0 @@ -==================== -object store account -==================== - -An **object store account** represents the top-level of the hierarchy that -is comprised of **containers** and **objects**. Applies to Object Storage v1. - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: object store account set - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: object store account show - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: object store account unset diff --git a/doc/source/cli/command-objects/object-store/index.rst b/doc/source/cli/command-objects/object-store/index.rst new file mode 100644 index 0000000000..fe8fb0842d --- /dev/null +++ b/doc/source/cli/command-objects/object-store/index.rst @@ -0,0 +1,8 @@ +======================= +Object Storage Commands +======================= + +.. toctree:: + :maxdepth: 1 + + v1/index diff --git a/doc/source/cli/command-objects/object-store/v1/index.rst b/doc/source/cli/command-objects/object-store/v1/index.rst new file mode 100644 index 0000000000..2f555b9937 --- /dev/null +++ b/doc/source/cli/command-objects/object-store/v1/index.rst @@ -0,0 +1,74 @@ +========================== +Object Storage v1 Commands +========================== + + +container +--------- + +A **container** defines a namespace for **objects**. + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: container create + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: container delete + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: container list + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: container save + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: container set + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: container show + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: container unset + + +object store account +-------------------- + +An **object store account** represents the top-level of the hierarchy that +is comprised of **containers** and **objects**. + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: object store account set + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: object store account show + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: object store account unset + + +object +------ + +An **object** stores data content, such as documents, images, and so on. They +can also store custom metadata with an object. + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: object create + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: object delete + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: object list + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: object save + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: object set + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: object show + +.. autoprogram-cliff:: openstack.object_store.v1 + :command: object unset diff --git a/doc/source/cli/command-objects/object.rst b/doc/source/cli/command-objects/object.rst deleted file mode 100644 index 8b328bd52f..0000000000 --- a/doc/source/cli/command-objects/object.rst +++ /dev/null @@ -1,27 +0,0 @@ -====== -object -====== - -An **object** stores data content, such as documents, images, and so on. They -can also store custom metadata with an object. Applies to Object Storage v1. - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: object create - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: object delete - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: object list - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: object save - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: object set - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: object show - -.. autoprogram-cliff:: openstack.object_store.v1 - :command: object unset diff --git a/doc/source/cli/command-objects/policy.rst b/doc/source/cli/command-objects/policy.rst deleted file mode 100644 index 66bc2545a6..0000000000 --- a/doc/source/cli/command-objects/policy.rst +++ /dev/null @@ -1,9 +0,0 @@ -====== -policy -====== - -A **policy** is an arbitrarily serialized policy engine rule set to be consumed -by a remote service. Applies to Identity v3. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: policy * diff --git a/doc/source/cli/command-objects/port.rst b/doc/source/cli/command-objects/port.rst deleted file mode 100644 index 3af5272eb2..0000000000 --- a/doc/source/cli/command-objects/port.rst +++ /dev/null @@ -1,12 +0,0 @@ -==== -port -==== - -A **port** is a connection point for attaching a single device, such as the -NIC of a server, to a network. The port also describes the associated network -configuration, such as the MAC and IP addresses to be used on that port. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: port * diff --git a/doc/source/cli/command-objects/project-cleanup.rst b/doc/source/cli/command-objects/project-cleanup.rst deleted file mode 100644 index e76e538948..0000000000 --- a/doc/source/cli/command-objects/project-cleanup.rst +++ /dev/null @@ -1,12 +0,0 @@ -=============== -project cleanup -=============== - -Clean resources associated with a specific project based on OpenStackSDK -implementation - -Block Storage v2, v3; Compute v2; Network v2; DNS v2; Orchestrate v1 - - -.. autoprogram-cliff:: openstack.common - :command: project cleanup diff --git a/doc/source/cli/command-objects/project-v2.rst b/doc/source/cli/command-objects/project-v2.rst deleted file mode 100644 index 502154d37e..0000000000 --- a/doc/source/cli/command-objects/project-v2.rst +++ /dev/null @@ -1,6 +0,0 @@ -===================== -project (Identity v2) -===================== - -.. autoprogram-cliff:: openstack.identity.v2 - :command: project * diff --git a/doc/source/cli/command-objects/project-v3.rst b/doc/source/cli/command-objects/project-v3.rst deleted file mode 100644 index 9bee6ae8cf..0000000000 --- a/doc/source/cli/command-objects/project-v3.rst +++ /dev/null @@ -1,6 +0,0 @@ -===================== -project (Identity v3) -===================== - -.. autoprogram-cliff:: openstack.identity.v3 - :command: project * diff --git a/doc/source/cli/command-objects/quota.rst b/doc/source/cli/command-objects/quota.rst deleted file mode 100644 index 59a8a9bb4e..0000000000 --- a/doc/source/cli/command-objects/quota.rst +++ /dev/null @@ -1,11 +0,0 @@ -===== -quota -===== - -Resource quotas appear in multiple APIs, OpenStackClient presents them as a -single object with multiple properties. - -Block Storage v1, v3; Compute v2; Network v2 - -.. autoprogram-cliff:: openstack.common - :command: quota * diff --git a/doc/source/cli/command-objects/region.rst b/doc/source/cli/command-objects/region.rst deleted file mode 100644 index 86e7ca8ae0..0000000000 --- a/doc/source/cli/command-objects/region.rst +++ /dev/null @@ -1,10 +0,0 @@ -====== -region -====== - -A **region** is a general division of an OpenStack deployment. You can associate -zero or more sub-regions with a region to create a tree-like structured -hierarchy. Applies to Identity v3. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: region * diff --git a/doc/source/cli/command-objects/registered-limit.rst b/doc/source/cli/command-objects/registered-limit.rst deleted file mode 100644 index 98a5efba9e..0000000000 --- a/doc/source/cli/command-objects/registered-limit.rst +++ /dev/null @@ -1,11 +0,0 @@ -================ -registered limit -================ - -Identity v3 - -Registered limits are used to define default limits for resources within a -deployment. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: registered limit * diff --git a/doc/source/cli/command-objects/request-token.rst b/doc/source/cli/command-objects/request-token.rst deleted file mode 100644 index ea333c4aba..0000000000 --- a/doc/source/cli/command-objects/request-token.rst +++ /dev/null @@ -1,10 +0,0 @@ -============= -request token -============= - -A **request token** is used by the Identity service's OS-OAUTH1 extension. It -is used by the **consumer** to request **access tokens**. Applicable to -Identity v3. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: request token * diff --git a/doc/source/cli/command-objects/role-assignment.rst b/doc/source/cli/command-objects/role-assignment.rst deleted file mode 100644 index aa618d4dd3..0000000000 --- a/doc/source/cli/command-objects/role-assignment.rst +++ /dev/null @@ -1,8 +0,0 @@ -=============== -role assignment -=============== - -Identity v2, v3 - -.. autoprogram-cliff:: openstack.identity.v3 - :command: role assignment list diff --git a/doc/source/cli/command-objects/role-v2.rst b/doc/source/cli/command-objects/role-v2.rst deleted file mode 100644 index dc5bac6c65..0000000000 --- a/doc/source/cli/command-objects/role-v2.rst +++ /dev/null @@ -1,6 +0,0 @@ -================== -role (Identity v2) -================== - -.. autoprogram-cliff:: openstack.identity.v2 - :command: role * diff --git a/doc/source/cli/command-objects/role-v3.rst b/doc/source/cli/command-objects/role-v3.rst deleted file mode 100644 index e36bef773f..0000000000 --- a/doc/source/cli/command-objects/role-v3.rst +++ /dev/null @@ -1,6 +0,0 @@ -================== -role (Identity v3) -================== - -.. autoprogram-cliff:: openstack.identity.v3 - :command: role * diff --git a/doc/source/cli/command-objects/router-ndp-proxy.rst b/doc/source/cli/command-objects/router-ndp-proxy.rst deleted file mode 100644 index 26c7172623..0000000000 --- a/doc/source/cli/command-objects/router-ndp-proxy.rst +++ /dev/null @@ -1,12 +0,0 @@ -================= -router ndp proxy -================= - -An **NDP proxy** publishes a internal IPv6 address to public network. With the -**NDP proxy**, the IPv6 address can be accessed from external. It is similar -to **Floating IP** of IPv4 in functionality. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: router ndp proxy * diff --git a/doc/source/cli/command-objects/router.rst b/doc/source/cli/command-objects/router.rst deleted file mode 100644 index 6e8e05d7c0..0000000000 --- a/doc/source/cli/command-objects/router.rst +++ /dev/null @@ -1,12 +0,0 @@ -====== -router -====== - -A **router** is a logical component that forwards data packets between -networks. It also provides Layer 3 and NAT forwarding to provide external -network access for servers on project networks. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: router * diff --git a/doc/source/cli/command-objects/security-group-rule.rst b/doc/source/cli/command-objects/security-group-rule.rst deleted file mode 100644 index 429bcf270b..0000000000 --- a/doc/source/cli/command-objects/security-group-rule.rst +++ /dev/null @@ -1,11 +0,0 @@ -=================== -security group rule -=================== - -A **security group rule** specifies the network access rules for servers -and other resources on the network. - -Compute v2, Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: security group rule * diff --git a/doc/source/cli/command-objects/security-group.rst b/doc/source/cli/command-objects/security-group.rst deleted file mode 100644 index 4edc199547..0000000000 --- a/doc/source/cli/command-objects/security-group.rst +++ /dev/null @@ -1,30 +0,0 @@ -============== -security group -============== - -A **security group** acts as a virtual firewall for servers and other -resources on a network. It is a container for security group rules -which specify the network access rules. - -Compute v2, Network v2 - -.. NOTE(efried): have to list these out one by one; 'security group *' pulls in - ... rule *. - -.. autoprogram-cliff:: openstack.network.v2 - :command: security group create - -.. autoprogram-cliff:: openstack.network.v2 - :command: security group delete - -.. autoprogram-cliff:: openstack.network.v2 - :command: security group list - -.. autoprogram-cliff:: openstack.network.v2 - :command: security group set - -.. autoprogram-cliff:: openstack.network.v2 - :command: security group show - -.. autoprogram-cliff:: openstack.network.v2 - :command: security group unset diff --git a/doc/source/cli/command-objects/server-backup.rst b/doc/source/cli/command-objects/server-backup.rst deleted file mode 100644 index f2a2e2c958..0000000000 --- a/doc/source/cli/command-objects/server-backup.rst +++ /dev/null @@ -1,11 +0,0 @@ -============= -server backup -============= - -A server backup is a disk image created in the Image store from a running server -instance. The backup command manages the number of archival copies to retain. - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server backup create diff --git a/doc/source/cli/command-objects/server-event.rst b/doc/source/cli/command-objects/server-event.rst deleted file mode 100644 index 372cb40073..0000000000 --- a/doc/source/cli/command-objects/server-event.rst +++ /dev/null @@ -1,12 +0,0 @@ -============ -server event -============ - -Server event are event record for server operations. They consist of: type -(create, delete, reboot and so on), result (success, error), start time, finish -time and so on. These are important for server maintenance. - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server event * diff --git a/doc/source/cli/command-objects/server-group.rst b/doc/source/cli/command-objects/server-group.rst deleted file mode 100644 index 741eb4adaf..0000000000 --- a/doc/source/cli/command-objects/server-group.rst +++ /dev/null @@ -1,10 +0,0 @@ -============ -server group -============ - -Server groups provide a mechanism to group servers according to certain policy. - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server group * diff --git a/doc/source/cli/command-objects/server-image.rst b/doc/source/cli/command-objects/server-image.rst deleted file mode 100644 index c013ca49bb..0000000000 --- a/doc/source/cli/command-objects/server-image.rst +++ /dev/null @@ -1,11 +0,0 @@ -============ -server image -============ - -A server image is a disk image created from a running server instance. The -image is created in the Image store. - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server image create diff --git a/doc/source/cli/command-objects/server-migration.rst b/doc/source/cli/command-objects/server-migration.rst deleted file mode 100644 index 9db58e3e46..0000000000 --- a/doc/source/cli/command-objects/server-migration.rst +++ /dev/null @@ -1,12 +0,0 @@ -================ -server migration -================ - -A server migration provides a way to move an instance from one -host to another. There are four types of migration operation -supported: live migration, cold migration, resize and evacuation. - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server migration * diff --git a/doc/source/cli/command-objects/server.rst b/doc/source/cli/command-objects/server.rst deleted file mode 100644 index cf7df1dae3..0000000000 --- a/doc/source/cli/command-objects/server.rst +++ /dev/null @@ -1,89 +0,0 @@ -====== -server -====== - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server add * - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server create - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server evacuate - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server delete - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server dump create - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server list - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server lock - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server migrate* - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server pause - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server reboot - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server rebuild - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server remove * - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server rescue - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server resize* - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server restore - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server resume - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server set - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server shelve - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server show - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server ssh - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server start - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server stop - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server suspend - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server unlock - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server unpause - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server unrescue - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server unset - -.. autoprogram-cliff:: openstack.compute.v2 - :command: server unshelve diff --git a/doc/source/cli/command-objects/service-provider.rst b/doc/source/cli/command-objects/service-provider.rst deleted file mode 100644 index 47a503278c..0000000000 --- a/doc/source/cli/command-objects/service-provider.rst +++ /dev/null @@ -1,10 +0,0 @@ -================ -service provider -================ - -A **service provider** is used by the Identity service's OS-FEDERATION -extension. It is used by to register another OpenStack Identity service. -Applicable to Identity v3. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: service provider * diff --git a/doc/source/cli/command-objects/service-v2.rst b/doc/source/cli/command-objects/service-v2.rst deleted file mode 100644 index 6b22b7eeb6..0000000000 --- a/doc/source/cli/command-objects/service-v2.rst +++ /dev/null @@ -1,6 +0,0 @@ -===================== -service (Identity v2) -===================== - -.. autoprogram-cliff:: openstack.identity.v2 - :command: service * diff --git a/doc/source/cli/command-objects/service-v3.rst b/doc/source/cli/command-objects/service-v3.rst deleted file mode 100644 index d4b702067b..0000000000 --- a/doc/source/cli/command-objects/service-v3.rst +++ /dev/null @@ -1,18 +0,0 @@ -===================== -service (Identity v3) -===================== - -.. autoprogram-cliff:: openstack.identity.v3 - :command: service create - -.. autoprogram-cliff:: openstack.identity.v3 - :command: service delete - -.. autoprogram-cliff:: openstack.identity.v3 - :command: service list - -.. autoprogram-cliff:: openstack.identity.v3 - :command: service show - -.. autoprogram-cliff:: openstack.identity.v3 - :command: service set diff --git a/doc/source/cli/command-objects/share/index.rst b/doc/source/cli/command-objects/share/index.rst new file mode 100644 index 0000000000..9f77d773c9 --- /dev/null +++ b/doc/source/cli/command-objects/share/index.rst @@ -0,0 +1,8 @@ +=========================== +Shared File System Commands +=========================== + +.. toctree:: + :maxdepth: 1 + + v2/index diff --git a/doc/source/cli/command-objects/share/v2/index.rst b/doc/source/cli/command-objects/share/v2/index.rst new file mode 100644 index 0000000000..0abf57de31 --- /dev/null +++ b/doc/source/cli/command-objects/share/v2/index.rst @@ -0,0 +1,36 @@ +================= +Share v2 Commands +================= + +share lock +---------- + +.. autoprogram-cliff:: openstack.share.v2 + :command: share lock * + + +share message +------------- + +.. autoprogram-cliff:: openstack.share.v2 + :command: share message * + + +share security service +---------------------- + +.. autoprogram-cliff:: openstack.share.v2 + :command: share security service * + + +share service +------------- + +.. autoprogram-cliff:: openstack.share.v2 + :command: share service set + +.. autoprogram-cliff:: openstack.share.v2 + :command: share service list + +.. autoprogram-cliff:: openstack.share.v2 + :command: share service ensure shares diff --git a/doc/source/cli/command-objects/subnet-pool.rst b/doc/source/cli/command-objects/subnet-pool.rst deleted file mode 100644 index ce9649dc94..0000000000 --- a/doc/source/cli/command-objects/subnet-pool.rst +++ /dev/null @@ -1,11 +0,0 @@ -=========== -subnet pool -=========== - -A **subnet pool** contains a collection of prefixes in CIDR notation -that are available for IP address allocation. - -Network v2 - -.. autoprogram-cliff:: openstack.network.v2 - :command: subnet pool * diff --git a/doc/source/cli/command-objects/subnet.rst b/doc/source/cli/command-objects/subnet.rst deleted file mode 100644 index 488fc5a2f5..0000000000 --- a/doc/source/cli/command-objects/subnet.rst +++ /dev/null @@ -1,30 +0,0 @@ -====== -subnet -====== - -A **subnet** is a block of IP addresses and associated configuration state. -Subnets are used to allocate IP addresses when new ports are created on a -network. - -Network v2 - -.. NOTE(efried): have to list these out one by one; 'subnet *' pulls in - subnet pool *. - -.. autoprogram-cliff:: openstack.network.v2 - :command: subnet create - -.. autoprogram-cliff:: openstack.network.v2 - :command: subnet delete - -.. autoprogram-cliff:: openstack.network.v2 - :command: subnet list - -.. autoprogram-cliff:: openstack.network.v2 - :command: subnet set - -.. autoprogram-cliff:: openstack.network.v2 - :command: subnet show - -.. autoprogram-cliff:: openstack.network.v2 - :command: subnet unset diff --git a/doc/source/cli/command-objects/token-v2.rst b/doc/source/cli/command-objects/token-v2.rst deleted file mode 100644 index c66302bfb2..0000000000 --- a/doc/source/cli/command-objects/token-v2.rst +++ /dev/null @@ -1,7 +0,0 @@ -=================== -token (Identity v2) -=================== - - -.. autoprogram-cliff:: openstack.identity.v2 - :command: token * diff --git a/doc/source/cli/command-objects/token-v3.rst b/doc/source/cli/command-objects/token-v3.rst deleted file mode 100644 index 6b2d87a61d..0000000000 --- a/doc/source/cli/command-objects/token-v3.rst +++ /dev/null @@ -1,7 +0,0 @@ -=================== -token (Identity v3) -=================== - - -.. autoprogram-cliff:: openstack.identity.v3 - :command: token * diff --git a/doc/source/cli/command-objects/trust.rst b/doc/source/cli/command-objects/trust.rst deleted file mode 100644 index 738c640c48..0000000000 --- a/doc/source/cli/command-objects/trust.rst +++ /dev/null @@ -1,9 +0,0 @@ -===== -trust -===== - -A **trust** provide project-specific role delegation between users, with -optional impersonation. Requires the OS-TRUST extension. Applies to Identity v3. - -.. autoprogram-cliff:: openstack.identity.v3 - :command: trust * diff --git a/doc/source/cli/command-objects/usage.rst b/doc/source/cli/command-objects/usage.rst deleted file mode 100644 index c2bcde0610..0000000000 --- a/doc/source/cli/command-objects/usage.rst +++ /dev/null @@ -1,8 +0,0 @@ -===== -usage -===== - -Compute v2 - -.. autoprogram-cliff:: openstack.compute.v2 - :command: usage * diff --git a/doc/source/cli/command-objects/user-v2.rst b/doc/source/cli/command-objects/user-v2.rst deleted file mode 100644 index 966bd37620..0000000000 --- a/doc/source/cli/command-objects/user-v2.rst +++ /dev/null @@ -1,7 +0,0 @@ -================== -user (Identity v2) -================== - - -.. autoprogram-cliff:: openstack.identity.v2 - :command: user * diff --git a/doc/source/cli/command-objects/user-v3.rst b/doc/source/cli/command-objects/user-v3.rst deleted file mode 100644 index c11ff9a645..0000000000 --- a/doc/source/cli/command-objects/user-v3.rst +++ /dev/null @@ -1,7 +0,0 @@ -================== -user (Identity v3) -================== - - -.. autoprogram-cliff:: openstack.identity.v3 - :command: user * diff --git a/doc/source/cli/command-objects/versions.rst b/doc/source/cli/command-objects/versions.rst deleted file mode 100644 index ebebec192a..0000000000 --- a/doc/source/cli/command-objects/versions.rst +++ /dev/null @@ -1,8 +0,0 @@ -======== -versions -======== - -Get a list of every version of every service in a given cloud. - -.. autoprogram-cliff:: openstack.common - :command: versions show diff --git a/doc/source/cli/command-objects/volume-attachment.rst b/doc/source/cli/command-objects/volume-attachment.rst deleted file mode 100644 index 5622444638..0000000000 --- a/doc/source/cli/command-objects/volume-attachment.rst +++ /dev/null @@ -1,8 +0,0 @@ -================= -volume attachment -================= - -Block Storage v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume attachment * diff --git a/doc/source/cli/command-objects/volume-backend.rst b/doc/source/cli/command-objects/volume-backend.rst deleted file mode 100644 index f47efd1976..0000000000 --- a/doc/source/cli/command-objects/volume-backend.rst +++ /dev/null @@ -1,8 +0,0 @@ -============== -volume backend -============== - -Block Storage v2, v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume backend * diff --git a/doc/source/cli/command-objects/volume-backup.rst b/doc/source/cli/command-objects/volume-backup.rst deleted file mode 100644 index 7b036ca0ce..0000000000 --- a/doc/source/cli/command-objects/volume-backup.rst +++ /dev/null @@ -1,9 +0,0 @@ -============= -volume backup -============= - -Block Storage v2, v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume backup * - diff --git a/doc/source/cli/command-objects/volume-group-snapshot.rst b/doc/source/cli/command-objects/volume-group-snapshot.rst deleted file mode 100644 index 02a33c1e3c..0000000000 --- a/doc/source/cli/command-objects/volume-group-snapshot.rst +++ /dev/null @@ -1,8 +0,0 @@ -===================== -volume group snapshot -===================== - -Block Storage v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume group snapshot * diff --git a/doc/source/cli/command-objects/volume-group-type.rst b/doc/source/cli/command-objects/volume-group-type.rst deleted file mode 100644 index edb88dc7b0..0000000000 --- a/doc/source/cli/command-objects/volume-group-type.rst +++ /dev/null @@ -1,8 +0,0 @@ -================= -volume group type -================= - -Block Storage v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume group type * diff --git a/doc/source/cli/command-objects/volume-group.rst b/doc/source/cli/command-objects/volume-group.rst deleted file mode 100644 index 50bc830f90..0000000000 --- a/doc/source/cli/command-objects/volume-group.rst +++ /dev/null @@ -1,23 +0,0 @@ -============ -volume group -============ - -Block Storage v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume group create - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume group delete - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume group list - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume group failover - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume group set - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume group show diff --git a/doc/source/cli/command-objects/volume-host.rst b/doc/source/cli/command-objects/volume-host.rst deleted file mode 100644 index acd0866356..0000000000 --- a/doc/source/cli/command-objects/volume-host.rst +++ /dev/null @@ -1,8 +0,0 @@ -=========== -volume host -=========== - -Block Storage v2, v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume host * diff --git a/doc/source/cli/command-objects/volume-message.rst b/doc/source/cli/command-objects/volume-message.rst deleted file mode 100644 index 5b1a8acef2..0000000000 --- a/doc/source/cli/command-objects/volume-message.rst +++ /dev/null @@ -1,8 +0,0 @@ -============== -volume message -============== - -Block Storage v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume message * diff --git a/doc/source/cli/command-objects/volume-qos.rst b/doc/source/cli/command-objects/volume-qos.rst deleted file mode 100644 index 82c4d540c7..0000000000 --- a/doc/source/cli/command-objects/volume-qos.rst +++ /dev/null @@ -1,8 +0,0 @@ -========== -volume qos -========== - -Block Storage v2, v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume qos * diff --git a/doc/source/cli/command-objects/volume-service.rst b/doc/source/cli/command-objects/volume-service.rst deleted file mode 100644 index 43d455ff5a..0000000000 --- a/doc/source/cli/command-objects/volume-service.rst +++ /dev/null @@ -1,8 +0,0 @@ -============== -volume service -============== - -Block Storage v2, v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume service * diff --git a/doc/source/cli/command-objects/volume-snapshot.rst b/doc/source/cli/command-objects/volume-snapshot.rst deleted file mode 100644 index e63e436dff..0000000000 --- a/doc/source/cli/command-objects/volume-snapshot.rst +++ /dev/null @@ -1,8 +0,0 @@ -=============== -volume snapshot -=============== - -Block Storage v2, v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume snapshot * diff --git a/doc/source/cli/command-objects/volume-transfer-request.rst b/doc/source/cli/command-objects/volume-transfer-request.rst deleted file mode 100644 index 97dac02a0a..0000000000 --- a/doc/source/cli/command-objects/volume-transfer-request.rst +++ /dev/null @@ -1,8 +0,0 @@ -======================= -volume transfer request -======================= - -Block Storage v2, v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume transfer request * diff --git a/doc/source/cli/command-objects/volume-type.rst b/doc/source/cli/command-objects/volume-type.rst deleted file mode 100644 index 003ee67306..0000000000 --- a/doc/source/cli/command-objects/volume-type.rst +++ /dev/null @@ -1,8 +0,0 @@ -=========== -volume type -=========== - -Block Storage v2, v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume type * diff --git a/doc/source/cli/command-objects/volume.rst b/doc/source/cli/command-objects/volume.rst deleted file mode 100644 index 337bb9fa2b..0000000000 --- a/doc/source/cli/command-objects/volume.rst +++ /dev/null @@ -1,34 +0,0 @@ -====== -volume -====== - -Block Storage v2, v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume create - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume delete - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume list - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume migrate - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume set - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume show - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume unset - -Block Storage v3 - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume summary - -.. autoprogram-cliff:: openstack.volume.v3 - :command: volume revert diff --git a/doc/source/cli/command-objects/volume/index.rst b/doc/source/cli/command-objects/volume/index.rst new file mode 100644 index 0000000000..9a343b96b1 --- /dev/null +++ b/doc/source/cli/command-objects/volume/index.rst @@ -0,0 +1,9 @@ +====================== +Block Storage Commands +====================== + +.. toctree:: + :maxdepth: 1 + + v2/index + v3/index diff --git a/doc/source/cli/command-objects/volume/v2/index.rst b/doc/source/cli/command-objects/volume/v2/index.rst new file mode 100644 index 0000000000..b69619c2f9 --- /dev/null +++ b/doc/source/cli/command-objects/volume/v2/index.rst @@ -0,0 +1,146 @@ +========================= +Block Storage v2 Commands +========================= + + +consistency group +----------------- + +.. autoprogram-cliff:: openstack.volume.v2 + :command: consistency group add volume + +.. autoprogram-cliff:: openstack.volume.v2 + :command: consistency group create + +.. autoprogram-cliff:: openstack.volume.v2 + :command: consistency group delete + +.. autoprogram-cliff:: openstack.volume.v2 + :command: consistency group list + +.. autoprogram-cliff:: openstack.volume.v2 + :command: consistency group remove volume + +.. autoprogram-cliff:: openstack.volume.v2 + :command: consistency group set + +.. autoprogram-cliff:: openstack.volume.v2 + :command: consistency group show + + +consistency group snapshot +-------------------------- + +.. autoprogram-cliff:: openstack.volume.v2 + :command: consistency group snapshot create + +.. autoprogram-cliff:: openstack.volume.v2 + :command: consistency group snapshot delete + +.. autoprogram-cliff:: openstack.volume.v2 + :command: consistency group snapshot list + +.. autoprogram-cliff:: openstack.volume.v2 + :command: consistency group snapshot show + + +volume backend +-------------- + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume backend * + + +volume backup +------------- + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume backup create + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume backup delete + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume backup list + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume backup restore + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume backup set + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume backup show + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume backup record export + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume backup record import + + +volume host +----------- + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume host * + + +volume qos +---------- + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume qos * + + +volume service +-------------- + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume service * + + +volume snapshot +--------------- + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume snapshot * + + +volume transfer request +----------------------- + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume transfer request * + + +volume type +----------- + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume type * + + +volume +------ + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume create + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume delete + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume list + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume migrate + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume set + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume show + +.. autoprogram-cliff:: openstack.volume.v2 + :command: volume unset diff --git a/doc/source/cli/command-objects/volume/v3/index.rst b/doc/source/cli/command-objects/volume/v3/index.rst new file mode 100644 index 0000000000..4c48f8c238 --- /dev/null +++ b/doc/source/cli/command-objects/volume/v3/index.rst @@ -0,0 +1,220 @@ +========================= +Block Storage v2 Commands +========================= + + +block storage +------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: block storage cleanup + + +block storage cluster +--------------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: block storage cluster * + + +Block Storage Log Level +----------------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: block storage log level * + + +Block Storage Manage +-------------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: block storage volume manageable list + +.. autoprogram-cliff:: openstack.volume.v3 + :command: block storage snapshot manageable list + + +block storage resource filter +----------------------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: block storage resource filter * + + +consistency group snapshot +-------------------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: consistency group snapshot create + +.. autoprogram-cliff:: openstack.volume.v3 + :command: consistency group snapshot delete + +.. autoprogram-cliff:: openstack.volume.v3 + :command: consistency group snapshot list + +.. autoprogram-cliff:: openstack.volume.v3 + :command: consistency group snapshot show + + +consistency group +----------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: consistency group add volume + +.. autoprogram-cliff:: openstack.volume.v3 + :command: consistency group create + +.. autoprogram-cliff:: openstack.volume.v3 + :command: consistency group delete + +.. autoprogram-cliff:: openstack.volume.v3 + :command: consistency group list + +.. autoprogram-cliff:: openstack.volume.v3 + :command: consistency group remove volume + +.. autoprogram-cliff:: openstack.volume.v3 + :command: consistency group set + +.. autoprogram-cliff:: openstack.volume.v3 + :command: consistency group show + + +volume attachment +----------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume attachment * + + +volume backend +-------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume backend * + + +volume backup +------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume backup * + + +volume group snapshot +--------------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group snapshot * + + +volume group type +----------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group type * + + +volume group +------------ + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group create + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group delete + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group list + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group failover + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group set + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume group show + + +volume host +----------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume host * + + +volume message +-------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume message * + + +volume qos +---------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume qos * + + +volume service +-------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume service * + + +volume snapshot +--------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume snapshot * + + +volume transfer request +----------------------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume transfer request * + + +volume type +----------- + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume type * + + +volume +------ + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume create + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume delete + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume list + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume migrate + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume set + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume show + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume unset + + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume summary + +.. autoprogram-cliff:: openstack.volume.v3 + :command: volume revert diff --git a/doc/source/cli/commands.rst b/doc/source/cli/commands.rst index 653e2a50bd..c7d60951d8 100644 --- a/doc/source/cli/commands.rst +++ b/doc/source/cli/commands.rst @@ -99,7 +99,6 @@ referring to both Compute and Volume quotas. * ``flavor``: (**Compute**) predefined server configurations: ram, root disk and so on * ``fixed ip``: (**Compute**) - an internal IP address assigned to a server * ``floating ip``: (**Network**) - a public IP address that can be mapped to a server -* ``floating ip pool``: (**Network**) - a pool of public IP addresses * ``group``: (**Identity**) a grouping of users * ``host``: (**Compute**) - the physical computer running compute services * ``hypervisor``: (**Compute**) the virtual machine manager diff --git a/doc/source/cli/data/glance.csv b/doc/source/cli/data/glance.csv index c248904fe1..b380886d6f 100644 --- a/doc/source/cli/data/glance.csv +++ b/doc/source/cli/data/glance.csv @@ -24,13 +24,13 @@ location-delete,,Remove locations (and related metadata) from an image. location-update,,Update metadata of an image's location. md-namespace-create,image metadef namespace create,Create a new metadata definitions namespace. md-namespace-delete,image metadef namespace delete,Delete specified metadata definitions namespace with its contents. -md-namespace-import,,Import a metadata definitions namespace from file or standard input. +md-namespace-import,WONTFIX,Import a metadata definitions namespace from file or standard input. md-namespace-list,image metadef namespace list,List metadata definitions namespaces. -md-namespace-objects-delete,,Delete all metadata definitions objects inside a specific namespace. -md-namespace-properties-delete,,Delete all metadata definitions property inside a specific namespace. +md-namespace-objects-delete,image metadef object delete,Delete all metadata definitions objects inside a specific namespace. +md-namespace-properties-delete,image metadef property delete,Delete all metadata definitions property inside a specific namespace. md-namespace-resource-type-list,image metadef resource type association list,List resource types associated to specific namespace. md-namespace-show,image metadef namespace show,Describe a specific metadata definitions namespace. -md-namespace-tags-delete,,Delete all metadata definitions tags inside a specific namespace. +md-namespace-tags-delete,image metadef namespace unset --all,Delete all metadata definitions tags inside a specific namespace. md-namespace-update,,Update an existing metadata definitions namespace. md-object-create,image metadef object create,Create a new metadata definitions object inside a namespace. md-object-show,image metadef object show,Describe a specific metadata definitions object inside a namespace. @@ -46,12 +46,12 @@ md-property-update,image metadef property set,Update metadata definitions proper md-resource-type-associate,image metadef resource type association create,Associate resource type with a metadata definitions namespace. md-resource-type-deassociate,image metadef resource type association delete,Deassociate resource type with a metadata definitions namespace. md-resource-type-list,image metadef resource type list,List available resource type names. -md-tag-create,,Add a new metadata definitions tag inside a namespace. -md-tag-create-multiple,,Create new metadata definitions tags inside a namespace. -md-tag-delete,,Delete a specific metadata definitions tag inside a namespace. -md-tag-list,,List metadata definitions tags inside a specific namespace. -md-tag-show,,Describe a specific metadata definitions tag inside a namespace. -md-tag-update,,Rename a metadata definitions tag inside a namespace. +md-tag-create,image metadef namespace set --tag,Add a new metadata definitions tag inside a namespace. +md-tag-create-multiple,image metadef namespace set --tag tag1 --tag tag2,Create new metadata definitions tags inside a namespace. +md-tag-delete,image metadef namespace unset --tag,Delete a specific metadata definitions tag inside a namespace. +md-tag-list,image metadef namespace show,List metadata definitions tags inside a specific namespace. +md-tag-show,image metadef namespace show,Describe a specific metadata definitions tag inside a namespace. +md-tag-update,image metadef namespace set --tag,Rename a metadata definitions tag inside a namespace. member-create,image add project,Create member for a given image. member-delete,image remove project,Delete image member. member-get,image member get,Show details of an image member diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst index 9cb3b33a73..17e50a07d2 100644 --- a/doc/source/cli/index.rst +++ b/doc/source/cli/index.rst @@ -13,12 +13,3 @@ interactive decoder backwards-incompatible - -.. NOTE(efried): Everything must be in a toctree but we don't want these to - show up to the reader. - -.. toctree:: - :glob: - :hidden: - - _hidden/* diff --git a/doc/source/contributor/command-errors.rst b/doc/source/contributor/command-errors.rst index f47dfec7c3..81bd8fa447 100644 --- a/doc/source/contributor/command-errors.rst +++ b/doc/source/contributor/command-errors.rst @@ -172,10 +172,11 @@ multiple ``delete_network()`` calls. .. code-block:: python - class DeleteNetwork(common.NetworkAndComputeCommand): + class DeleteNetwork(common.NetworkDelete): """Delete network(s)""" - def update_parser_common(self, parser): + def get_parser(self, prog_name): + parser = super().get_parser(prog_name) parser.add_argument( 'network', metavar="", @@ -184,20 +185,7 @@ multiple ``delete_network()`` calls. ) return parser - def take_action(self, client, parsed_args): - ret = 0 - - for network in parsed_args.network: - try: - obj = client.find_network(network, ignore_missing=False) - client.delete_network(obj) - except Exception: - LOG.error(_("Failed to delete network with name " - "or ID %s."), network) - ret += 1 - - if ret > 0: - total = len(parsed_args.network) - msg = (_("Failed to delete %(ret)s of %(total)s networks.") - % {"ret": ret, "total": total}) - raise exceptions.CommandError(msg) + def take_action_delete(self, parsed_args): + client = self.app.client_manager.network + obj = client.find_network(network, ignore_missing=False) + client.delete_network(obj) diff --git a/doc/source/contributor/index.rst b/doc/source/contributor/index.rst index 445aac92d1..199296db99 100644 --- a/doc/source/contributor/index.rst +++ b/doc/source/contributor/index.rst @@ -12,5 +12,6 @@ command-errors command-logs plugins + osc-plugin-migration humaninterfaceguide api/modules diff --git a/doc/source/contributor/osc-plugin-migration.rst b/doc/source/contributor/osc-plugin-migration.rst new file mode 100644 index 0000000000..4cdeb55d3f --- /dev/null +++ b/doc/source/contributor/osc-plugin-migration.rst @@ -0,0 +1,94 @@ +.. _neutron-cli-migration: + +=========================================== +Migrating Project Client OSC Plugins to OSC +=========================================== + +The guide documents the process for migrating project client OSC plugins from +the project client into ``python-openstackclient``. It focuses on neutron client +and clients for special networking services that were previously implemented as +external OSC plugins (e.g., BGP VPN, TaaS, etc.), but it should apply to any +project client's OSC plugin. + +Background +========== + +Historically, advanced Neutron services provided their CLI commands through +``python-neutronclient`` as OSC plugins. As the old python-xclient libraries +were deprecated and their functionality were moved to python-openstackclient +it was also decided to move these plugins to python-openstackclient, see the +2025 October PTG etherpad: +https://etherpad.opendev.org/p/oct2025-ptg-neutron#L156 + +These migrated commands become part of the core OSC codebase but are +organized in separate subdirectories under ``openstackclient/network/v2/`` +to maintain clear separation and ownership. + +Migration Steps +=============== + +1. Create the Command Module Directory +--------------------------------------- + +Create a new python module under ``openstackclient/network/v2/`` for your +service with appropriate name, for example for tap-as-a-service ``taas``. + +2. Migrate the Command Classes +------------------------------- + +Copy or migrate your command implementation files from the neutronclient +plugin to the new directory. Each command should inherit from the appropriate +base class (i.e.: ``command.Command``, ``command.Lister``, or +``command.ShowOne``). + +3. Register Entry Points in pyproject.toml +------------------------------------------- + +Add entry points for your commands in ``pyproject.toml`` under a dedicated +group. The group name should be the same as the module name under +``network/v2``. + +Example for BGP VPN: + +.. code-block:: toml + + [project.entry-points."openstack.network.v2.bgpvpn"] + bgpvpn_create = "openstackclient.network.v2.bgpvpn.bgpvpn:CreateBgpvpn" + bgpvpn_delete = "openstackclient.network.v2.bgpvpn.bgpvpn:DeleteBgpvpn" + bgpvpn_list = "openstackclient.network.v2.bgpvpn.bgpvpn:ListBgpvpn" + bgpvpn_show = "openstackclient.network.v2.bgpvpn.bgpvpn:ShowBgpvpn" + bgpvpn_set = "openstackclient.network.v2.bgpvpn.bgpvpn:SetBgpvpn" + bgpvpn_unset = "openstackclient.network.v2.bgpvpn.bgpvpn:UnsetBgpvpn" + +4. Register the Service in API_EXTENSIONS +------------------------------------------ + +Add your service name to the ``API_EXTENSIONS`` tuple in +``openstackclient/network/client.py``: + +.. code-block:: python + + API_EXTENSIONS = ('taas', 'bgpvpn', '') + +This tells OSC to load the entry points from your dedicated group. + +5. Ignore the Old Neutronclient Plugin +--------------------------------------- + +To prevent conflicts with the old neutronclient plugin (if users still have +it installed), add the old plugin module to the ``IGNORED_MODULES`` tuple in +``openstackclient/shell.py``: + +.. code-block:: python + + IGNORED_MODULES = ( + 'neutron_taas.taas_client.osc', + 'neutronclient.osc.v2.taas', + 'neutronclient.osc.v2.networking_bgpvpn', + 'neutronclient.osc.v2.', + ) + +6. Add Unit Tests +----------------- + +Create unit tests under ``openstackclient/tests/unit/network/v2//``. diff --git a/doc/test/redirect-tests.txt b/doc/test/redirect-tests.txt index 5ed7c19cb5..0525abcaf8 100644 --- a/doc/test/redirect-tests.txt +++ b/doc/test/redirect-tests.txt @@ -24,3 +24,125 @@ /python-openstackclient/latest/cli/command-objects/service.html 301 /python-openstackclient/latest/cli/_hidden/service.html /python-openstackclient/latest/cli/command-objects/token.html 301 /python-openstackclient/latest/cli/_hidden/token.html /python-openstackclient/latest/cli/command-objects/user.html 301 /python-openstackclient/latest/cli/_hidden/user.html +/python-openstackclient/latest/cli/command-objects/availability-zone.html 301 /python-openstackclient/latest/cli/command-objects/common/index.html +/python-openstackclient/latest/cli/command-objects/command.html 301 /python-openstackclient/latest/cli/command-objects/common/index.html +/python-openstackclient/latest/cli/command-objects/complete.html 301 /python-openstackclient/latest/cli/command-objects/common/index.html +/python-openstackclient/latest/cli/command-objects/configuration.html 301 /python-openstackclient/latest/cli/command-objects/common/index.html +/python-openstackclient/latest/cli/command-objects/extension.html 301 /python-openstackclient/latest/cli/command-objects/common/index.html +/python-openstackclient/latest/cli/command-objects/limits.html 301 /python-openstackclient/latest/cli/command-objects/common/index.html +/python-openstackclient/latest/cli/command-objects/module.html 301 /python-openstackclient/latest/cli/command-objects/common/index.html +/python-openstackclient/latest/cli/command-objects/project-cleanup.html 301 /python-openstackclient/latest/cli/command-objects/common/index.html +/python-openstackclient/latest/cli/command-objects/quota.html 301 /python-openstackclient/latest/cli/command-objects/common/index.html +/python-openstackclient/latest/cli/command-objects/versions.html 301 /python-openstackclient/latest/cli/command-objects/common/index.html +/python-openstackclient/latest/cli/command-objects/aggregate.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/compute-agent.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/compute-service.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/console-connection.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/console-log.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/console-url.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/flavor.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/host.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/hypervisor-stats.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/hypervisor.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/keypair.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/server-backup.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/server-event.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/server-group.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/server-image.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/server-migration.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/server.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/usage.html 301 /python-openstackclient/latest/cli/command-objects/compute/v2/index.html +/python-openstackclient/latest/cli/command-objects/ec2-credentials-v2.html 301 /python-openstackclient/latest/cli/command-objects/identity/v2/index.html +/python-openstackclient/latest/cli/command-objects/endpoint-v2.html 301 /python-openstackclient/latest/cli/command-objects/identity/v2/index.html +/python-openstackclient/latest/cli/command-objects/project-v2.html 301 /python-openstackclient/latest/cli/command-objects/identity/v2/index.html +/python-openstackclient/latest/cli/command-objects/role-v2.html 301 /python-openstackclient/latest/cli/command-objects/identity/v2/index.html +/python-openstackclient/latest/cli/command-objects/service-v2.html 301 /python-openstackclient/latest/cli/command-objects/identity/v2/index.html +/python-openstackclient/latest/cli/command-objects/token-v2.html 301 /python-openstackclient/latest/cli/command-objects/identity/v2/index.html +/python-openstackclient/latest/cli/command-objects/user-v2.html 301 /python-openstackclient/latest/cli/command-objects/identity/v2/index.html +/python-openstackclient/latest/cli/command-objects/access-rules.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/access-token.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/application-credentials.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/catalog.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/consumer.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/credential.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/domain.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/ec2-credentials-v3.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/endpoint-v3.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/endpoint_group.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/federation-domain-project.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/federation-protocol.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/group.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/identity-provider.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/implied_role.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/limit.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/mapping.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/policy.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/project-v3.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/region.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/registered-limit.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/request-token.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/role-assignment.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/role-v3.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/service-provider.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/service-v3.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/token-v3.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/trust.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/user-v3.html 301 /python-openstackclient/latest/cli/command-objects/identity/v3/index.html +/python-openstackclient/latest/cli/command-objects/image-member.html 301 /python-openstackclient/latest/cli/command-objects/image/v2/index.html +/python-openstackclient/latest/cli/command-objects/image-task.html 301 /python-openstackclient/latest/cli/command-objects/image/v2/index.html +/python-openstackclient/latest/cli/command-objects/image-v2.html 301 /python-openstackclient/latest/cli/command-objects/image/v2/index.html +/python-openstackclient/latest/cli/command-objects/address-group.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/address-scope.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/default-security-group-rule.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/floating-ip-port-forwarding.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/floating-ip.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/ip-availability.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/local-ip-association.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/local-ip.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-agent.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-auto-allocated-topology.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-flavor-profile.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-flavor.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-l3-conntrack-helper.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-meter-rule.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-meter.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-qos-policy.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-qos-rule-type.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-qos-rule.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-rbac.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-segment.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-service-provider.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network-trunk.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/network_segment_range.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/port.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/router-ndp-proxy.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/router.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/security-group-rule.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/security-group.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/subnet-pool.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/subnet.html 301 /python-openstackclient/latest/cli/command-objects/network/v2/index.html +/python-openstackclient/latest/cli/command-objects/container.html 301 /python-openstackclient/latest/cli/command-objects/object-store/v1/index.html +/python-openstackclient/latest/cli/command-objects/object-store-account.html 301 /python-openstackclient/latest/cli/command-objects/object-store/v1/index.html +/python-openstackclient/latest/cli/command-objects/object.html 301 /python-openstackclient/latest/cli/command-objects/object-store/v1/index.html +/python-openstackclient/latest/cli/command-objects/block-storage-cleanup.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/block-storage-cluster.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/block-storage-log-level.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/block-storage-manage.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/block-storage-resource-filter.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/consistency-group-snapshot.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/consistency-group.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-attachment.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-backend.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-backup.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-group-snapshot.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-group-type.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-group.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-host.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-message.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-qos.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-service.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-snapshot.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-transfer-request.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume-type.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html +/python-openstackclient/latest/cli/command-objects/volume.html 301 /python-openstackclient/latest/cli/command-objects/volume/v3/index.html diff --git a/examples/common.py b/examples/common.py index 650139ec27..8213f4b532 100755 --- a/examples/common.py +++ b/examples/common.py @@ -264,7 +264,7 @@ def main(opts, run): if dump_stack_trace: _logger.error(traceback.format_exc(e)) else: - _logger.error('Exception raised: ' + str(e)) + _logger.error('Exception raised: %s', e) return 1 diff --git a/hacking/checks.py b/hacking/checks.py new file mode 100644 index 0000000000..80807df87b --- /dev/null +++ b/hacking/checks.py @@ -0,0 +1,236 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ast +import os +import re + +from hacking import core + +""" +Guidelines for writing new hacking checks + + - Use only for python-openstackclient specific tests. OpenStack general tests + should be submitted to the common 'hacking' module. + - Pick numbers in the range O4xx. Find the current test with the highest + allocated number and then pick the next value. +""" + + +@core.flake8ext +def assert_no_oslo(logical_line): + """Check for use of oslo libraries. + + Okay: import os + Okay: from os import path + O400: import oslo_messaging + O400: from oslo_log import log + """ + if match := re.match(r'(from|import) (oslo_.*)', logical_line): + if match.group(2) == 'oslo_i18n': + return + yield (0, "O400: oslo libraries should not be used in SDK projects") + + +@core.flake8ext +def assert_no_duplicated_setup(logical_line, filename): + """Check for use of various unnecessary test duplications. + + This check only applies to files under openstackclient/tests/unit/. + + Okay: self.app = fakes.FakeShell() + Okay: self.app.client_manager.auth_ref = mock.Mock() + O401: self.app = Namespace(self.app, self.namespace) + O401: self.network_client = self.app.client_manager.network + O401: self.app.client_manager.network = mock.Mock() + """ + if os.path.join('openstackclient', 'tests', 'unit') not in filename: + return + + if re.match(r'self.app = .*\(self.app, self.namespace\)', logical_line): + yield ( + 0, + 'O401: It is not necessary to create dummy Namespace objects', + ) + + if os.path.basename(filename) != 'fakes.py': + if re.match( + r'self.[a-zA-Z0-9_]+ = self.app.client_manager.*', logical_line + ): + yield ( + 0, + "O401: Aliases for mocks of the service client are already " + "provided by the respective service's FakeClientMixin class", + ) + + if match := re.match( + r'self.app.client_manager.([a-z_]+) = mock.Mock', logical_line + ): + service = match.group(1) + if service == 'auth_ref': + return + yield ( + 0, + f"O401: client_manager.{service} mocks are already provided " + f"by the {service} service's FakeClientMixin class", + ) + + +@core.flake8ext +def assert_use_of_client_aliases(logical_line, filename): + """Ensure we use $service_client instead of $sdk_connection.service. + + Okay: self.compute_client.find_server(foo) + O402: self.app.client_manager.sdk_connnection.compute.find_server(foo) + + The following checks only apply to files under openstackclient/tests/unit/: + + O402: self.app.client_manager.compute.find_server.return_value = server + O402: self.app.client_manager.compute.find_server = mock.Mock() + O402: self.compute_client.find_server = mock.Mock() + """ + # we should expand the list of services as we drop legacy clients + if match := re.match( + r'self\.app\.client_manager\.sdk_connnection\.(compute|network|image)', + logical_line, + ): + service = match.group(1) + yield (0, f"O402: prefer {service}_client to sdk_connection.{service}") + + # everything from here down only affects unit tests + if os.path.join('openstackclient', 'tests', 'unit') not in filename: + return + + if match := re.match( + r'(self\.app\.client_manager\.(compute|network|image)+\.[a-z_]+)\.(return_value|side_effect) = ', # noqa: E501 + logical_line, + ): + service = match.group(1) + yield ( + 0, + f"O402: prefer {service}_client to " + f"self.app.client_manager.{service}", + ) + + if match := re.match( + r'(self\.app\.client_manager\.(compute|network|image)+\.[a-z_]+) = mock.Mock', # noqa: E501 + logical_line, + ): + yield ( + 0, + f"O402: {match.group(1)} is already a mock: there's no need to " + f"assign a new mock.Mock instance.", + ) + + if match := re.match( + r'(self\.(compute|network|image)_client\.[a-z_]+) = mock.Mock', + logical_line, + ): + yield ( + 0, + f"O402: {match.group(1)} is already a mock: there's no need to " + f"assign a new mock.Mock instance.", + ) + + +class SDKProxyFindChecker(ast.NodeVisitor): + """NodeVisitor to find ``*_client.find_*`` statements.""" + + def __init__(self): + self.error = False + + def visit_Call(self, node): + # No need to keep visiting the AST if we already found something. + if self.error: + return + + self.generic_visit(node) + + if not ( + isinstance(node.func, ast.Attribute) + and node.func.attr.startswith('find_') # and + # isinstance(node.func.value, ast.Attribute) and + # node.func.value.attr.endswith('_client') + ): + # print(f'skipping: got {node.func}') + return + + if not ( + ( + # handle calls like 'identity_client.find_project' + isinstance(node.func.value, ast.Name) + and node.func.value.id.endswith('client') + ) + or ( + # handle calls like 'self.compute_client.find_server' + isinstance(node.func.value, ast.Attribute) + and node.func.value.attr.endswith('_client') + ) + or ( + # handle calls like 'self.app.client_manager.image.find_image' + isinstance(node.func.value, ast.Attribute) + and node.func.value.attr + in ('identity', 'network', 'image', 'compute') + ) + ): + return + + if not any(kw.arg == 'ignore_missing' for kw in node.keywords): + self.error = True + + +@core.flake8ext +def assert_find_ignore_missing_kwargs(logical_line, filename): + """Ensure ignore_missing is always used for ``find_*`` SDK proxy calls. + + Okay: self.compute_client.find_server(foo, ignore_missing=True) + Okay: self.image_client.find_server(foo, ignore_missing=False) + Okay: self.volume_client.volumes.find(name='foo') + O403: self.network_client.find_network(parsed_args.network) + O403: self.compute_client.find_flavor(flavor_id, get_extra_specs=True) + """ + if 'tests' in filename: + return + + checker = SDKProxyFindChecker() + try: + parsed_logical_line = ast.parse(logical_line) + except SyntaxError: + # let flake8 catch this itself + # https://github.com/PyCQA/flake8/issues/1948 + return + checker.visit(parsed_logical_line) + if checker.error: + yield ( + 0, + 'O403: Calls to find_* proxy methods must explicitly set ' + 'ignore_missing', + ) + + +@core.flake8ext +def assert_use_of_osc_command(logical_line, filename): + """Ensure we use openstackclient.command instead of osc_lib.command. + + Okay: from openstackclient.command import command + Okay: import openstackclient.command + O404: from osc_lib.command import command + """ + if filename == 'openstackclient/command.py': + return + + if re.match(r'^from osc_lib\.command import command$', logical_line): + yield ( + 0, + 'O404: Import Command classes from openstackclient.command, not ' + 'osc_lib.command.command', + ) diff --git a/openstackclient/__init__.py b/openstackclient/__init__.py index e6e7f7c031..2f0b0cd3a6 100644 --- a/openstackclient/__init__.py +++ b/openstackclient/__init__.py @@ -11,12 +11,8 @@ # under the License. # -import pbr.version +import importlib.metadata __all__ = ['__version__'] -version_info = pbr.version.VersionInfo('python-openstackclient') -try: - __version__ = version_info.version_string() -except AttributeError: - __version__ = None +__version__ = importlib.metadata.version('python-openstackclient') diff --git a/openstackclient/api/api.py b/openstackclient/api/api.py index 5f78b0ee54..651f9a7cad 100644 --- a/openstackclient/api/api.py +++ b/openstackclient/api/api.py @@ -13,6 +13,8 @@ """Base API Library""" +from typing import Any + from keystoneauth1 import exceptions as ks_exceptions from keystoneauth1 import session as ks_session from osc_lib import exceptions @@ -30,7 +32,9 @@ class KeystoneSession: """ - def __init__(self, session=None, endpoint=None, **kwargs): + def __init__( + self, session: Any = None, endpoint: str | None = None, **kwargs: Any + ) -> None: """Base object that contains some common API objects and methods :param Session session: @@ -46,7 +50,9 @@ def __init__(self, session=None, endpoint=None, **kwargs): self.session = session self.endpoint = endpoint - def _request(self, method, url, session=None, **kwargs): + def _request( + self, method: str, url: str, session: Any = None, **kwargs: Any + ) -> Any: """Perform call into session All API calls are funneled through this method to provide a common @@ -82,8 +88,12 @@ class BaseAPI(KeystoneSession): """Base API""" def __init__( - self, session=None, service_type=None, endpoint=None, **kwargs - ): + self, + session: Any = None, + service_type: str | None = None, + endpoint: str | None = None, + **kwargs: Any, + ) -> None: """Base object that contains some common API objects and methods :param Session session: @@ -101,7 +111,13 @@ def __init__( # The basic action methods all take a Session and return dict/lists - def create(self, url, session=None, method=None, **params): + def create( + self, + url: str, + session: Any = None, + method: str | None = None, + **params: Any, + ) -> Any: """Create a new resource :param string url: @@ -121,7 +137,7 @@ def create(self, url, session=None, method=None, **params): except requests.JSONDecodeError: return ret - def delete(self, url, session=None, **params): + def delete(self, url: str, session: Any = None, **params: Any) -> Any: """Delete a resource :param string url: @@ -132,7 +148,14 @@ def delete(self, url, session=None, **params): return self._request('DELETE', url, **params) - def list(self, path, session=None, body=None, detailed=False, **params): + def list( + self, + path: str, + session: Any = None, + body: Any = None, + detailed: bool = False, + **params: Any, + ) -> Any: """Return a list of resources GET ${ENDPOINT}/${PATH}?${PARAMS} @@ -177,11 +200,11 @@ def list(self, path, session=None, body=None, detailed=False, **params): def find_attr( self, - path, - value=None, - attr=None, - resource=None, - ): + path: str, + value: str | None = None, + attr: str | None = None, + resource: str | None = None, + ) -> Any: """Find a resource via attribute or ID Most APIs return a list wrapped by a dict with the resource @@ -211,7 +234,7 @@ def find_attr( if resource is None: resource = path - def getlist(kw): + def getlist(kw: dict[str, Any]) -> Any: """Do list call, unwrap resource dict if present""" ret = self.list(path, **kw) if isinstance(ret, dict) and resource in ret: @@ -219,7 +242,7 @@ def getlist(kw): return ret # Search by attribute - kwargs = {attr: value} + kwargs: dict[str, Any] = {attr: value} data = getlist(kwargs) if isinstance(data, dict): return data @@ -241,7 +264,7 @@ def getlist(kw): msg % {'resource': resource, 'attr': attr, 'value': value} ) - def find_bulk(self, path, **kwargs): + def find_bulk(self, path: str, **kwargs: Any) -> Any: """Bulk load and filter locally :param string path: @@ -254,7 +277,7 @@ def find_bulk(self, path, **kwargs): items = self.list(path) if isinstance(items, dict): # strip off the enclosing dict - key = list(items.keys())[0] + key = next(iter(items.keys())) items = items[key] ret = [] @@ -267,7 +290,7 @@ def find_bulk(self, path, **kwargs): return ret - def find_one(self, path, **kwargs): + def find_one(self, path: str, **kwargs: Any) -> Any: """Find a resource by name or ID :param string path: @@ -286,12 +309,7 @@ def find_one(self, path, **kwargs): raise RuntimeError(msg) return bulk_list[0] - def find( - self, - path, - value=None, - attr=None, - ): + def find(self, path: str, value: str, attr: str) -> Any: """Find a single resource by name or ID :param string path: diff --git a/openstackclient/api/compute_v2.py b/openstackclient/api/compute_v2.py index 95521eae9a..ce617b8ba4 100644 --- a/openstackclient/api/compute_v2.py +++ b/openstackclient/api/compute_v2.py @@ -17,6 +17,7 @@ """ import http +from typing import Any from openstack import exceptions as sdk_exceptions from osc_lib import exceptions @@ -25,7 +26,11 @@ # security groups -def create_security_group(compute_client, name=None, description=None): +def create_security_group( + compute_client: Any, + name: str | None = None, + description: str | None = None, +) -> Any: """Create a new security group https://docs.openstack.org/api-ref/compute/#create-security-group @@ -46,7 +51,9 @@ def create_security_group(compute_client, name=None, description=None): return response.json()['security_group'] -def list_security_groups(compute_client, all_projects=None): +def list_security_groups( + compute_client: Any, all_projects: bool | None = None +) -> Any: """Get all security groups https://docs.openstack.org/api-ref/compute/#list-security-groups @@ -63,8 +70,8 @@ def list_security_groups(compute_client, all_projects=None): return response.json()['security_groups'] -def find_security_group(compute_client, name_or_id): - """Find the name for a given security group name or ID +def find_security_group(compute_client: Any, name_or_id: str) -> Any: + """Find the security group for a given name or ID https://docs.openstack.org/api-ref/compute/#show-security-group-details @@ -101,8 +108,11 @@ def find_security_group(compute_client, name_or_id): def update_security_group( - compute_client, security_group_id, name=None, description=None -): + compute_client: Any, + security_group_id: str, + name: str | None = None, + description: str | None = None, +) -> Any: """Update an existing security group https://docs.openstack.org/api-ref/compute/#update-security-group @@ -127,7 +137,9 @@ def update_security_group( return response.json()['security_group'] -def delete_security_group(compute_client, security_group_id=None): +def delete_security_group( + compute_client: Any, security_group_id: str | None = None +) -> None: """Delete a security group https://docs.openstack.org/api-ref/compute/#delete-security-group @@ -146,14 +158,14 @@ def delete_security_group(compute_client, security_group_id=None): def create_security_group_rule( - compute_client, - security_group_id=None, - ip_protocol=None, - from_port=None, - to_port=None, - remote_ip=None, - remote_group=None, -): + compute_client: Any, + security_group_id: str | None = None, + ip_protocol: str | None = None, + from_port: int | None = None, + to_port: int | None = None, + remote_ip: str | None = None, + remote_group: str | None = None, +) -> Any: """Create a new security group rule https://docs.openstack.org/api-ref/compute/#create-security-group-rule @@ -182,7 +194,9 @@ def create_security_group_rule( return response.json()['security_group_rule'] -def delete_security_group_rule(compute_client, security_group_rule_id=None): +def delete_security_group_rule( + compute_client: Any, security_group_rule_id: str | None = None +) -> None: """Delete a security group rule https://docs.openstack.org/api-ref/compute/#delete-security-group-rule @@ -201,7 +215,12 @@ def delete_security_group_rule(compute_client, security_group_rule_id=None): # networks -def create_network(compute_client, name, subnet, share_subnet=None): +def create_network( + compute_client: Any, + name: str, + subnet: str, + share_subnet: bool | None = None, +) -> Any: """Create a new network https://docs.openstack.org/api-ref/compute/#create-network @@ -212,7 +231,7 @@ def create_network(compute_client, name, subnet, share_subnet=None): :param bool share_subnet: Shared subnet between projects :returns: A network object """ - data = { + data: dict[str, Any] = { 'label': name, 'cidr': subnet, } @@ -226,7 +245,7 @@ def create_network(compute_client, name, subnet, share_subnet=None): return response.json()['network'] -def list_networks(compute_client): +def list_networks(compute_client: Any) -> Any: """Get all networks https://docs.openstack.org/api-ref/compute/#list-networks @@ -239,8 +258,8 @@ def list_networks(compute_client): return response.json()['networks'] -def find_network(compute_client, name_or_id): - """Find the ID for a given network name or ID +def find_network(compute_client: Any, name_or_id: str) -> Any: + """Find the network for a given name or ID https://docs.openstack.org/api-ref/compute/#show-network-details @@ -276,7 +295,7 @@ def find_network(compute_client, name_or_id): return found -def delete_network(compute_client, network_id): +def delete_network(compute_client: Any, network_id: str) -> None: """Delete a network https://docs.openstack.org/api-ref/compute/#delete-network @@ -294,7 +313,7 @@ def delete_network(compute_client, network_id): # floating ips -def create_floating_ip(compute_client, network): +def create_floating_ip(compute_client: Any, network: str) -> Any: """Create a new floating ip https://docs.openstack.org/api-ref/compute/#create-allocate-floating-ip-address @@ -309,7 +328,7 @@ def create_floating_ip(compute_client, network): return response.json()['floating_ip'] -def list_floating_ips(compute_client): +def list_floating_ips(compute_client: Any) -> Any: """Get all floating IPs https://docs.openstack.org/api-ref/compute/#list-floating-ip-addresses @@ -321,7 +340,7 @@ def list_floating_ips(compute_client): return response.json()['floating_ips'] -def get_floating_ip(compute_client, floating_ip_id): +def get_floating_ip(compute_client: Any, floating_ip_id: str) -> Any: """Get a floating IP https://docs.openstack.org/api-ref/compute/#show-floating-ip-address-details @@ -336,7 +355,7 @@ def get_floating_ip(compute_client, floating_ip_id): return response.json()['floating_ip'] -def delete_floating_ip(compute_client, floating_ip_id): +def delete_floating_ip(compute_client: Any, floating_ip_id: str) -> None: """Delete a floating IP https://docs.openstack.org/api-ref/compute/#delete-deallocate-floating-ip-address @@ -353,7 +372,7 @@ def delete_floating_ip(compute_client, floating_ip_id): # floating ip pools -def list_floating_ip_pools(compute_client): +def list_floating_ip_pools(compute_client: Any) -> Any: """Get all floating IP pools https://docs.openstack.org/api-ref/compute/#list-floating-ip-pools diff --git a/openstackclient/api/image_v1.py b/openstackclient/api/image_v1.py deleted file mode 100644 index a8b61aca96..0000000000 --- a/openstackclient/api/image_v1.py +++ /dev/null @@ -1,69 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -"""Image v1 API Library""" - -from openstackclient.api import api - - -class APIv1(api.BaseAPI): - """Image v1 API""" - - _endpoint_suffix = '/v1' - - def __init__(self, endpoint=None, **kwargs): - super().__init__(endpoint=endpoint, **kwargs) - - self.endpoint = self.endpoint.rstrip('/') - self._munge_url() - - def _munge_url(self): - # Hack this until discovery is up - if not self.endpoint.endswith(self._endpoint_suffix): - self.endpoint = self.endpoint + self._endpoint_suffix - - def image_list( - self, detailed=False, public=False, private=False, **filter - ): - """Get available images - - :param detailed: - Retrieve detailed response from server if True - :param public: - Return public images if True - :param private: - Return private images if True - - If public and private are both True or both False then all images are - returned. Both arguments False is equivalent to no filter and all - images are returned. Both arguments True is a filter that includes - both public and private images which is the same set as all images. - - http://docs.openstack.org/api/openstack-image-service/1.1/content/requesting-a-list-of-public-vm-images.html - http://docs.openstack.org/api/openstack-image-service/1.1/content/requesting-detailed-metadata-on-public-vm-images.html - http://docs.openstack.org/api/openstack-image-service/1.1/content/filtering-images-returned-via-get-images-and-get-imagesdetail.html - """ - - url = "/images" - if detailed or public or private: - # Because we can't all use /details - url += "/detail" - - image_list = self.list(url, **filter)['images'] - - if public != private: - # One is True and one is False, so public represents the filter - # state in either case - image_list = [i for i in image_list if i['is_public'] == public] - - return image_list diff --git a/openstackclient/api/image_v2.py b/openstackclient/api/image_v2.py deleted file mode 100644 index 9b0e9b1f8e..0000000000 --- a/openstackclient/api/image_v2.py +++ /dev/null @@ -1,79 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -"""Image v2 API Library""" - -from openstackclient.api import image_v1 - - -class APIv2(image_v1.APIv1): - """Image v2 API""" - - _endpoint_suffix = '/v2' - - def _munge_url(self): - # Hack this until discovery is up, and ignore parent endpoint setting - if not self.endpoint.endswith(self._endpoint_suffix): - self.endpoint = self.endpoint + self._endpoint_suffix - - def image_list( - self, - detailed=False, - public=False, - private=False, - community=False, - shared=False, - **filter, - ): - """Get available images - - can add limit/marker - - :param detailed: - For v1 compatibility only, ignored as v2 is always 'detailed' - :param public: - Return public images if True - :param private: - Return private images if True - :param community: - Return commuity images if True - :param shared: - Return shared images if True - - If public, private, community and shared are all True or all False - then all images are returned. All arguments False is equivalent to no - filter and all images are returned. All arguments True is a filter - that includes all public, private, community and shared images which - is the same set as all images. - - http://docs.openstack.org/api/openstack-image-service/2.0/content/list-images.html - """ - - if not public and not private and not community and not shared: - # No filtering for all False - filter.pop('visibility', None) - elif public: - filter['visibility'] = 'public' - elif private: - filter['visibility'] = 'private' - elif community: - filter['visibility'] = 'community' - elif shared: - filter['visibility'] = 'shared' - - url = "/images" - if detailed: - # Because we can't all use /details - url += "/detail" - - return self.list(url, **filter)['images'] diff --git a/openstackclient/api/object_store_v1.py b/openstackclient/api/object_store_v1.py index fd941e4838..92d44459c7 100644 --- a/openstackclient/api/object_store_v1.py +++ b/openstackclient/api/object_store_v1.py @@ -16,7 +16,8 @@ import logging import os import sys -import urllib +from typing import Any +import urllib.parse from osc_lib import utils @@ -31,12 +32,15 @@ class APIv1(api.BaseAPI): """Object Store v1 API""" - def __init__(self, **kwargs): + def __init__(self, **kwargs: Any) -> None: super().__init__(**kwargs) def container_create( - self, container=None, public=False, storage_policy=None - ): + self, + container: str, + public: bool = False, + storage_policy: str | None = None, + ) -> dict[str, Any | None]: """Create a container :param string container: @@ -58,7 +62,9 @@ def container_create( headers['x-storage-policy'] = storage_policy response = self.create( - urllib.parse.quote(container), method='PUT', headers=headers + urllib.parse.quote(container), + method='PUT', + headers=headers, ) data = { @@ -69,10 +75,7 @@ def container_create( return data - def container_delete( - self, - container=None, - ): + def container_delete(self, container: str) -> None: """Delete a container :param string container: @@ -84,13 +87,13 @@ def container_delete( def container_list( self, - full_listing=False, - limit=None, - marker=None, - end_marker=None, - prefix=None, - **params, - ): + full_listing: bool = False, + limit: int | None = None, + marker: str | None = None, + end_marker: str | None = None, + prefix: str | None = None, + **params: Any, + ) -> Any: """Get containers in an account :param boolean full_listing: @@ -142,10 +145,7 @@ def container_list( return self.list('', **params) - def container_save( - self, - container=None, - ): + def container_save(self, container: str) -> None: """Save all the content from a container :param string container: @@ -158,9 +158,9 @@ def container_save( def container_set( self, - container, - properties, - ): + container: str, + properties: dict[str, str], + ) -> None: """Set container properties :param string container: @@ -173,10 +173,7 @@ def container_set( if headers: self.create(urllib.parse.quote(container), headers=headers) - def container_show( - self, - container=None, - ): + def container_show(self, container: str) -> dict[str, Any]: """Get container details :param string container: @@ -213,9 +210,9 @@ def container_show( def container_unset( self, - container, - properties, - ): + container: str, + properties: dict[str, str], + ) -> None: """Unset container properties :param string container: @@ -231,11 +228,8 @@ def container_unset( self.create(urllib.parse.quote(container), headers=headers) def object_create( - self, - container=None, - object=None, - name=None, - ): + self, container: str, object: str, name: str | None = None + ) -> dict[str, Any]: """Create an object inside a container :param string container: @@ -254,30 +248,25 @@ def object_create( # For uploading a file, if name is provided then set it as the # object's name in the container. - object_name_str = name if name else object + name = name if name else object - full_url = f"{urllib.parse.quote(container)}/{urllib.parse.quote(object_name_str)}" with open(object, 'rb') as f: response = self.create( - full_url, + f"{urllib.parse.quote(container)}/{urllib.parse.quote(name)}", method='PUT', data=f, ) data = { 'account': self._find_account_id(), 'container': container, - 'object': object_name_str, + 'object': name, 'x-trans-id': response.headers.get('X-Trans-Id'), 'etag': response.headers.get('Etag'), } return data - def object_delete( - self, - container=None, - object=None, - ): + def object_delete(self, container: str, object: str) -> None: """Delete an object from a container :param string container: @@ -295,15 +284,15 @@ def object_delete( def object_list( self, - container=None, - full_listing=False, - limit=None, - marker=None, - end_marker=None, - delimiter=None, - prefix=None, - **params, - ): + container: str, + full_listing: bool = False, + limit: int | None = None, + marker: str | None = None, + end_marker: str | None = None, + delimiter: str | None = None, + prefix: str | None = None, + **params: Any, + ) -> Any: """List objects in a container :param string container: @@ -371,11 +360,8 @@ def object_list( return self.list(urllib.parse.quote(container), **params) def object_save( - self, - container=None, - object=None, - file=None, - ): + self, container: str, object: str, file: str | None = None + ) -> None: """Save an object stored in a container :param string container: @@ -391,7 +377,7 @@ def object_save( response = self._request( 'GET', - f"{urllib.parse.quote(container)}/{urllib.parse.quote(object)}", + f'{urllib.parse.quote(container)}/{urllib.parse.quote(object)}', stream=True, ) if response.status_code == 200: @@ -400,19 +386,20 @@ def object_save( for chunk in response.iter_content(64 * 1024): f.write(chunk) else: - if not os.path.exists(os.path.dirname(file)): - if len(os.path.dirname(file)) > 0: - os.makedirs(os.path.dirname(file)) - with open(file, 'wb') as f: + file_path = file or '' + if not os.path.exists(os.path.dirname(file_path)): + if len(os.path.dirname(file_path)) > 0: + os.makedirs(os.path.dirname(file_path)) + with open(file_path, 'wb') as f: for chunk in response.iter_content(64 * 1024): f.write(chunk) def object_set( self, - container, - object, - properties, - ): + container: str, + object: str, + properties: dict[str, str], + ) -> None: """Set object properties :param string container: @@ -431,11 +418,8 @@ def object_set( ) def object_unset( - self, - container, - object, - properties, - ): + self, container: str, object: str, properties: dict[str, str] + ) -> None: """Unset object properties :param string container: @@ -453,11 +437,7 @@ def object_unset( headers=headers, ) - def object_show( - self, - container=None, - object=None, - ): + def object_show(self, container: str, object: str) -> dict[str, Any]: """Get object details :param string container: @@ -499,10 +479,7 @@ def object_show( return data - def account_set( - self, - properties, - ): + def account_set(self, properties: dict[str, str]) -> None: """Set account properties :param dict properties: @@ -516,13 +493,13 @@ def account_set( # registered in the catalog self.create("", headers=headers) - def account_show(self): + def account_show(self) -> dict[str, Any]: """Show account details""" # NOTE(stevemar): Just a HEAD request to the endpoint already in the # catalog should be enough. response = self._request("HEAD", "") - data = {} + data: dict[str, Any] = {} properties = self._get_properties(response.headers, 'x-account-meta-') if properties: @@ -538,8 +515,8 @@ def account_show(self): def account_unset( self, - properties, - ): + properties: dict[str, str], + ) -> None: """Unset account properties :param dict properties: @@ -552,11 +529,13 @@ def account_unset( if headers: self.create("", headers=headers) - def _find_account_id(self): - url_parts = urllib.parse.urlparse(self.endpoint) - return url_parts.path.split('/')[-1] + def _find_account_id(self) -> str: + url_parts = urllib.parse.urlparse(self.endpoint or '') + return str(url_parts.path).split('/')[-1] - def _unset_properties(self, properties, header_tag): + def _unset_properties( + self, properties: dict[str, str], header_tag: str + ) -> dict[str, str]: # NOTE(stevemar): As per the API, the headers have to be in the form # of "X-Remove-Account-Meta-Book: x". In the case where metadata is # removed, we can set the value of the header to anything, so it's @@ -564,13 +543,15 @@ def _unset_properties(self, properties, header_tag): # "X-Remove-Container-Meta-Book: x", and the same logic applies for # Object properties - headers = {} + headers: dict[str, str] = {} for k in properties: header_name = header_tag % k headers[header_name] = 'x' return headers - def _set_properties(self, properties, header_tag): + def _set_properties( + self, properties: dict[str, str], header_tag: str + ) -> dict[str, str]: # NOTE(stevemar): As per the API, the headers have to be in the form # of "X-Account-Meta-Book: MobyDick". In the case of a Container # property we use: "X-Add-Container-Meta-Book: MobyDick", and the same @@ -578,7 +559,7 @@ def _set_properties(self, properties, header_tag): log = logging.getLogger(__name__ + '._set_properties') - headers = {} + headers: dict[str, str] = {} for k, v in properties.items(): if not utils.is_ascii(k) or not utils.is_ascii(v): log.error('Cannot set property %s to non-ascii value', k) @@ -588,10 +569,10 @@ def _set_properties(self, properties, header_tag): headers[header_name] = v return headers - def _get_properties(self, headers, header_tag): + def _get_properties(self, headers: Any, header_tag: str) -> dict[str, Any]: # Add in properties as a top level key, this is consistent with other # OSC commands - properties = {} + properties: dict[str, Any] = {} for k, v in headers.items(): if k.lower().startswith(header_tag): properties[k[len(header_tag) :]] = v diff --git a/openstackclient/api/volume_v2.py b/openstackclient/api/volume_v2.py new file mode 100644 index 0000000000..ef2191b179 --- /dev/null +++ b/openstackclient/api/volume_v2.py @@ -0,0 +1,61 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Volume v2 API Library + +A collection of wrappers for deprecated Block Storage v2 APIs that are not +intentionally supported by SDK. +""" + +import http +from typing import Any + +from openstack import exceptions as sdk_exceptions +from osc_lib import exceptions + + +# consistency groups + + +def find_consistency_group(compute_client: Any, name_or_id: str) -> Any: + """Find the consistency group for a given name or ID + + https://docs.openstack.org/api-ref/block-storage/v3/#show-a-consistency-group-s-details + + :param volume_client: A volume client + :param name_or_id: The name or ID of the consistency group to look up + :returns: A consistency group object + :raises exception.NotFound: If a matching consistency group could not be + found or more than one match was found + """ + response = compute_client.get(f'/consistencygroups/{name_or_id}') + if response.status_code != http.HTTPStatus.NOT_FOUND: + # there might be other, non-404 errors + sdk_exceptions.raise_from_response(response) + return response.json()['consistencygroup'] + + response = compute_client.get('/consistencygroups') + sdk_exceptions.raise_from_response(response) + found = None + consistency_groups = response.json()['consistencygroups'] + for consistency_group in consistency_groups: + if consistency_group['name'] == name_or_id: + if found: + raise exceptions.NotFound( + f'multiple matches found for {name_or_id}' + ) + found = consistency_group + + if not found: + raise exceptions.NotFound(f'{name_or_id} not found') + + return found diff --git a/openstackclient/api/volume_v3.py b/openstackclient/api/volume_v3.py new file mode 100644 index 0000000000..d86537c3be --- /dev/null +++ b/openstackclient/api/volume_v3.py @@ -0,0 +1,61 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Volume v3 API Library + +A collection of wrappers for deprecated Block Storage v3 APIs that are not +intentionally supported by SDK. +""" + +import http +from typing import Any + +from openstack import exceptions as sdk_exceptions +from osc_lib import exceptions + + +# consistency groups + + +def find_consistency_group(compute_client: Any, name_or_id: str) -> Any: + """Find the consistency group for a given name or ID + + https://docs.openstack.org/api-ref/block-storage/v3/#show-a-consistency-group-s-details + + :param volume_client: A volume client + :param name_or_id: The name or ID of the consistency group to look up + :returns: A consistency group object + :raises exception.NotFound: If a matching consistency group could not be + found or more than one match was found + """ + response = compute_client.get(f'/consistencygroups/{name_or_id}') + if response.status_code != http.HTTPStatus.NOT_FOUND: + # there might be other, non-404 errors + sdk_exceptions.raise_from_response(response) + return response.json()['consistencygroup'] + + response = compute_client.get('/consistencygroups') + sdk_exceptions.raise_from_response(response) + found = None + consistency_groups = response.json()['consistencygroups'] + for consistency_group in consistency_groups: + if consistency_group['name'] == name_or_id: + if found: + raise exceptions.NotFound( + f'multiple matches found for {name_or_id}' + ) + found = consistency_group + + if not found: + raise exceptions.NotFound(f'{name_or_id} not found') + + return found diff --git a/openstackclient/command.py b/openstackclient/command.py new file mode 100644 index 0000000000..124d755aa9 --- /dev/null +++ b/openstackclient/command.py @@ -0,0 +1,27 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from cliff import lister +from cliff import show +from osc_lib.command import command + +from openstackclient import shell + + +class Command(command.Command): + app: shell.OpenStackShell + + +class Lister(Command, lister.Lister): ... + + +class ShowOne(Command, show.ShowOne): ... diff --git a/openstackclient/common/availability_zone.py b/openstackclient/common/availability_zone.py index 61f77cc06e..cc8d82d580 100644 --- a/openstackclient/common/availability_zone.py +++ b/openstackclient/common/availability_zone.py @@ -13,21 +13,27 @@ """Availability Zone action implementations""" +import argparse import copy import logging +from collections.abc import Iterable, Sequence +from typing import Any from openstack import exceptions as sdk_exceptions -from osc_lib.command import command +from openstack import utils as sdk_utils from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -def _xform_compute_availability_zone(az, include_extra): - result = [] +def _xform_compute_availability_zone( + az: Any, include_extra: bool +) -> list[dict[str, str]]: + result: list[dict[str, str]] = [] zone_info = { 'zone_name': az.name, 'zone_status': ( @@ -61,20 +67,20 @@ def _xform_compute_availability_zone(az, include_extra): return result -def _xform_volume_availability_zone(az): +def _xform_share_availability_zone(az: Any) -> list[dict[str, str]]: result = [] zone_info = { 'zone_name': az.name, - 'zone_status': ( - 'available' if az.state['available'] else 'not available' - ), + # manila doesn't have the concept of availability zone statuses so we + # show zones as always "available" + 'zone_status': 'available', } result.append(zone_info) return result -def _xform_network_availability_zone(az): - result = [] +def _xform_network_availability_zone(az: Any) -> list[dict[str, str]]: + result: list[dict[str, str]] = [] zone_info = {} zone_info['zone_name'] = az.name zone_info['zone_status'] = az.state @@ -85,10 +91,22 @@ def _xform_network_availability_zone(az): return result +def _xform_volume_availability_zone(az: Any) -> list[dict[str, str]]: + result: list[dict[str, str]] = [] + zone_info = { + 'zone_name': az.name, + 'zone_status': ( + 'available' if az.state['available'] else 'not available' + ), + } + result.append(zone_info) + return result + + class ListAvailabilityZone(command.Lister): _description = _("List availability zones and their status") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--compute', @@ -108,6 +126,12 @@ def get_parser(self, prog_name): default=False, help=_('List volume availability zones'), ) + parser.add_argument( + '--share', + action='store_true', + default=False, + help=_('List share availability zones'), + ) parser.add_argument( '--long', action='store_true', @@ -116,13 +140,15 @@ def get_parser(self, prog_name): ) return parser - def _get_compute_availability_zones(self, parsed_args): + def _get_compute_availability_zones( + self, parsed_args: argparse.Namespace + ) -> list[dict[str, str]]: compute_client = self.app.client_manager.compute try: data = list(compute_client.availability_zones(details=True)) except sdk_exceptions.ForbiddenException: # policy doesn't allow try: - data = compute_client.availability_zones(details=False) + data = list(compute_client.availability_zones(details=False)) except Exception: raise @@ -131,47 +157,85 @@ def _get_compute_availability_zones(self, parsed_args): result += _xform_compute_availability_zone(zone, parsed_args.long) return result - def _get_volume_availability_zones(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def _get_network_availability_zones( + self, parsed_args: argparse.Namespace + ) -> list[dict[str, str]]: + network_client = self.app.client_manager.network + try: + # Verify that the extension exists. + network_client.find_extension( + 'Availability Zone', ignore_missing=False + ) + except Exception as e: + LOG.debug('Network availability zone exception: ', e) + if parsed_args.network: + message = _( + "Availability zones list not supported by Network API" + ) + LOG.warning(message) + return [] + + result = [] + for zone in network_client.availability_zones(): + result += _xform_network_availability_zone(zone) + return result + + def _get_share_availability_zone( + self, parsed_args: argparse.Namespace + ) -> list[dict[str, str]]: data = [] try: - data = list(volume_client.availability_zones()) + share_client = self.app.client_manager.sdk_connection.share + data = list(share_client.availability_zones()) except Exception as e: - LOG.debug('Volume availability zone exception: %s', e) - if parsed_args.volume: + if parsed_args.share: message = _( "Availability zones list not supported by " - "Block Storage API" + "Shared File System API" ) LOG.warning(message) + else: + LOG.debug('Share availability zone not available: %s', e) result = [] for zone in data: - result += _xform_volume_availability_zone(zone) + result += _xform_share_availability_zone(zone) return result - def _get_network_availability_zones(self, parsed_args): - network_client = self.app.client_manager.network + def _get_volume_availability_zones( + self, parsed_args: argparse.Namespace + ) -> list[dict[str, str]]: + if not self.app.client_manager.is_volume_endpoint_enabled(): + if parsed_args.volume: + message = _( + "Block Storage API is not available in the current cloud" + ) + LOG.warning(message) + return [] + + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + data = [] try: - # Verify that the extension exists. - network_client.find_extension( - 'Availability Zone', ignore_missing=False - ) + data = list(volume_client.availability_zones()) except Exception as e: - LOG.debug('Network availability zone exception: ', e) - if parsed_args.network: + LOG.debug('Volume availability zone exception: %s', e) + if parsed_args.volume: message = _( - "Availability zones list not supported by Network API" + "Availability zones list not supported by " + "Block Storage API" ) LOG.warning(message) - return [] result = [] - for zone in network_client.availability_zones(): - result += _xform_network_availability_zone(zone) + for zone in data: + result += _xform_volume_availability_zone(zone) return result - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: columns: tuple[str, ...] = ('Zone Name', 'Zone Status') if parsed_args.long: columns += ( @@ -184,17 +248,20 @@ def take_action(self, parsed_args): # Show everything by default. show_all = ( not parsed_args.compute - and not parsed_args.volume and not parsed_args.network + and not parsed_args.share + and not parsed_args.volume ) result = [] if parsed_args.compute or show_all: result += self._get_compute_availability_zones(parsed_args) - if parsed_args.volume or show_all: - result += self._get_volume_availability_zones(parsed_args) if parsed_args.network or show_all: result += self._get_network_availability_zones(parsed_args) + if parsed_args.share or show_all: + result += self._get_share_availability_zone(parsed_args) + if parsed_args.volume or show_all: + result += self._get_volume_availability_zones(parsed_args) return ( columns, diff --git a/openstackclient/common/clientmanager.py b/openstackclient/common/clientmanager.py index a0455bf1a3..ca081664e0 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -15,20 +15,28 @@ """Manage access to the clients, including authenticating when needed.""" +import argparse +from collections.abc import Callable import importlib import logging import sys -import typing as ty - +from typing import Any, Protocol, TypeVar, runtime_checkable + +from keystoneauth1 import access as ksa_access +from openstack.block_storage import v2 as volume_v2 +from openstack.block_storage import v3 as volume_v3 +from openstack.compute import v2 as compute_v2 +from openstack.image import v2 as image_v2 +from openstack.network import v2 as network_v2 +from osc_lib.cli import client_config from osc_lib import clientmanager from osc_lib import shell import stevedore +from openstackclient.api import object_store_v1 LOG = logging.getLogger(__name__) - -PLUGIN_MODULES: list[ty.Any] = [] - +PLUGIN_MODULES: list[Any] = [] USER_AGENT = 'python-openstackclient' @@ -40,18 +48,27 @@ class ClientManager(clientmanager.ClientManager): in osc-lib so we need to maintain a transition period. """ - # A simple incrementing version for the plugin to know what is available - PLUGIN_INTERFACE_VERSION = "2" - - # Let the commands set this - _auth_required = False + # we know this will be set by us and will not be nullable + auth_ref: ksa_access.AccessInfo + + # this is a hack to keep mypy happy: the actual attributes are set in + # get_plugin_modules below + # TODO(stephenfin): Change the types of identity, object store and share + # once we've migrated everything to SDK + compute: compute_v2.Proxy + identity: Any + image: image_v2.Proxy + network: network_v2.Proxy + object_store: object_store_v1.APIv1 + share: Any + volume: volume_v2.Proxy | volume_v3.Proxy def __init__( self, - cli_options=None, - api_version=None, - pw_func=None, - ): + cli_options: Any = None, + api_version: Any = None, + pw_func: Any = None, + ) -> None: super().__init__( cli_options=cli_options, api_version=api_version, @@ -59,7 +76,7 @@ def __init__( ) # TODO(dtroyer): For compatibility; mark this for removal when plugin - # interface v2 is removed + # interface v2 is removed self._region_name = self.region_name self._interface = self.interface self._cacert = self.cacert @@ -67,7 +84,7 @@ def __init__( # store original auth_type self._original_auth_type = cli_options.auth_type - def setup_auth(self): + def setup_auth(self) -> None: """Set up authentication""" if self._auth_setup_completed: @@ -81,6 +98,12 @@ def setup_auth(self): self._auth_required and self._cli_options._openstack_config is not None ): + if not isinstance( + self._cli_options._openstack_config, client_config.OSC_Config + ): + # programmer error + raise TypeError('unexpected type for _openstack_config') + self._cli_options._openstack_config._pw_callback = ( shell.prompt_for_password ) @@ -97,7 +120,7 @@ def setup_auth(self): return super().setup_auth() - def _fallback_load_auth_plugin(self, e): + def _fallback_load_auth_plugin(self, e: Exception) -> None: # NOTES(RuiChen): Hack to avoid auth plugins choking on data they don't # expect, delete fake token and endpoint, then try to # load auth plugin again with user specified options. @@ -107,6 +130,13 @@ def _fallback_load_auth_plugin(self, e): self._cli_options.config['auth_type'] = self._original_auth_type del self._cli_options.config['auth']['token'] del self._cli_options.config['auth']['endpoint'] + + if not isinstance( + self._cli_options._openstack_config, client_config.OSC_Config + ): + # programmer error + raise TypeError('unexpected type for _openstack_config') + self._cli_options._auth = ( self._cli_options._openstack_config.load_auth_plugin( self._cli_options.config, @@ -115,79 +145,107 @@ def _fallback_load_auth_plugin(self, e): else: raise e - def is_network_endpoint_enabled(self): + def is_network_endpoint_enabled(self) -> bool: """Check if the network endpoint is enabled""" # NOTE(dtroyer): is_service_available() can also return None if # there is no Service Catalog, callers here are # not expecting that so fold None into True to # use Network API by default - return self.is_service_available('network') is not False + return ( + self.is_service_available( + 'network', self.region_name, self.interface + ) + is not False + ) - def is_compute_endpoint_enabled(self): + def is_compute_endpoint_enabled(self) -> bool: """Check if Compute endpoint is enabled""" - return self.is_service_available('compute') is not False + return ( + self.is_service_available( + 'compute', self.region_name, self.interface + ) + is not False + ) # TODO(stephenfin): Drop volume_client argument in OSC 8.0 or later. - def is_volume_endpoint_enabled(self, volume_client=None): + def is_volume_endpoint_enabled(self, volume_client: Any = None) -> bool: """Check if volume endpoint is enabled""" # We check against the service type and all aliases defined by the # Service Types Authority # https://service-types.openstack.org/service-types.json return ( - self.is_service_available('block-storage') is not False - or self.is_service_available('volume') is not False - or self.is_service_available('volumev3') is not False - or self.is_service_available('volumev2') is not False - or self.is_service_available('block-store') is not False + self.is_service_available( + 'block-storage', self.region_name, self.interface + ) + is not False + or self.is_service_available( + 'volume', self.region_name, self.interface + ) + is not False + or self.is_service_available( + 'volumev3', self.region_name, self.interface + ) + is not False + or self.is_service_available( + 'volumev2', self.region_name, self.interface + ) + is not False + or self.is_service_available( + 'block-store', self.region_name, self.interface + ) + is not False ) # Plugin Support +ArgumentParserT = TypeVar('ArgumentParserT', bound=argparse.ArgumentParser) + + +@runtime_checkable # Optional: allows usage with isinstance() +class PluginModule(Protocol): + DEFAULT_API_VERSION: str + API_VERSION_OPTION: str + API_NAME: str + API_VERSIONS: tuple[str] + + make_client: Callable[..., Any] + build_option_parser: Callable[[ArgumentParserT], ArgumentParserT] + check_api_version: Callable[[str], bool] + def _on_load_failure_callback( - manager: stevedore.ExtensionManager, + manager: stevedore.ExtensionManager[PluginModule], ep: importlib.metadata.EntryPoint, - err: Exception, + err: BaseException, ) -> None: sys.stderr.write( f"WARNING: Failed to import plugin {ep.group}:{ep.name}: {err}.\n" ) -def get_plugin_modules(group): +def get_plugin_modules(group: str) -> list[Any]: """Find plugin entry points""" mod_list = [] + mgr: stevedore.ExtensionManager[PluginModule] mgr = stevedore.ExtensionManager( group, on_load_failure_callback=_on_load_failure_callback ) for ep in mgr: LOG.debug('Found plugin %s', ep.name) - # Different versions of stevedore use different - # implementations of EntryPoint from other libraries, which - # are not API-compatible. - try: - module_name = ep.entry_point.module_name - except AttributeError: - try: - module_name = ep.entry_point.module - except AttributeError: - module_name = ep.entry_point.value + module_name = ep.entry_point.module try: module = importlib.import_module(module_name) except Exception as err: sys.stderr.write( - f"WARNING: Failed to import plugin {ep.group}:{ep.name}: " - f"{err}.\n" + f"WARNING: Failed to import plugin " + f"{ep.module_name}:{ep.name}: {err}.\n" ) continue mod_list.append(module) - init_func = getattr(module, 'Initialize', None) - if init_func: - init_func('x') # Add the plugin to the ClientManager setattr( @@ -200,7 +258,9 @@ def get_plugin_modules(group): return mod_list -def build_plugin_option_parser(parser): +def build_plugin_option_parser( + parser: ArgumentParserT, +) -> ArgumentParserT: """Add plugin options to the parser""" # Loop through extensions to get parser additions diff --git a/openstackclient/common/configuration.py b/openstackclient/common/configuration.py index 418f7fdd0f..e73d42373b 100644 --- a/openstackclient/common/configuration.py +++ b/openstackclient/common/configuration.py @@ -13,9 +13,13 @@ """Configuration action implementations""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from keystoneauth1.loading import base -from osc_lib.command import command +from openstackclient import command from openstackclient.i18n import _ REDACTED = "" @@ -26,7 +30,7 @@ class ShowConfiguration(command.ShowOne): auth_required = False - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) mask_group = parser.add_mutually_exclusive_group() mask_group.add_argument( @@ -44,7 +48,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: info = self.app.client_manager.get_configuration() # Assume a default secret list in case we do not have an auth_plugin @@ -68,4 +74,5 @@ def take_action(self, parsed_args): if secret_opt in info: info[secret_opt] = REDACTED - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data diff --git a/openstackclient/common/envvars.py b/openstackclient/common/envvars.py index 5f702ff3d4..60b545a75b 100644 --- a/openstackclient/common/envvars.py +++ b/openstackclient/common/envvars.py @@ -15,7 +15,8 @@ from openstackclient.i18n import _ -def bool_from_str(value, strict=False): +# TODO(stephenfin): Move this to osc-lib +def bool_from_str(value: bool | str, strict: bool = False) -> bool: true_strings = ('1', 't', 'true', 'on', 'y', 'yes') false_strings = ('0', 'f', 'false', 'off', 'n', 'no') @@ -39,7 +40,7 @@ def bool_from_str(value, strict=False): raise ValueError(msg) -def boolenv(*vars, default=False): +def boolenv(*vars: str, default: bool = False) -> bool: """Search for the first defined of possibly many bool-like env vars. Returns the first environment variable defined in vars, or returns the diff --git a/openstackclient/common/extension.py b/openstackclient/common/extension.py index 42c2e9b9f3..106f65a5c6 100644 --- a/openstackclient/common/extension.py +++ b/openstackclient/common/extension.py @@ -15,17 +15,23 @@ """Extension action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.identity import v2 as identity_v2 from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -def _get_extension_columns(item): +def _get_extension_columns( + item: Any, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { 'updated': 'updated_at', } @@ -38,7 +44,7 @@ def _get_extension_columns(item): class ListExtension(command.Lister): _description = _("List API extensions") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--compute', @@ -50,7 +56,9 @@ def get_parser(self, prog_name): '--identity', action='store_true', default=False, - help=_('List extensions for the Identity API'), + help=_( + 'List extensions for the Identity API (only supported by v2)' + ), ) parser.add_argument( '--network', @@ -72,12 +80,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: columns: tuple[str, ...] = ('Name', 'Alias', 'Description') if parsed_args.long: columns += ('Namespace', 'Updated At', 'Links') - data = [] + data: list[Any] = [] # by default we want to show everything, unless the # user specifies one or more of the APIs to show @@ -90,12 +100,16 @@ def take_action(self, parsed_args): ) if parsed_args.identity or show_all: - identity_client = self.app.client_manager.identity - try: - data += identity_client.extensions.list() - except Exception: - message = _("Extensions list not supported by Identity API") - LOG.warning(message) + identity_client = self.app.client_manager.sdk_connection.identity + # only identity v2 support extensions + if isinstance(identity_client, identity_v2.Proxy): + try: + data += identity_client.extensions() + except Exception: + message = _( + "Extensions list not supported by Identity API" + ) + LOG.warning(message) if parsed_args.compute or show_all: compute_client = self.app.client_manager.compute @@ -106,12 +120,18 @@ def take_action(self, parsed_args): LOG.warning(message) if parsed_args.volume or show_all: - volume_client = self.app.client_manager.sdk_connection.volume - try: - data += volume_client.extensions() - except Exception: + if self.app.client_manager.is_volume_endpoint_enabled(): + volume_client = self.app.client_manager.volume + try: + data += volume_client.extensions() + except Exception: + message = _( + "Extensions list not supported by Block Storage API" + ) + LOG.warning(message) + elif parsed_args.volume: message = _( - "Extensions list not supported by Block Storage API" + "Block Storage API is not available in the current cloud" ) LOG.warning(message) @@ -139,7 +159,7 @@ def take_action(self, parsed_args): class ShowExtension(command.ShowOne): _description = _("Show API extension") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'extension', @@ -152,7 +172,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network extension = client.find_extension( diff --git a/openstackclient/common/limits.py b/openstackclient/common/limits.py index 0c92805937..5eae379321 100644 --- a/openstackclient/common/limits.py +++ b/openstackclient/common/limits.py @@ -15,17 +15,20 @@ """Limits Action Implementation""" +import argparse import itertools +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command -from osc_lib import utils +from openstack import utils as sdk_utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common as identity_common -def _format_absolute_limit(absolute_limits): - info = {} +def _format_absolute_limit(absolute_limits: Any) -> dict[str, Any]: + info: dict[str, Any] = {} for key in set(absolute_limits): if key in ('id', 'name', 'location'): @@ -36,7 +39,7 @@ def _format_absolute_limit(absolute_limits): return info -def _format_rate_limit(rate_limits): +def _format_rate_limit(rate_limits: Any) -> Any: # flatten this: # # {'uri': '', 'limit': [{'value': '', ...], ...} @@ -52,7 +55,7 @@ def _format_rate_limit(rate_limits): class ShowLimits(command.Lister): _description = _("Show compute and block storage limits") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) type_group = parser.add_mutually_exclusive_group(required=True) type_group.add_argument( @@ -90,33 +93,29 @@ def get_parser(self, prog_name): '(only valid with --absolute)' ), ) + identity_common.add_project_domain_option_to_parser(parser) + # deprecated alias for --project-domain parser.add_argument( '--domain', metavar='', - help=_( - 'Domain the project belongs to (name or ID) ' - '(only valid with --absolute)' - ), + dest='project_domain', + help=argparse.SUPPRESS, ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: project_id = None if parsed_args.project is not None: - identity_client = self.app.client_manager.identity - if parsed_args.domain is not None: - domain = identity_common.find_domain( - identity_client, parsed_args.domain - ) - project_id = utils.find_resource( - identity_client.projects, - parsed_args.project, - domain_id=domain.id, - ).id - else: - project_id = utils.find_resource( - identity_client.projects, parsed_args.project - ).id + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) compute_limits = None volume_limits = None @@ -128,7 +127,7 @@ def take_action(self, parsed_args): ) if self.app.client_manager.is_volume_endpoint_enabled(): - volume_client = self.app.client_manager.sdk_connection.volume + volume_client = self.app.client_manager.volume volume_limits = volume_client.get_limits( project=project_id, ) diff --git a/openstackclient/common/module.py b/openstackclient/common/module.py index d4a5b955a0..ca0ab779b8 100644 --- a/openstackclient/common/module.py +++ b/openstackclient/common/module.py @@ -15,11 +15,15 @@ """Module action implementation""" +import argparse +import importlib.metadata import sys +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -28,7 +32,7 @@ class ListCommand(command.Lister): auth_required = False - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--group', @@ -41,14 +45,18 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: cm = self.app.command_manager groups = cm.get_command_groups() groups = sorted(groups) columns = ('Command Group', 'Commands') if parsed_args.group: - groups = (group for group in groups if parsed_args.group in group) + groups = sorted( + group for group in groups if parsed_args.group in group + ) commands = [] for group in groups: @@ -59,7 +67,7 @@ def take_action(self, parsed_args): # TODO(bapalm): Fix this when cliff properly supports # handling the detection rather than using the hard-code below. if parsed_args.formatter == 'table': - command_names = utils.format_list(command_names, "\n") + command_names = utils.format_list(command_names, "\n") # type: ignore[assignment] commands.append((group, command_names)) @@ -71,7 +79,7 @@ class ListModule(command.ShowOne): auth_required = False - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--all', @@ -81,38 +89,34 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: data = {} # Get module versions - mods = sys.modules - for k in mods.keys(): - k = k.split('.')[0] + package_distributions = importlib.metadata.packages_distributions() + for k in sys.modules.keys(): + module = k.split('.')[0] # Skip private modules and the modules that had been added, # like: keystoneclient, keystoneclient.exceptions and # keystoneclient.auth - if not k.startswith('_') and k not in data: - # TODO(dtroyer): Need a better way to decide which modules to - # show for the default (not --all) invocation. - # It should be just the things we actually care - # about like client and plugin modules... - if ( - parsed_args.all - or - # Handle xxxclient and openstacksdk - (k.endswith('client') or k == 'openstack') - ): - try: - # NOTE(RuiChen): openstacksdk bug/1588823 exist, - # no good way to add __version__ for - # openstack module properly, hard code - # looks bad, but openstacksdk module - # information is important. - if k == 'openstack': - data[k] = mods[k].version.__version__ - else: - data[k] = mods[k].__version__ - except Exception: # noqa: S110 - # Catch all exceptions, just skip it - pass - - return zip(*sorted(data.items())) + if module.startswith('_') or module in data: + continue + + # TODO(dtroyer): Need a better way to decide which modules to + # show for the default (not --all) invocation. + # It should be just the things we actually care + # about like client and plugin modules... + # Handle xxxclient and openstacksdk + if parsed_args.all or ( + module.endswith('client') or module == 'openstack' + ): + try: + package = package_distributions[module][0] + data[module] = importlib.metadata.version(package) + except Exception: # noqa: S110 + # Catch all exceptions, just skip it + pass + + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data diff --git a/openstackclient/common/pagination.py b/openstackclient/common/pagination.py index b6a11c5f5f..241c6ff17b 100644 --- a/openstackclient/common/pagination.py +++ b/openstackclient/common/pagination.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse + from osc_lib.cli import parseractions from openstackclient.i18n import _ @@ -19,7 +21,12 @@ # useful -def add_marker_pagination_option_to_parser(parser): +# TODO(stephenfin): include_max_items should not be necessary once we need to +# migrate remaining users to openstacksdk +def add_marker_pagination_option_to_parser( + parser: argparse.ArgumentParser, + include_max_items: bool = True, +) -> None: """Add marker-based pagination options to the parser. APIs that use marker-based paging use the marker and limit query parameters @@ -35,8 +42,11 @@ def add_marker_pagination_option_to_parser(parser): type=int, action=parseractions.NonNegativeAction, help=_( - 'The maximum number of entries to return. If the value exceeds ' - 'the server-defined maximum, then the maximum value will be used.' + 'The maximum number of entries to return per page. If the value ' + 'exceeds the server-defined maximum, then the server-defined ' + 'value will be used. Note that this controls the page size, not ' + 'the total number of entries returned. Use --max-items to limit ' + 'the total number of entries returned.' ), ) parser.add_argument( @@ -48,9 +58,27 @@ def add_marker_pagination_option_to_parser(parser): 'This should be a value that was returned in a previous request.' ), ) + if include_max_items: + parser.add_argument( + '--max-items', + metavar='', + type=int, + action=parseractions.NonNegativeAction, + default=None, + help=_( + 'The maximum number of entries to return in total, paging ' + 'through multiple requests if needed. Use --limit to control ' + 'the page size.' + ), + ) -def add_offset_pagination_option_to_parser(parser): +# TODO(stephenfin): include_max_items should not be necessary once we need to +# migrate remaining users to openstacksdk +def add_offset_pagination_option_to_parser( + parser: argparse.ArgumentParser, + include_max_items: bool = True, +) -> None: """Add offset-based pagination options to the parser. APIs that use offset-based paging use the offset and limit query parameters @@ -65,8 +93,11 @@ def add_offset_pagination_option_to_parser(parser): type=int, action=parseractions.NonNegativeAction, help=_( - 'The maximum number of entries to return. If the value exceeds ' - 'the server-defined maximum, then the maximum value will be used.' + 'The maximum number of entries to return per page. If the value ' + 'exceeds the server-defined maximum, then the maximum value will ' + 'be used. Note that this controls the page size, not the total ' + 'number of entries returned. Use --max-items to limit the total ' + 'number of entries returned.' ), ) parser.add_argument( @@ -80,3 +111,16 @@ def add_offset_pagination_option_to_parser(parser): 'return.' ), ) + if include_max_items: + parser.add_argument( + '--max-items', + metavar='', + type=int, + action=parseractions.NonNegativeAction, + default=None, + help=_( + 'The maximum number of entries to return in total, paging ' + 'through multiple requests if needed. Use --limit to control ' + 'the page size.' + ), + ) diff --git a/openstackclient/common/progressbar.py b/openstackclient/common/progressbar.py index 2852bb250a..a2e22cfe46 100644 --- a/openstackclient/common/progressbar.py +++ b/openstackclient/common/progressbar.py @@ -14,6 +14,7 @@ # under the License. import sys +from typing import Any class _ProgressBarBase: @@ -28,24 +29,24 @@ class _ProgressBarBase: :note: The progress will be displayed only if sys.stdout is a tty. """ - def __init__(self, wrapped, totalsize): + def __init__(self, wrapped: Any, totalsize: int) -> None: self._wrapped = wrapped self._totalsize = float(totalsize) self._show_progress = sys.stdout.isatty() and self._totalsize != 0 - self._percent = 0 + self._percent = 0.0 - def _display_progress_bar(self, size_read): + def _display_progress_bar(self, size_read: int) -> None: if self._show_progress: self._percent += size_read / self._totalsize # Output something like this: [==========> ] 49% sys.stdout.write( '\r[{:<30}] {:.0%}'.format( - '=' * int(round(self._percent * 29)) + '>', self._percent + '=' * round(self._percent * 29) + '>', self._percent ) ) sys.stdout.flush() - def __getattr__(self, attr): + def __getattr__(self, attr: str) -> Any: # Forward other attribute access to the wrapped object. return getattr(self._wrapped, attr) @@ -57,7 +58,7 @@ class VerboseFileWrapper(_ProgressBarBase): wrapped file's read method is called. """ - def read(self, *args, **kwargs): + def read(self, *args: Any, **kwargs: Any) -> Any: data = self._wrapped.read(*args, **kwargs) if data: self._display_progress_bar(len(data)) diff --git a/openstackclient/common/project_cleanup.py b/openstackclient/common/project_cleanup.py index 84a07353d7..6721028bce 100644 --- a/openstackclient/common/project_cleanup.py +++ b/openstackclient/common/project_cleanup.py @@ -13,15 +13,17 @@ # under the License. # +import argparse import getpass import logging import os import queue -import typing as ty +from typing import Any from cliff.formatters import table -from osc_lib.command import command +from openstack import utils as sdk_utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -29,7 +31,7 @@ LOG = logging.getLogger(__name__) -def ask_user_yesno(msg): +def ask_user_yesno(msg: str) -> bool: """Ask user Y/N question :param str msg: question text @@ -46,7 +48,7 @@ def ask_user_yesno(msg): class ProjectCleanup(command.Command): _description = _("Clean resources associated with a project") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) action_group = parser.add_mutually_exclusive_group() action_group.add_argument( @@ -89,19 +91,24 @@ def get_parser(self, prog_name): identity_common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): - sdk = self.app.client_manager.sdk_connection + def take_action(self, parsed_args: argparse.Namespace) -> None: + connection = self.app.client_manager.sdk_connection if parsed_args.auth_project: - project_connect = sdk + # is we've got a project already configured, use the connection + # as-is + pass elif parsed_args.project: - project = sdk.identity.find_project( + identity_client = sdk_utils.ensure_service_version( + connection.identity, '3' + ) + project = identity_client.find_project( name_or_id=parsed_args.project, ignore_missing=False ) - project_connect = sdk.connect_as_project(project) + connection = connection.connect_as_project(project) - if project_connect: - status_queue: queue.Queue[ty.Any] = queue.Queue() + if connection: + status_queue: queue.Queue[Any] = queue.Queue() parsed_args.max_width = int( os.environ.get('CLIFF_MAX_TERM_WIDTH', 0) ) @@ -120,7 +127,7 @@ def take_action(self, parsed_args): if parsed_args.updated_before: filters['updated_at'] = parsed_args.updated_before - project_connect.project_cleanup( + connection.project_cleanup( dry_run=True, status_queue=status_queue, filters=filters, @@ -150,7 +157,7 @@ def take_action(self, parsed_args): self.log.warning(_('Deleting resources')) - project_connect.project_cleanup( + connection.project_cleanup( dry_run=False, status_queue=status_queue, filters=filters, diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index f706a59746..01b1660819 100644 --- a/openstackclient/common/quota.py +++ b/openstackclient/common/quota.py @@ -18,15 +18,16 @@ import itertools import logging import sys -import typing as ty +from collections.abc import Iterable, Sequence +from typing import Any from openstack import exceptions as sdk_exceptions -from osc_lib.command import command +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ -from openstackclient.network import common LOG = logging.getLogger(__name__) @@ -50,6 +51,7 @@ 'backups': 'backups', 'backup_gigabytes': 'backup-gigabytes', 'gigabytes': 'gigabytes', + 'groups': 'groups', 'per_volume_gigabytes': 'per-volume-gigabytes', 'snapshots': 'snapshots', 'volumes': 'volumes', @@ -61,13 +63,6 @@ 'volumes', ] -NOVA_NETWORK_QUOTAS = { - 'fixed_ips': 'fixed-ips', - 'floating_ips': 'floating-ips', - 'security_group_rules': 'secgroup-rules', - 'security_groups': 'secgroups', -} - NETWORK_QUOTAS = { 'floatingip': 'floating-ips', 'security_group_rule': 'secgroup-rules', @@ -76,6 +71,7 @@ 'subnet': 'subnets', 'port': 'ports', 'router': 'routers', + 'router_route': 'router-routes', 'rbac_policy': 'rbac-policies', 'subnetpool': 'subnetpools', } @@ -84,6 +80,7 @@ 'floating_ips', 'networks', 'rbac_policies', + 'router_routes', 'routers', 'ports', 'security_group_rules', @@ -93,9 +90,13 @@ ] -def _xform_get_quota(data, value, keys): - res = [] - res_info = {} +def _xform_get_quota( + data: Any, + value: str, + keys: Any, +) -> list[dict[str, Any]]: + res: list[dict[str, Any]] = [] + res_info: dict[str, Any] = {} for key in keys: res_info[key] = getattr(data, key, '') @@ -104,20 +105,21 @@ def _xform_get_quota(data, value, keys): return res -def get_project(app, project): +def get_project(app: Any, project: str | None) -> dict[str, str]: if project is not None: - identity_client = app.client_manager.sdk_connection.identity - project = identity_client.find_project(project, ignore_missing=False) - project_id = project.id - project_name = project.name - elif app.client_manager.auth_ref: - # Get the project from the current auth - project = app.client_manager.auth_ref - project_id = project.project_id - project_name = project.project_name + identity_client = sdk_utils.ensure_service_version( + app.client_manager.sdk_connection.identity, '3' + ) + found_project = identity_client.find_project( + project, ignore_missing=False + ) + project_id = found_project.id + project_name = found_project.name else: - project_id = None - project_name = None + # Get the project from the current auth + auth_ref = app.client_manager.auth_ref + project_id = auth_ref.project_id + project_name = auth_ref.project_name return { 'id': project_id, @@ -126,12 +128,12 @@ def get_project(app, project): def get_compute_quotas( - app, - project_id, + app: Any, + project_id: str | None, *, - detail=False, - default=False, -): + detail: bool = False, + default: bool = False, +) -> dict[str, Any]: try: client = app.client_manager.compute if default: @@ -140,7 +142,7 @@ def get_compute_quotas( quota = client.get_quota_set(project_id, usage=detail) except sdk_exceptions.EndpointNotFound: return {} - data = quota.to_dict() + data: dict[str, Any] = quota.to_dict() if not detail: del data['usage'] del data['reservation'] @@ -148,21 +150,21 @@ def get_compute_quotas( def get_volume_quotas( - app, - project_id, + app: Any, + project_id: str | None, *, - detail=False, - default=False, -): + detail: bool = False, + default: bool = False, +) -> dict[str, Any]: try: - client = app.client_manager.sdk_connection.volume + client = app.client_manager.volume if default: quota = client.get_quota_set_defaults(project_id) else: quota = client.get_quota_set(project_id, usage=detail) except sdk_exceptions.EndpointNotFound: return {} - data = quota.to_dict() + data: dict[str, Any] = quota.to_dict() if not detail: del data['usage'] del data['reservation'] @@ -170,17 +172,19 @@ def get_volume_quotas( def get_network_quotas( - app, - project_id, + app: Any, + project_id: str | None, *, - detail=False, - default=False, -): - def _network_quota_to_dict(network_quota, detail=False): - dict_quota = network_quota.to_dict(computed=False) + detail: bool = False, + default: bool = False, +) -> dict[str, Any]: + def _network_quota_to_dict( + network_quota: Any, detail: bool = False + ) -> dict[str, Any]: + data: dict[str, Any] = network_quota.to_dict(computed=False) if not detail: - return dict_quota + return data # Neutron returns quota details in dict which is in format like: # {'resource_name': {'in_use': X, 'limit': Y, 'reserved': Z}, @@ -200,8 +204,8 @@ def _network_quota_to_dict(network_quota, detail=False): # # so we need to make conversion to have data in same format from # all of the services - result: dict[str, ty.Any] = {"usage": {}, "reservation": {}} - for key, values in dict_quota.items(): + result: dict[str, Any] = {"usage": {}, "reservation": {}} + for key, values in data.items(): if values is None: continue if isinstance(values, dict): @@ -232,7 +236,7 @@ class ListQuota(command.Lister): inspected with 'openstack quota show --default'. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) option = parser.add_mutually_exclusive_group(required=True) option.add_argument( @@ -255,7 +259,11 @@ def get_parser(self, prog_name): ) return parser - def _list_quota_compute(self, parsed_args, project_ids): + def _list_quota_compute( + self, + parsed_args: argparse.Namespace, + project_ids: list[str], + ) -> tuple[tuple[str, ...], Any]: compute_client = self.app.client_manager.compute result = [] @@ -272,7 +280,10 @@ def _list_quota_compute(self, parsed_args, project_ids): sdk_exceptions.NotFoundException, ) as exc: # Project not found, move on to next one - LOG.warning(f"Project {project_id} not found: {exc}") + LOG.warning( + 'Project %(project_id)s not found: %(exc)s', + {'project_id': project_id, 'exc': exc}, + ) continue project_result = _xform_get_quota( @@ -322,8 +333,12 @@ def _list_quota_compute(self, parsed_args, project_ids): (utils.get_dict_properties(s, columns) for s in result), ) - def _list_quota_volume(self, parsed_args, project_ids): - volume_client = self.app.client_manager.sdk_connection.volume + def _list_quota_volume( + self, + parsed_args: argparse.Namespace, + project_ids: list[str], + ) -> tuple[tuple[str, ...], Any]: + volume_client = self.app.client_manager.volume result = [] for project_id in project_ids: @@ -334,7 +349,10 @@ def _list_quota_volume(self, parsed_args, project_ids): sdk_exceptions.NotFoundException, ) as exc: # Project not found, move on to next one - LOG.warning(f"Project {project_id} not found: {exc}") + LOG.warning( + 'Project %(project_id)s not found: %(exc)s', + {'project_id': project_id, 'exc': exc}, + ) continue project_result = _xform_get_quota( @@ -358,6 +376,7 @@ def _list_quota_volume(self, parsed_args, project_ids): 'backups', 'backup_gigabytes', 'gigabytes', + 'groups', 'per_volume_gigabytes', 'snapshots', 'volumes', @@ -367,6 +386,7 @@ def _list_quota_volume(self, parsed_args, project_ids): 'Backups', 'Backup Gigabytes', 'Gigabytes', + 'Groups', 'Per Volume Gigabytes', 'Snapshots', 'Volumes', @@ -377,7 +397,11 @@ def _list_quota_volume(self, parsed_args, project_ids): (utils.get_dict_properties(s, columns) for s in result), ) - def _list_quota_network(self, parsed_args, project_ids): + def _list_quota_network( + self, + parsed_args: argparse.Namespace, + project_ids: list[str], + ) -> tuple[tuple[str, ...], Any]: network_client = self.app.client_manager.network result = [] @@ -389,7 +413,10 @@ def _list_quota_network(self, parsed_args, project_ids): sdk_exceptions.ForbiddenException, ) as exc: # Project not found, move on to next one - LOG.warning(f"Project {project_id} not found: {exc}") + LOG.warning( + 'Project %(project_id)s not found: %(exc)s', + {'project_id': project_id, 'exc': exc}, + ) continue project_result = _xform_get_quota( @@ -414,6 +441,7 @@ def _list_quota_network(self, parsed_args, project_ids): 'networks', 'ports', 'rbac_policies', + 'router_routes', 'routers', 'security_groups', 'security_group_rules', @@ -426,6 +454,7 @@ def _list_quota_network(self, parsed_args, project_ids): 'Networks', 'Ports', 'RBAC Policies', + 'Router Routes', 'Routers', 'Security Groups', 'Security Group Rules', @@ -438,11 +467,14 @@ def _list_quota_network(self, parsed_args, project_ids): (utils.get_dict_properties(s, columns) for s in result), ) - def take_action(self, parsed_args): - project_ids = [ - p.id - for p in self.app.client_manager.sdk_connection.identity.projects() - ] + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + + project_ids = [p.id for p in identity_client.projects()] if parsed_args.compute: return self._list_quota_compute(parsed_args, project_ids) elif parsed_args.volume: @@ -454,44 +486,23 @@ def take_action(self, parsed_args): return ((), ()) -class SetQuota(common.NetDetectionMixin, command.Command): +class SetQuota(command.Command): _description = _("Set quotas for project or class") - def _build_options_list(self): + def _build_options_list(self) -> list[tuple[str, str, str]]: help_fmt = _('New value for the %s quota') - # Compute and volume quota options are always the same + # Compute volume and network quota options are always the same rets = [ (k, v, help_fmt % v) for k, v in itertools.chain( COMPUTE_QUOTAS.items(), VOLUME_QUOTAS.items(), + NETWORK_QUOTAS.items(), ) ] - # For docs build, we want to produce helps for both neutron and - # nova-network options. They overlap, so we have to figure out which - # need to be tagged as specific to one network type or the other. - if self.is_docs_build: - # NOTE(efried): This takes advantage of the fact that we know the - # nova-net options are a subset of the neutron options. If that - # ever changes, this algorithm will need to be adjusted accordingly - inv_compute = set(NOVA_NETWORK_QUOTAS.values()) - for k, v in NETWORK_QUOTAS.items(): - _help = help_fmt % v - if v not in inv_compute: - # This one is unique to neutron - _help = self.enhance_help_neutron(_help) - rets.append((k, v, _help)) - elif self.is_neutron: - rets.extend( - [(k, v, help_fmt % v) for k, v in NETWORK_QUOTAS.items()] - ) - elif self.is_nova_network: - rets.extend( - [(k, v, help_fmt % v) for k, v in NOVA_NETWORK_QUOTAS.items()] - ) return rets - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'project', @@ -566,7 +577,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: if parsed_args.quota_class: msg = _( "The '--class' option has been deprecated. Quota classes were " @@ -586,6 +597,9 @@ def take_action(self, parsed_args): compute_kwargs = {} volume_kwargs = {} network_kwargs = {} + compute_client = None + volume_client = None + network_client = None if self.app.client_manager.is_compute_endpoint_enabled(): compute_client = self.app.client_manager.compute @@ -599,7 +613,7 @@ def take_action(self, parsed_args): compute_kwargs['force'] = parsed_args.force if self.app.client_manager.is_volume_endpoint_enabled(): - volume_client = self.app.client_manager.sdk_connection.volume + volume_client = self.app.client_manager.volume for k, v in VOLUME_QUOTAS.items(): value = getattr(parsed_args, k, None) @@ -618,11 +632,6 @@ def take_action(self, parsed_args): value = getattr(parsed_args, k, None) if value is not None: network_kwargs[k] = value - elif self.app.client_manager.is_compute_endpoint_enabled(): - for k, v in NOVA_NETWORK_QUOTAS.items(): - value = getattr(parsed_args, k, None) - if value is not None: - compute_kwargs[k] = value if network_kwargs: if parsed_args.force is True: @@ -634,12 +643,12 @@ def take_action(self, parsed_args): network_kwargs['check_limit'] = True if parsed_args.quota_class or parsed_args.default: - if compute_kwargs: + if compute_kwargs and compute_client: compute_client.update_quota_class_set( parsed_args.project or 'default', **compute_kwargs, ) - if volume_kwargs: + if volume_kwargs and volume_client: volume_client.update_quota_class_set( parsed_args.project or 'default', **volume_kwargs, @@ -655,11 +664,11 @@ def take_action(self, parsed_args): project_info = get_project(self.app, parsed_args.project) project = project_info['id'] - if compute_kwargs: + if compute_kwargs and compute_client: compute_client.update_quota_set(project, **compute_kwargs) - if volume_kwargs: + if volume_kwargs and volume_client: volume_client.update_quota_set(project, **volume_kwargs) - if network_kwargs: + if network_kwargs and network_client: network_client.update_quota(project, **network_kwargs) @@ -671,7 +680,7 @@ class ShowQuota(command.Lister): and ``server-group-members`` output for a given quota class.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'project', @@ -733,7 +742,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: project_info = get_project(self.app, parsed_args.project) project = project_info['id'] @@ -746,21 +757,32 @@ def take_action(self, parsed_args): # values if the project or class does not exist. This is expected # behavior. However, we have already checked for the presence of the # project above so it shouldn't be an issue. - if parsed_args.service in {'all', 'compute'}: + if parsed_args.service == 'compute' or ( + parsed_args.service == 'all' + and self.app.client_manager.is_compute_endpoint_enabled() + ): compute_quota_info = get_compute_quotas( self.app, project, detail=parsed_args.usage, default=parsed_args.default, ) - if parsed_args.service in {'all', 'volume'}: + + if parsed_args.service == 'volume' or ( + parsed_args.service == 'all' + and self.app.client_manager.is_volume_endpoint_enabled() + ): volume_quota_info = get_volume_quotas( self.app, project, detail=parsed_args.usage, default=parsed_args.default, ) - if parsed_args.service in {'all', 'network'}: + + if parsed_args.service == 'network' or ( + parsed_args.service == 'all' + and self.app.client_manager.is_network_endpoint_enabled() + ): network_quota_info = get_network_quotas( self.app, project, @@ -782,33 +804,39 @@ def take_action(self, parsed_args): info["usage"].update(volume_quota_info.pop("usage", {})) info["usage"].update(network_quota_info.pop("usage", {})) + # Keypair usage is tracked per-user rather than per-project. + # To avoid mislead return N/A + info["usage"]["key_pairs"] = "N/A" + info.update(compute_quota_info) info.update(volume_quota_info) info.update(network_quota_info) - # Map the internal quota names to the external ones - # COMPUTE_QUOTAS and NETWORK_QUOTAS share floating-ips, - # secgroup-rules and secgroups as dict value, so when - # neutron is enabled, quotas of these three resources - # in nova will be replaced by neutron's. - for k, v in itertools.chain( - COMPUTE_QUOTAS.items(), - NOVA_NETWORK_QUOTAS.items(), - VOLUME_QUOTAS.items(), - NETWORK_QUOTAS.items(), - ): - if not k == v and info.get(k) is not None: - info[v] = info[k] - info.pop(k) + def _normalize_names(section: dict[str, Any]) -> None: + # Map the internal quota names to the external ones + # COMPUTE_QUOTAS and NETWORK_QUOTAS share floating-ips, + # secgroup-rules and secgroups as dict value, so when + # neutron is enabled, quotas of these three resources + # in nova will be replaced by neutron's. + for k, v in itertools.chain( + COMPUTE_QUOTAS.items(), + VOLUME_QUOTAS.items(), + NETWORK_QUOTAS.items(), + ): + if not k == v and section.get(k) is not None: + section[v] = section.pop(k) + + _normalize_names(info) + if parsed_args.usage: + _normalize_names(info["reservation"]) + _normalize_names(info["usage"]) # Remove the 'id' field since it's not very useful - if 'id' in info: - del info['id'] + info.pop('id', None) # Remove the sdk-derived fields for field in ('location', 'name', 'force'): - if field in info: - del info[field] + info.pop(field, None) if not parsed_args.usage: result = [{'resource': k, 'limit': v} for k, v in info.items()] @@ -854,7 +882,7 @@ class DeleteQuota(command.Command): "Delete configured quota for a project and revert to defaults." ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'project', @@ -899,20 +927,29 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + project = identity_client.find_project( parsed_args.project, ignore_missing=False ) # compute quotas - if parsed_args.service in {'all', 'compute'}: + if parsed_args.service == 'compute' or ( + parsed_args.service == 'all' + and self.app.client_manager.is_compute_endpoint_enabled() + ): compute_client = self.app.client_manager.compute compute_client.revert_quota_set(project.id) # volume quotas - if parsed_args.service in {'all', 'volume'}: - volume_client = self.app.client_manager.sdk_connection.volume + if parsed_args.service == 'volume' or ( + parsed_args.service == 'all' + and self.app.client_manager.is_volume_endpoint_enabled() + ): + volume_client = self.app.client_manager.volume volume_client.revert_quota_set(project.id) # network quotas (but only if we're not using nova-network, otherwise diff --git a/openstackclient/common/versions.py b/openstackclient/common/versions.py index 662233875f..5fb39e1168 100644 --- a/openstackclient/common/versions.py +++ b/openstackclient/common/versions.py @@ -14,15 +14,18 @@ """Versions Action Implementation""" -from osc_lib.command import command +import argparse +from collections.abc import Iterable, Sequence +from typing import Any +from openstackclient import command from openstackclient.i18n import _ class ShowVersions(command.Lister): _description = _("Show available versions of services") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) interface_group = parser.add_mutually_exclusive_group() interface_group.add_argument( @@ -68,7 +71,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: interface = parsed_args.interface if parsed_args.is_all_interfaces: interface = None diff --git a/openstackclient/compute/client.py b/openstackclient/compute/client.py index 73ce2f87d5..4d7a6f82f2 100644 --- a/openstackclient/compute/client.py +++ b/openstackclient/compute/client.py @@ -12,7 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import logging +from typing import Any from osc_lib import utils @@ -27,7 +29,7 @@ API_VERSIONS = ('2', '2.1') -def make_client(instance): +def make_client(instance: Any) -> Any: """Returns a compute service client.""" LOG.debug( 'Compute client initialized using OpenStack SDK: %s', @@ -36,7 +38,9 @@ def make_client(instance): return instance.sdk_connection.compute -def build_option_parser(parser): +def build_option_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: """Hook to add global options""" parser.add_argument( '--os-compute-api-version', @@ -48,6 +52,6 @@ def build_option_parser(parser): return parser -def check_api_version(check_version): +def check_api_version(check_version: str) -> bool: # SDK supports auto-negotiation for us: always return True return True diff --git a/openstackclient/compute/v2/agent.py b/openstackclient/compute/v2/agent.py index 5bc6603f40..3582a59b0f 100644 --- a/openstackclient/compute/v2/agent.py +++ b/openstackclient/compute/v2/agent.py @@ -11,17 +11,19 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# """Agent action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from openstack import exceptions as sdk_exceptions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -36,7 +38,7 @@ class CreateAgent(command.ShowOne): 23.0.0 (Wallaby) release. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument("os", metavar="", help=_("Type of OS")) parser.add_argument( @@ -55,7 +57,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute # doing this since openstacksdk has decided not to support this @@ -76,7 +80,8 @@ def take_action(self, parsed_args): sdk_exceptions.raise_from_response(response) agent = response.json().get('agent') - return zip(*sorted(agent.items())) + col_headers, col_data = zip(*sorted(agent.items())) + return col_headers, col_data class DeleteAgent(command.Command): @@ -87,14 +92,14 @@ class DeleteAgent(command.Command): 23.0.0 (Wallaby) release. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "id", metavar="", nargs='+', help=_("ID of agent(s) to delete") ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute result = 0 for id in parsed_args.id: @@ -129,7 +134,7 @@ class ListAgent(command.Lister): 23.0.0 (Wallaby) release. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--hypervisor", @@ -138,7 +143,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute columns = ( "Agent ID", @@ -171,7 +178,7 @@ class SetAgent(command.Command): 23.0.0 (Wallaby) release. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "id", @@ -193,7 +200,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute response = compute_client.get('/os-agents', microversion='2.1') diff --git a/openstackclient/compute/v2/aggregate.py b/openstackclient/compute/v2/aggregate.py index e649607218..71aff69746 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -16,23 +16,26 @@ """Compute v2 Aggregate action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging -import typing as ty +from typing import Any +from cliff import columns from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -_aggregate_formatters = { +_aggregate_formatters: dict[str, type[columns.FormattableColumn[Any]]] = { 'Hosts': format_columns.ListColumn, 'Metadata': format_columns.DictColumn, 'hosts': format_columns.ListColumn, @@ -40,7 +43,9 @@ } -def _get_aggregate_columns(item): +def _get_aggregate_columns( + item: Any, +) -> tuple[tuple[str, ...], tuple[str, ...]]: # To maintain backwards compatibility we need to rename sdk props to # whatever OSC was using before column_map = { @@ -55,7 +60,7 @@ def _get_aggregate_columns(item): class AddAggregateHost(command.ShowOne): _description = _("Add host to aggregate") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'aggregate', @@ -67,7 +72,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute aggregate = compute_client.find_aggregate( @@ -88,7 +95,7 @@ def take_action(self, parsed_args): class CreateAggregate(command.ShowOne): _description = _("Create a new aggregate") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "name", metavar="", help=_("New aggregate name") @@ -110,7 +117,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute attrs = {'name': parsed_args.name} @@ -136,7 +145,7 @@ def take_action(self, parsed_args): class DeleteAggregate(command.Command): _description = _("Delete existing aggregate(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'aggregate', @@ -146,7 +155,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute result = 0 for a in parsed_args.aggregate: @@ -179,7 +188,7 @@ def take_action(self, parsed_args): class ListAggregate(command.Lister): _description = _("List all aggregates") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -187,13 +196,44 @@ def get_parser(self, prog_name): default=False, help=_("List additional fields in output"), ) + parser.add_argument( + '--availability-zone', + metavar='', + default=None, + help=_("Filter by availability zone name"), + ) + parser.add_argument( + '--host', + metavar='', + default=None, + help=_("Filter by aggregates containing this host"), + ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute aggregates = list(compute_client.aggregates()) + # NOTE: The compute API does not support server-side filtering of + # aggregates, so we filter client-side. When more than one filter is + # given they are combined with AND (results must match all filters). + if parsed_args.availability_zone is not None: + aggregates = [ + aggregate + for aggregate in aggregates + if aggregate.availability_zone == parsed_args.availability_zone + ] + + if parsed_args.host is not None: + aggregates = [ + aggregate + for aggregate in aggregates + if aggregate.hosts and parsed_args.host in aggregate.hosts + ] + if sdk_utils.supports_microversion(compute_client, '2.41'): column_headers: tuple[str, ...] = ("ID", "UUID") columns: tuple[str, ...] = ("id", "uuid") @@ -239,7 +279,7 @@ def take_action(self, parsed_args): class RemoveAggregateHost(command.ShowOne): _description = _("Remove host from aggregate") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'aggregate', @@ -251,7 +291,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute aggregate = compute_client.find_aggregate( @@ -272,7 +314,7 @@ def take_action(self, parsed_args): class SetAggregate(command.Command): _description = _("Set aggregate properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'aggregate', @@ -308,7 +350,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute aggregate = compute_client.find_aggregate( parsed_args.aggregate, ignore_missing=False @@ -322,7 +364,7 @@ def take_action(self, parsed_args): if kwargs: compute_client.update_aggregate(aggregate.id, **kwargs) - properties: dict[str, ty.Any] = {} + properties: dict[str, Any] = {} if parsed_args.no_property: # NOTE(RuiChen): "availability_zone" can not be unset from # properties. It is already excluded from show and create output. @@ -344,7 +386,7 @@ def take_action(self, parsed_args): class ShowAggregate(command.ShowOne): _description = _("Display aggregate details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'aggregate', @@ -353,7 +395,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute aggregate = compute_client.find_aggregate( parsed_args.aggregate, ignore_missing=False @@ -373,7 +417,7 @@ def take_action(self, parsed_args): class UnsetAggregate(command.Command): _description = _("Unset aggregate properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "aggregate", @@ -393,7 +437,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute aggregate = compute_client.find_aggregate( parsed_args.aggregate, ignore_missing=False @@ -410,7 +454,7 @@ class CacheImageForAggregate(command.Command): # NOTE(gtema): According to stephenfin and dansmith there is no and will # not be anything to return. - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'aggregate', @@ -428,7 +472,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute if not sdk_utils.supports_microversion(compute_client, '2.81'): @@ -438,15 +482,15 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) + image_client = self.app.client_manager.sdk_connection.image + aggregate = compute_client.find_aggregate( parsed_args.aggregate, ignore_missing=False ) images = [] for img in parsed_args.image: - image = self.app.client_manager.sdk_connection.image.find_image( - img, ignore_missing=False - ) + image = image_client.find_image(img, ignore_missing=False) images.append(image.id) compute_client.aggregate_precache_images(aggregate.id, images) diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index bcabcd2d88..4fb09a5600 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -15,14 +15,18 @@ """Compute v2 Console action implementations""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ -def _get_console_columns(item): +def _get_console_columns(item: Any) -> tuple[tuple[str, ...], tuple[str, ...]]: # To maintain backwards compatibility we need to rename sdk props to # whatever OSC was using before hidden_columns = ['id', 'links', 'location', 'name'] @@ -34,7 +38,7 @@ def _get_console_columns(item): class ShowConsoleLog(command.Command): _description = _("Show server's console output") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -54,7 +58,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( @@ -64,19 +68,21 @@ def take_action(self, parsed_args): output = compute_client.get_server_console_output( server.id, length=parsed_args.lines ) - data = None + data: str | None = None if output: data = output.get('output', None) if data and data[-1] != '\n': data += '\n' - self.app.stdout.write(data) + + if data: + self.app.stdout.write(data) class ShowConsoleURL(command.ShowOne): _description = _("Show server's remote console URL") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -136,17 +142,21 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute server = compute_client.find_server( parsed_args.server, ignore_missing=False ) - data = compute_client.create_console( + console = compute_client.create_console( server.id, console_type=parsed_args.url_type ) - display_columns, columns = _get_console_columns(data) - data = utils.get_dict_properties(data, columns) + display_columns, columns = _get_console_columns(console) + # this is a bug in SDK + # https://review.opendev.org/c/openstack/openstacksdk/+/992548 + data = utils.get_dict_properties(console, columns) # type: ignore[arg-type] return (display_columns, data) diff --git a/openstackclient/compute/v2/console_connection.py b/openstackclient/compute/v2/console_connection.py index d90b2ceb9c..f7cbcc6364 100644 --- a/openstackclient/compute/v2/console_connection.py +++ b/openstackclient/compute/v2/console_connection.py @@ -13,13 +13,19 @@ """Compute v2 Console auth token implementations.""" -from osc_lib.command import command +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ -def _get_console_connection_columns(item): +def _get_console_connection_columns( + item: Any, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map: dict[str, str] = {} hidden_columns = ['id', 'location', 'name'] return utils.get_osc_show_columns_for_sdk_resource( @@ -30,7 +36,7 @@ def _get_console_connection_columns(item): class ShowConsoleConnectionInformation(command.ShowOne): _description = _("Show server's remote console connection information") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'token', @@ -39,10 +45,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute - data = compute_client.validate_console_auth_token(parsed_args.token) - display_columns, columns = _get_console_connection_columns(data) - data = utils.get_dict_properties(data, columns) + auth_token = compute_client.validate_console_auth_token( + parsed_args.token + ) + display_columns, columns = _get_console_connection_columns(auth_token) + data = utils.get_dict_properties(auth_token, columns) return (display_columns, data) diff --git a/openstackclient/compute/v2/flavor.py b/openstackclient/compute/v2/flavor.py index 76635e12b0..3011ac1ab5 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -15,16 +15,19 @@ """Flavor action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from openstack import exceptions as sdk_exceptions from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -39,7 +42,7 @@ } -def _get_flavor_columns(item): +def _get_flavor_columns(item: Any) -> tuple[tuple[str, ...], tuple[str, ...]]: # To maintain backwards compatibility we need to rename sdk props to # whatever OSC was using before column_map = { @@ -57,7 +60,7 @@ def _get_flavor_columns(item): class CreateFlavor(command.ShowOne): _description = _("Create new flavor") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "name", metavar="", help=_("New flavor name") @@ -102,8 +105,7 @@ def get_parser(self, prog_name): "--rxtx-factor", type=float, metavar="", - default=1.0, - help=_("RX/TX factor (default 1.0)"), + help=_("RX/TX factor"), ) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( @@ -148,26 +150,52 @@ def get_parser(self, prog_name): identity_common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute - identity_client = self.app.client_manager.identity + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) if parsed_args.project and parsed_args.public: msg = _("--project is only allowed with --private") raise exceptions.CommandError(msg) + flavor_id = parsed_args.id + if parsed_args.id == 'auto': + # novaclient aliased 'auto' to mean "generate a UUID for me": we + # do the same to avoid breaking existing users + flavor_id = None + + msg = _( + "Passing '--id auto' is deprecated. Nova will automatically " + "assign a UUID-like ID if no ID is provided. Omit the '--id' " + "parameter instead." + ) + self.log.warning(msg) + args = { 'name': parsed_args.name, 'ram': parsed_args.ram, 'vcpus': parsed_args.vcpus, 'disk': parsed_args.disk, - 'id': parsed_args.id, + 'id': flavor_id, 'ephemeral': parsed_args.ephemeral, 'swap': parsed_args.swap, - 'rxtx_factor': parsed_args.rxtx_factor, 'is_public': parsed_args.public, } + if parsed_args.rxtx_factor: + if sdk_utils.supports_microversion(compute_client, '2.102'): + msg = _( + 'The --rxtx-factor parameter is only supported until ' + 'API microversion 2.101' + ) + raise exceptions.CommandError(msg) + + args['rxtx_factor'] = parsed_args.rxtx_factor + if parsed_args.description: if not sdk_utils.supports_microversion(compute_client, '2.55'): msg = _( @@ -182,17 +210,18 @@ def take_action(self, parsed_args): if parsed_args.project: try: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) compute_client.flavor_add_tenant_access(flavor.id, project_id) except Exception as e: msg = _( "Failed to add project %(project)s access to flavor: %(e)s" ) LOG.error(msg, {'project': parsed_args.project, 'e': e}) + if parsed_args.properties: try: flavor = compute_client.create_flavor_extra_specs( @@ -212,7 +241,7 @@ def take_action(self, parsed_args): class DeleteFlavor(command.Command): _description = _("Delete flavor(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "flavor", @@ -222,7 +251,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute result = 0 for f in parsed_args.flavor: @@ -251,7 +280,7 @@ def take_action(self, parsed_args): class ListFlavor(command.Lister): _description = _("List flavors") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( @@ -295,7 +324,9 @@ def get_parser(self, prog_name): pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute # is_public is ternary - None means give all flavors, # True is public only and False is private only @@ -310,6 +341,8 @@ def take_action(self, parsed_args): if parsed_args.limit: query_attrs['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + query_attrs['max_items'] = parsed_args.max_items if parsed_args.limit or parsed_args.marker: # User passed explicit pagination request, switch off SDK @@ -322,7 +355,7 @@ def take_action(self, parsed_args): if parsed_args.min_ram: query_attrs['min_ram'] = parsed_args.min_ram - data = list(compute_client.flavors(**query_attrs)) + data = list(compute_client.flavors(**query_attrs)) # type: ignore[arg-type] # Even if server supports 2.61 some policy might stop it sending us # extra_specs. So try to fetch them if they are absent for f in data: @@ -373,7 +406,7 @@ def take_action(self, parsed_args): class SetFlavor(command.Command): _description = _("Set flavor properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "flavor", @@ -417,9 +450,11 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute - identity_client = self.app.client_manager.identity + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) try: flavor = compute_client.find_flavor( @@ -465,15 +500,13 @@ def take_action(self, parsed_args): if flavor.is_public: msg = _("Cannot set access for a public flavor") raise exceptions.CommandError(msg) - else: - project_id = identity_common.find_project( - identity_client, - parsed_args.project, - parsed_args.project_domain, - ).id - compute_client.flavor_add_tenant_access( - flavor.id, project_id - ) + + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + compute_client.flavor_add_tenant_access(flavor.id, project_id) except Exception as e: LOG.error(_("Failed to set flavor access to project: %s"), e) result += 1 @@ -487,7 +520,7 @@ def take_action(self, parsed_args): class ShowFlavor(command.ShowOne): _description = _("Display flavor details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "flavor", @@ -496,7 +529,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute flavor = compute_client.find_flavor( parsed_args.flavor, get_extra_specs=True, ignore_missing=False @@ -522,21 +557,21 @@ def take_action(self, parsed_args): # Since we need to inject "access_project_id" into resource - convert # it to dict and treat it respectively - flavor = flavor.to_dict() - flavor['access_project_ids'] = access_projects + flavor_dict = flavor.to_dict() + flavor_dict['access_project_ids'] = access_projects - display_columns, columns = _get_flavor_columns(flavor) + display_columns, columns = _get_flavor_columns(flavor_dict) data = utils.get_dict_properties( - flavor, columns, formatters=_formatters + flavor_dict, columns, formatters=_formatters ) - return (display_columns, data) + return display_columns, data class UnsetFlavor(command.Command): _description = _("Unset flavor properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "flavor", @@ -564,9 +599,11 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute - identity_client = self.app.client_manager.identity + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) try: flavor = compute_client.find_flavor( @@ -592,11 +629,11 @@ def take_action(self, parsed_args): msg = _("Cannot remove access for a public flavor") raise exceptions.CommandError(msg) - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) compute_client.flavor_remove_tenant_access( flavor.id, project_id ) diff --git a/openstackclient/compute/v2/host.py b/openstackclient/compute/v2/host.py index aa1d1a5eba..98bf75efed 100644 --- a/openstackclient/compute/v2/host.py +++ b/openstackclient/compute/v2/host.py @@ -15,17 +15,21 @@ """Host action implementations""" +import argparse +from collections.abc import Iterable +from typing import Any + from openstack import exceptions as sdk_exceptions -from osc_lib.command import command from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ class ListHost(command.Lister): _description = _("DEPRECATED: List hosts") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--zone", @@ -34,7 +38,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute self.log.warning( @@ -57,7 +63,7 @@ def take_action(self, parsed_args): class SetHost(command.Command): _description = _("DEPRECATED: Set host properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "host", metavar="", help=_("Host to modify (name only)") @@ -82,7 +88,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute self.log.warning( @@ -115,12 +121,14 @@ def take_action(self, parsed_args): class ShowHost(command.Lister): _description = _("DEPRECATED: Display host details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument("host", metavar="", help=_("Name of host")) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute self.log.warning( diff --git a/openstackclient/compute/v2/hypervisor.py b/openstackclient/compute/v2/hypervisor.py index 0a6cc5326e..9f12df8a07 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -15,21 +15,26 @@ """Hypervisor action implementations""" +import argparse +from collections.abc import Iterable, Sequence import json import re +from typing import Any from openstack import exceptions as sdk_exceptions from openstack import utils as sdk_utils from osc_lib.cli import format_columns -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ -def _get_hypervisor_columns(item, client): +def _get_hypervisor_columns( + item: Any, client: Any +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = {'name': 'hypervisor_hostname'} hidden_columns = ['location', 'servers'] @@ -70,7 +75,7 @@ def _get_hypervisor_columns(item, client): class ListHypervisor(command.Lister): _description = _("List hypervisors") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--matching', @@ -89,7 +94,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute list_opts = {} @@ -115,6 +122,8 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) list_opts['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + list_opts['max_items'] = parsed_args.max_items if parsed_args.matching: list_opts['hypervisor_hostname_pattern'] = parsed_args.matching @@ -160,7 +169,7 @@ def take_action(self, parsed_args): class ShowHypervisor(command.ShowOne): _description = _("Display hypervisor details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "hypervisor", @@ -169,7 +178,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute hypervisor_id = compute_client.find_hypervisor( diff --git a/openstackclient/compute/v2/hypervisor_stats.py b/openstackclient/compute/v2/hypervisor_stats.py index 3b9a68614d..6c0213502a 100644 --- a/openstackclient/compute/v2/hypervisor_stats.py +++ b/openstackclient/compute/v2/hypervisor_stats.py @@ -13,13 +13,19 @@ """Hypervisor Stats action implementations""" -from osc_lib.command import command +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ -def _get_hypervisor_stat_columns(item): +def _get_hypervisor_stat_columns( + item: Any, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { # NOTE(gtema): If we decide to use SDK names - empty this 'disk_available': 'disk_available_least', @@ -39,7 +45,9 @@ def _get_hypervisor_stat_columns(item): class ShowHypervisorStats(command.ShowOne): _description = _("Display hypervisor stats details") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: # The command is deprecated since it is being dropped in Nova. self.log.warning(_("This command is deprecated.")) compute_client = self.app.client_manager.compute diff --git a/openstackclient/compute/v2/keypair.py b/openstackclient/compute/v2/keypair.py index 97dd04f4e7..5bb51b365c 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -11,21 +11,25 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# """Keypair action implementations""" +import argparse import collections +from collections.abc import Iterable, Sequence import logging import os +from typing import Any from cryptography.hazmat.primitives.asymmetric import ed25519 from cryptography.hazmat.primitives import serialization +from openstack.compute.v2 import keypair as _keypair +from openstack import resource from openstack import utils as sdk_utils -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -35,7 +39,7 @@ Keypair = collections.namedtuple('Keypair', 'private_key public_key') -def _generate_keypair(): +def _generate_keypair() -> Keypair: """Generate a Ed25519 keypair in OpenSSH format. :returns: A `Keypair` named tuple with the generated private and public @@ -58,7 +62,11 @@ def _generate_keypair(): return Keypair(private_key, public_key) -def _get_keypair_columns(item, hide_pub_key=False, hide_priv_key=False): +def _get_keypair_columns( + item: resource.Resource, + hide_pub_key: bool = False, + hide_priv_key: bool = False, +) -> tuple[tuple[str, ...], tuple[str, ...]]: # To maintain backwards compatibility we need to rename sdk props to # whatever OSC was using before hidden_columns = ['links', 'location'] @@ -74,7 +82,7 @@ def _get_keypair_columns(item, hide_pub_key=False, hide_priv_key=False): class CreateKeypair(command.ShowOne): _description = _("Create new public or private key for server ssh access") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar='', help=_("New public or private key name") @@ -119,9 +127,13 @@ def get_parser(self, prog_name): identity_common.add_user_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute - identity_client = self.app.client_manager.identity + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {'name': parsed_args.name} @@ -182,11 +194,11 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - kwargs['user_id'] = identity_common.find_user( + kwargs['user_id'] = identity_common.find_user_id_sdk( identity_client, parsed_args.user, parsed_args.user_domain, - ).id + ) keypair = compute_client.create_keypair(**kwargs) @@ -202,13 +214,13 @@ def take_action(self, parsed_args): return (display_columns, data) else: self.app.stdout.write(generated_keypair.private_key) - return ({}, {}) + return ((), ()) class DeleteKeypair(command.Command): _description = _("Delete public or private key(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -227,9 +239,11 @@ def get_parser(self, prog_name): identity_common.add_user_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> Any: compute_client = self.app.client_manager.compute - identity_client = self.app.client_manager.identity + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {} result = 0 @@ -242,11 +256,11 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - kwargs['user_id'] = identity_common.find_user( + kwargs['user_id'] = identity_common.find_user_id_sdk( identity_client, parsed_args.user, parsed_args.user_domain, - ).id + ) for n in parsed_args.name: try: @@ -272,7 +286,7 @@ def take_action(self, parsed_args): class ListKeypair(command.Lister): _description = _("List key fingerprints") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) user_group = parser.add_mutually_exclusive_group() user_group.add_argument( @@ -297,10 +311,13 @@ def get_parser(self, prog_name): pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute - identity_client = self.app.client_manager.identity - identity_sdk_client = self.app.client_manager.sdk_connection.identity + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {} @@ -323,7 +340,10 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) kwargs['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + kwargs['max_items'] = parsed_args.max_items + data: list[_keypair.Keypair] if parsed_args.project: if not sdk_utils.supports_microversion(compute_client, '2.10'): msg = _( @@ -341,12 +361,12 @@ def take_action(self, parsed_args): # NOTE(stephenfin): This is done client side because nova doesn't # currently support doing so server-side. If this is slow, we can # think about spinning up a threadpool or similar. - project = identity_common.find_project( + project = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id - assignments = identity_sdk_client.role_assignments( + ) + assignments = identity_client.role_assignments( scope_project_id=project ) user_ids = set() @@ -366,16 +386,16 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - user = identity_common.find_user( + user_id = identity_common.find_user_id_sdk( identity_client, parsed_args.user, parsed_args.user_domain, ) - kwargs['user_id'] = user.id + kwargs['user_id'] = user_id - data = compute_client.keypairs(**kwargs) + data = list(compute_client.keypairs(**kwargs)) else: - data = compute_client.keypairs(**kwargs) + data = list(compute_client.keypairs(**kwargs)) columns: tuple[str, ...] = ("Name", "Fingerprint") @@ -391,7 +411,7 @@ def take_action(self, parsed_args): class ShowKeypair(command.ShowOne): _description = _("Display key details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -415,9 +435,13 @@ def get_parser(self, prog_name): identity_common.add_user_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute - identity_client = self.app.client_manager.identity + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {} @@ -429,11 +453,11 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - kwargs['user_id'] = identity_common.find_user( + kwargs['user_id'] = identity_common.find_user_id_sdk( identity_client, parsed_args.user, parsed_args.user_domain, - ).id + ) keypair = compute_client.find_keypair( parsed_args.name, **kwargs, ignore_missing=False @@ -447,4 +471,4 @@ def take_action(self, parsed_args): return (display_columns, data) else: self.app.stdout.write(keypair.public_key) - return ({}, {}) + return ((), ()) diff --git a/openstackclient/compute/v2/server.py b/openstackclient/compute/v2/server.py index 524eae5490..a076af5108 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -17,10 +17,12 @@ import argparse import base64 +from collections.abc import Iterable, Sequence import getpass import json import logging import os +from typing import Any from cliff import columns as cliff_columns import iso8601 @@ -28,23 +30,22 @@ from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils from openstackclient.api import compute_v2 +from openstackclient import command from openstackclient.common import envvars from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common -from openstackclient.network import common as network_common LOG = logging.getLogger(__name__) IMAGE_STRING_FOR_BFV = 'N/A (booted from volume)' -class PowerStateColumn(cliff_columns.FormattableColumn): +class PowerStateColumn(cliff_columns.FormattableColumn[int]): """Generate a formatted string of a server's power state.""" power_states = [ @@ -58,45 +59,50 @@ class PowerStateColumn(cliff_columns.FormattableColumn): 'Suspended', # 0x07 ] - def human_readable(self): + def human_readable(self) -> str: try: return self.power_states[self._value] except Exception: return 'N/A' -class AddressesColumn(cliff_columns.FormattableColumn): +class AddressesColumn(cliff_columns.FormattableColumn[Any]): """Generate a formatted string of a server's addresses.""" - def human_readable(self): + def human_readable(self) -> str: try: - return utils.format_dict_of_list( - { - k: [i['addr'] for i in v if 'addr' in i] - for k, v in self._value.items() - } + return ( + utils.format_dict_of_list( + { + k: [i['addr'] for i in v if 'addr' in i] + for k, v in self._value.items() + } + ) + or '' ) except Exception: return 'N/A' - def machine_readable(self): + def machine_readable(self) -> Any: return { k: [i['addr'] for i in v if 'addr' in i] for k, v in (self._value.items() if self._value else []) } -class HostColumn(cliff_columns.FormattableColumn): +class HostColumn(cliff_columns.FormattableColumn[str | None]): """Generate a formatted string of a hostname.""" - def human_readable(self): + def human_readable(self) -> str: if self._value is None: return '' return self._value -def _get_ip_address(addresses, address_type, ip_address_family): +def _get_ip_address( + addresses: Any, address_type: str, ip_address_family: list[int] +) -> Any: # Old style addresses if address_type in addresses: for addy in addresses[address_type]: @@ -129,7 +135,13 @@ def _get_ip_address(addresses, address_type, ip_address_family): ) -def _prep_server_detail(compute_client, image_client, server, *, refresh=True): +def _prep_server_detail( + compute_client: Any, + image_client: Any, + server: Any, + *, + refresh: bool = True, +) -> dict[str, Any]: """Prepare the detailed server dict for printing :param compute_client: a compute client instance @@ -183,9 +195,17 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True): 'updated_at': 'updated', 'user_data': 'OS-EXT-SRV-ATTR:user_data', 'vm_state': 'OS-EXT-STS:vm_state', - 'pinned_availability_zone': 'pinned_availability_zone', - 'scheduler_hints': 'scheduler_hints', } + # NOTE(ratailor): microversion 2.96 introduces + # pinned_availability_zone support + if sdk_utils.supports_microversion(compute_client, '2.96'): + column_map['pinned_availability_zone'] = 'pinned_availability_zone' + + # NOTE(ratailor): microversion 2.100 introduces + # scheduler_hints support + if sdk_utils.supports_microversion(compute_client, '2.100'): + column_map['scheduler_hints'] = 'scheduler_hints' + # Some columns returned by openstacksdk should not be shown because they're # either irrelevant or duplicates ignored_columns = { @@ -240,6 +260,11 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True): if not sdk_utils.supports_microversion(compute_client, '2.100'): info.pop('scheduler_hints', None) + # NOTE(ratailor): microversion 2.96 introduces + # pinned_availability_zone support + if not sdk_utils.supports_microversion(compute_client, '2.96'): + info.pop('pinned_availability_zone', None) + # Convert the image blob to a name image_info = info.get('image', {}) if image_info and any(image_info.values()): @@ -326,10 +351,11 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True): info['OS-EXT-STS:power_state'] ) - if 'scheduler_hints' in info: - info['scheduler_hints'] = format_columns.DictListColumn( - info.pop('scheduler_hints', {}), - ) + if sdk_utils.supports_microversion(compute_client, '2.100'): + if 'scheduler_hints' in info: + info['scheduler_hints'] = format_columns.DictListColumn( + info.pop('scheduler_hints', {}), + ) return info @@ -337,7 +363,7 @@ def _prep_server_detail(compute_client, image_client, server, *, refresh=True): class AddFixedIP(command.ShowOne): _description = _("Add fixed IP address to server") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "server", @@ -366,7 +392,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute server = compute_client.find_server( parsed_args.server, ignore_missing=False @@ -388,7 +416,7 @@ def take_action(self, parsed_args): else: net_id = parsed_args.network - kwargs = {'net_id': net_id} + kwargs: dict[str, Any] = {'net_id': net_id} if parsed_args.fixed_ip_address: kwargs['fixed_ips'] = [ {"ip_address": parsed_args.fixed_ip_address} @@ -432,10 +460,11 @@ def take_action(self, parsed_args): ) -class AddFloatingIP(network_common.NetworkAndComputeCommand): +class AddFloatingIP(command.Command): _description = _("Add floating IP address to server") - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( "server", metavar="", @@ -460,7 +489,8 @@ def update_parser_common(self, parser): ) return parser - def take_action_network(self, client, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network compute_client = self.app.client_manager.compute attrs = {} @@ -519,19 +549,11 @@ def take_action_network(self, client, parsed_args): if error: raise error - def take_action_compute(self, client, parsed_args): - server = client.find_server(parsed_args.server, ignore_missing=False) - client.add_floating_ip_to_server( - server, - parsed_args.ip_address, - fixed_address=parsed_args.fixed_ip_address, - ) - class AddPort(command.Command): _description = _("Add port to server") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "server", @@ -553,7 +575,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( @@ -585,7 +607,7 @@ def take_action(self, parsed_args): class AddNetwork(command.Command): _description = _("Add network to server") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "server", @@ -607,7 +629,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( @@ -640,7 +662,7 @@ def take_action(self, parsed_args): class AddServerSecurityGroup(command.Command): _description = _("Add security group(s) to server") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -658,7 +680,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( @@ -681,9 +703,10 @@ def take_action(self, parsed_args): errors = 0 for security_group in security_groups: try: + # SDK wants a SecurityGroup object but will handle a dict compute_client.add_security_group_to_server( server, - {'name': security_group}, + {'name': security_group}, # type: ignore[arg-type] ) except sdk_exceptions.HttpException as e: errors += 1 @@ -714,7 +737,7 @@ class AddServerVolume(command.ShowOne): with status ``SHELVED`` or ``SHELVED_OFFLOADED``.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -760,9 +783,11 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute - volume_client = self.app.client_manager.sdk_connection.volume + volume_client = self.app.client_manager.volume server = compute_client.find_server( parsed_args.server, @@ -834,7 +859,12 @@ def take_action(self, parsed_args): class NoneNICAction(argparse.Action): - def __init__(self, option_strings, dest, help=None): + def __init__( + self, + option_strings: list[str], + dest: str, + help: str | None = None, + ) -> None: super().__init__( option_strings=option_strings, dest=dest, @@ -844,7 +874,13 @@ def __init__(self, option_strings, dest, help=None): help=help, ) - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Any, + option_string: str | None = None, + ) -> None: # Make sure we have an empty dict rather than None if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, []) @@ -853,7 +889,12 @@ def __call__(self, parser, namespace, values, option_string=None): class AutoNICAction(argparse.Action): - def __init__(self, option_strings, dest, help=None): + def __init__( + self, + option_strings: list[str], + dest: str, + help: str | None = None, + ) -> None: super().__init__( option_strings=option_strings, dest=dest, @@ -863,7 +904,13 @@ def __init__(self, option_strings, dest, help=None): help=help, ) - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Any, + option_string: str | None = None, + ) -> None: # Make sure we have an empty dict rather than None if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, []) @@ -874,12 +921,12 @@ def __call__(self, parser, namespace, values, option_string=None): class NICAction(argparse.Action): def __init__( self, - option_strings, - dest, - help=None, - metavar=None, - key=None, - ): + option_strings: list[str], + dest: str, + help: str | None = None, + metavar: str | None = None, + key: str | None = None, + ) -> None: self.key = key super().__init__( option_strings=option_strings, @@ -894,7 +941,13 @@ def __init__( metavar=metavar, ) - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Any, + option_string: str | None = None, + ) -> None: # Make sure we have an empty dict rather than None if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, []) @@ -925,9 +978,9 @@ def __call__(self, parser, namespace, values, option_string=None): } for kv_str in values.split(','): - k, sep, v = kv_str.partition('=') + k, _sep, v = kv_str.partition('=') - if k not in list(info) + ['tag'] or not v: + if k not in [*list(info), 'tag'] or not v: msg = _( "Invalid argument %s; argument must be of form " "'net-id=net-uuid,port-id=port-uuid,v4-fixed-ip=ip-addr," @@ -955,12 +1008,18 @@ def __call__(self, parser, namespace, values, option_string=None): class BDMLegacyAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Any, + option_string: str | None = None, + ) -> None: # Make sure we have an empty list rather than None if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, []) - dev_name, sep, dev_map = values.partition('=') + dev_name, _sep, dev_map = values.partition('=') dev_map = dev_map.split(':') if dev_map else dev_map if not dev_name or not dev_map or len(dev_map) > 4: msg = _( @@ -999,7 +1058,9 @@ def __call__(self, parser, namespace, values, option_string=None): class BDMAction(parseractions.MultiKeyValueAction): - def __init__(self, option_strings, dest, **kwargs): + def __init__( + self, option_strings: list[str], dest: str, **kwargs: Any + ) -> None: optional_keys = [ 'uuid', 'source_type', @@ -1024,7 +1085,7 @@ def __init__(self, option_strings, dest, **kwargs): # TODO(stephenfin): Remove once I549d0897ef3704b7f47000f867d6731ad15d3f2b # or similar lands in a release - def validate_keys(self, keys): + def validate_keys(self, keys: Sequence[str]) -> None: """Validate the provided keys. :param keys: A list of keys to validate. @@ -1060,7 +1121,13 @@ def validate_keys(self, keys): }, ) - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Any, + option_string: str | None = None, + ) -> None: if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, []) @@ -1076,10 +1143,30 @@ def __call__(self, parser, namespace, values, option_string=None): super().__call__(parser, namespace, values, option_string) +def _server_create_needs_volume_client( + parsed_args: argparse.Namespace, +) -> bool: + if parsed_args.volume or parsed_args.snapshot: + return True + + for mapping in parsed_args.block_device_mapping: + if mapping['source_type'] in ('volume', 'snapshot'): + return True + + return False + + +def _get_required_volume_client(app: Any) -> Any: + if not app.client_manager.is_volume_endpoint_enabled(): + msg = _('Volume service is not available in the current cloud') + raise exceptions.CommandError(msg) + return app.client_manager.volume + + class CreateServer(command.ShowOne): _description = _("Create a new server") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server_name', @@ -1520,16 +1607,21 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _show_progress(progress): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + def _show_progress(progress: int | None) -> None: if progress: self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.flush() compute_client = self.app.client_manager.compute - volume_client = self.app.client_manager.volume image_client = self.app.client_manager.image + volume_client = None + if _server_create_needs_volume_client(parsed_args): + volume_client = _get_required_volume_client(self.app) + # Lookup parsed_args.image image = None if parsed_args.image: @@ -1539,7 +1631,7 @@ def _show_progress(progress): if not image and parsed_args.image_properties: - def _match_image(image_api, wanted_properties): + def _match_image(image_api: Any, wanted_properties: Any) -> Any: image_list = image_api.images() images_matched = [] for img in image_list: @@ -1600,9 +1692,10 @@ def _match_image(image_api, wanted_properties): msg = _('--volume is not allowed with --boot-from-volume') raise exceptions.CommandError(msg) - volume = utils.find_resource( - volume_client.volumes, + assert volume_client is not None # narrow type + volume = volume_client.find_volume( parsed_args.volume, + ignore_missing=False, ).id snapshot = None @@ -1612,9 +1705,10 @@ def _match_image(image_api, wanted_properties): msg = _('--snapshot is not allowed with --boot-from-volume') raise exceptions.CommandError(msg) - snapshot = utils.find_resource( - volume_client.volume_snapshots, + assert volume_client is not None # narrow type + snapshot = volume_client.find_snapshot( parsed_args.snapshot, + ignore_missing=False, ).id flavor = compute_client.find_flavor( @@ -1754,15 +1848,17 @@ def _match_image(image_api, wanted_properties): # The 'uuid' field isn't necessarily a UUID yet; let's validate it # just in case if mapping['source_type'] == 'volume': - volume_id = utils.find_resource( - volume_client.volumes, + assert volume_client is not None # narrow type + volume_id = volume_client.find_volume( mapping['uuid'], + ignore_missing=False, ).id mapping['uuid'] = volume_id elif mapping['source_type'] == 'snapshot': - snapshot_id = utils.find_resource( - volume_client.volume_snapshots, + assert volume_client is not None # narrow type + snapshot_id = volume_client.find_snapshot( mapping['uuid'], + ignore_missing=False, ).id mapping['uuid'] = snapshot_id elif mapping['source_type'] == 'image': @@ -1894,6 +1990,7 @@ def _match_image(image_api, wanted_properties): networks = parsed_args.nics[0] else: + _networks = [] for nic in parsed_args.nics: if 'tag' in nic: if not sdk_utils.supports_microversion( @@ -1947,14 +2044,15 @@ def _match_image(image_api, wanted_properties): network['port'] = nic['port-id'] if nic['v4-fixed-ip']: - network['fixed'] = nic['v4-fixed-ip'] + network['fixed_ip'] = nic['v4-fixed-ip'] elif nic['v6-fixed-ip']: - network['fixed'] = nic['v6-fixed-ip'] + network['fixed_ip'] = nic['v6-fixed-ip'] if nic.get('tag'): # tags are optional network['tag'] = nic['tag'] - networks.append(network) # type: ignore[union-attr] + _networks.append(network) + networks = _networks if not parsed_args.nics and sdk_utils.supports_microversion( compute_client, '2.37' @@ -2129,18 +2227,17 @@ def _match_image(image_api, wanted_properties): f.close() if parsed_args.wait: - if utils.wait_for_status( + if not utils.wait_for_status( compute_client.get_server, server.id, callback=_show_progress, ): - self.app.stdout.write('\n') - else: msg = _('Error creating server: %s') % parsed_args.server_name raise exceptions.CommandError(msg) data = _prep_server_detail(compute_client, image_client, server) - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class CreateServerDump(command.Command): @@ -2155,7 +2252,7 @@ class CreateServerDump(command.Command): This command requires ``--os-compute-api-version`` 2.17 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -2165,17 +2262,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute for name_or_id in parsed_args.server: - server = compute_client.find_server(name_or_id) + server = compute_client.find_server( + name_or_id, ignore_missing=False + ) server.trigger_crash_dump(compute_client) class DeleteServer(command.Command): _description = _("Delete server(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -2204,46 +2303,71 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _show_progress(progress): + def take_action(self, parsed_args: argparse.Namespace) -> None: + def _show_progress(progress: int | None) -> None: if progress: self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.flush() compute_client = self.app.client_manager.compute + + deleted_servers = [] for server in parsed_args.server: - server_obj = compute_client.find_server( - server, - ignore_missing=False, - all_projects=parsed_args.all_projects, - ) + try: + server_obj = compute_client.find_server( + server, + ignore_missing=False, + all_projects=parsed_args.all_projects, + ) - compute_client.delete_server(server_obj, force=parsed_args.force) + compute_client.delete_server( + server_obj, force=parsed_args.force + ) + deleted_servers.append(server_obj) + except Exception as e: + LOG.error( + _( + "Failed to delete server with " + "name or ID '%(server)s': %(e)s" + ), + {'server': server, 'e': e}, + ) - if parsed_args.wait: + if parsed_args.wait: + for server_obj in deleted_servers: try: compute_client.wait_for_delete( server_obj, callback=_show_progress ) except sdk_exceptions.ResourceTimeout: msg = _('Error deleting server: %s') % server_obj.id + deleted_servers.remove(server_obj) raise exceptions.CommandError(msg) + fails = len(parsed_args.server) - len(deleted_servers) + if fails > 0: + total = len(parsed_args.server) + msg = _("%(fails)s of %(total)s servers failed to delete.") % { + 'fails': fails, + 'total': total, + } + raise exceptions.CommandError(msg) + class PercentAction(argparse.Action): def __init__( self, - option_strings, - dest, - nargs=None, - const=None, - default=None, - type=None, - choices=None, - required=False, - help=None, - metavar=None, - ): + option_strings: list[str], + dest: str, + nargs: int | str | None = None, + const: Any = None, + default: Any = None, + type: Any = None, + choices: Any = None, + required: bool = False, + help: str | None = None, + metavar: str | tuple[str, ...] | None = None, + ) -> None: if nargs == 0: raise ValueError( 'nargs for store actions must be != 0; if you ' @@ -2267,7 +2391,13 @@ def __init__( metavar=metavar, ) - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Any, + option_string: str | None = None, + ) -> None: x = int(values) if not 0 < x <= 100: raise argparse.ArgumentError(self, "Must be between 0 and 100") @@ -2277,7 +2407,7 @@ def __call__(self, parser, namespace, values, option_string=None): class ListServer(command.Lister): _description = _("List servers") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--reservation-id', @@ -2617,27 +2747,31 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute - identity_client = self.app.client_manager.identity + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) image_client = self.app.client_manager.image project_id = None if parsed_args.project: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) parsed_args.all_projects = True user_id = None if parsed_args.user: - user_id = identity_common.find_user( + user_id = identity_common.find_user_id_sdk( identity_client, parsed_args.user, parsed_args.user_domain, - ).id + ) # Nova only supports list servers searching by flavor ID. So if a # flavor name is given, map it to ID. @@ -2750,6 +2884,9 @@ def take_action(self, parsed_args): search_opts['limit'] = parsed_args.limit search_opts['paginated'] = False + if parsed_args.max_items is not None: + search_opts['max_items'] = parsed_args.max_items + LOG.debug('search options: %s', search_opts) if search_opts['changes-before']: @@ -2840,18 +2977,25 @@ def take_action(self, parsed_args): if parsed_args.long: columns += ( 'availability_zone', - 'pinned_availability_zone', 'hypervisor_hostname', 'metadata', - 'scheduler_hints', ) column_headers += ( 'Availability Zone', - 'Pinned Availability Zone', 'Host', 'Properties', - 'Scheduler Hints', ) + if sdk_utils.supports_microversion(compute_client, '2.96'): + columns += ('pinned_availability_zone',) + column_headers += ('Pinned Availability Zone',) + + if sdk_utils.supports_microversion(compute_client, '2.100'): + columns += ('scheduler_hints',) + column_headers += ('Scheduler Hints',) + + if parsed_args.all_projects: + columns += ('project_id',) + column_headers += ('Project ID',) # support for additional columns if parsed_args.columns: @@ -2885,10 +3029,11 @@ def take_action(self, parsed_args): column_headers += ('Availability Zone',) if c in ( 'pinned_availability_zone', - "Pinned Availability Zone", + 'Pinned Availability Zone', ): - columns += ('Pinned Availability Zone',) - column_headers += ('Pinned Availability Zone',) + if sdk_utils.supports_microversion(compute_client, '2.96'): + columns += ('pinned_availability_zone',) + column_headers += ('Pinned Availability Zone',) if c in ('Host', "host"): columns += ('hypervisor_hostname',) column_headers += ('Host',) @@ -2899,8 +3044,11 @@ def take_action(self, parsed_args): 'scheduler_hints', "Scheduler Hints", ): - columns += ('scheduler_hints',) - column_headers += ('Scheduler Hints',) + if sdk_utils.supports_microversion( + compute_client, '2.100' + ): + columns += ('scheduler_hints',) + column_headers += ('Scheduler Hints',) # remove duplicates column_headers = tuple(dict.fromkeys(column_headers)) @@ -2987,8 +3135,8 @@ def take_action(self, parsed_args): else: try: flavors_list = compute_client.flavors(is_public=None) - for i in flavors_list: - flavors[i.id] = i + for f in flavors_list: + flavors[f.id] = f except Exception: # noqa: S110 # retrieving flavor names is not crucial, so we swallow any # exceptions @@ -3009,30 +3157,33 @@ def take_action(self, parsed_args): if 'id' in s.image and s.image.id is not None: image = images.get(s.image['id']) if image: - s.image_name = image.name + setattr(s, 'image_name', image.name) s.image_id = s.image['id'] else: # NOTE(melwitt): An server booted from a volume will have no # image associated with it. We fill in the Image Name and ID # with "N/A (booted from volume)" to help users who want to be # able to grep for boot-from-volume servers when using the CLI. - s.image_name = IMAGE_STRING_FOR_BFV + setattr(s, 'image_name', IMAGE_STRING_FOR_BFV) s.image_id = IMAGE_STRING_FOR_BFV if not sdk_utils.supports_microversion(compute_client, '2.47'): - flavor = flavors.get(s.flavor['id']) - if flavor: - s.flavor_name = flavor.name + if s.flavor['id'] in flavors: + setattr(s, 'flavor_name', flavors[s.flavor['id']].name) s.flavor_id = s.flavor['id'] else: - s.flavor_name = s.flavor['original_name'] + setattr(s, 'flavor_name', s.flavor['original_name']) # Add a list with security group name as attribute for s in data: if hasattr(s, 'security_groups') and s.security_groups is not None: - s.security_groups_name = [x["name"] for x in s.security_groups] + setattr( + s, + 'security_groups_name', + [x["name"] for x in s.security_groups], + ) else: - s.security_groups_name = [] + setattr(s, 'security_groups_name', []) # The host_status field contains the status of the compute host the # server is on. It is only returned by the API when the nova-api @@ -3083,7 +3234,7 @@ class LockServer(command.Command): A non-admin user will not be able to execute actions.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -3102,7 +3253,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute kwargs = {} @@ -3138,16 +3289,25 @@ class MigrateServer(command.Command): _description = _( """Migrate server to different host. -A migrate operation is implemented as a resize operation using the same flavor -as the old server. This means that, like resize, migrate works by creating a -new server using the same flavor and copying the contents of the original disk -into a new one. As with resize, the migrate operation is a two-step process for -the user: the first step is to perform the migrate, and the second step is to -either confirm (verify) success and release the old server, or to declare a -revert to release the new server and restart the old one.""" +There are two types of migration operation: a cold migration and a live +migration. + +A cold migration operation is implemented as a resize operation +using the same flavor as the old server. This means that, like resize, migrate +works by shutting down the original server, creating a new server using the +same flavor and copying the contents of the original disk into a new one. +As with resize, the migrate operation is a two-step process for the user: +the first step is to perform the migrate, and the second step is to either +confirm (verify) success and release the old server, or to declare a revert +to release the new server and restart the old one. + +By comparison, a live migration operation does not involve shutting the server +down, and is a one-step process that does not require a confirmation or revert +to finish. +""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -3221,8 +3381,8 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _show_progress(progress): + def take_action(self, parsed_args: argparse.Namespace) -> None: + def _show_progress(progress: int | None) -> None: if progress: self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.flush() @@ -3319,7 +3479,7 @@ def _show_progress(progress): class PauseServer(command.Command): _description = _("Pause server(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -3329,7 +3489,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute for server in parsed_args.server: server_id = compute_client.find_server( @@ -3342,7 +3502,7 @@ def take_action(self, parsed_args): class RebootServer(command.Command): _description = _("Perform a hard or soft server reboot") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -3373,8 +3533,8 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _show_progress(progress): + def take_action(self, parsed_args: argparse.Namespace) -> None: + def _show_progress(progress: int | None) -> None: if progress: self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.flush() @@ -3402,7 +3562,7 @@ def _show_progress(progress): class RebuildServer(command.ShowOne): _description = _("Rebuild server") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -3578,8 +3738,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _show_progress(progress): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + def _show_progress(progress: int | None) -> None: if progress: self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.flush() @@ -3771,7 +3933,8 @@ def _show_progress(progress): data = _prep_server_detail( compute_client, image_client, server, refresh=False ) - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class EvacuateServer(command.ShowOne): @@ -3792,7 +3955,7 @@ class EvacuateServer(command.ShowOne): host.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -3839,8 +4002,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _show_progress(progress): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + def _show_progress(progress: int | None) -> None: if progress: self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.flush() @@ -3878,9 +4043,15 @@ def _show_progress(progress): compute_client.evacuate_server(server, **kwargs) if parsed_args.wait: + orig_status = server.status + success = ['ACTIVE'] + if orig_status == 'SHUTOFF': + success.append('SHUTOFF') + if utils.wait_for_status( compute_client.get_server, server.id, + success_status=success, callback=_show_progress, ): self.app.stdout.write(_('Complete\n')) @@ -3889,13 +4060,14 @@ def _show_progress(progress): raise exceptions.CommandError(msg) data = _prep_server_detail(compute_client, image_client, server) - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class RemoveFixedIP(command.Command): _description = _("Remove fixed IP address from server") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "server", @@ -3909,7 +4081,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( @@ -3920,10 +4092,11 @@ def take_action(self, parsed_args): ) -class RemoveFloatingIP(network_common.NetworkAndComputeCommand): +class RemoveFloatingIP(command.Command): _description = _("Remove floating IP address from server") - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( "server", metavar="", @@ -3938,7 +4111,8 @@ def update_parser_common(self, parser): ) return parser - def take_action_network(self, client, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network obj = client.find_ip( parsed_args.ip_address, ignore_missing=False, @@ -3946,15 +4120,11 @@ def take_action_network(self, client, parsed_args): client.update_ip(obj, port_id=None) - def take_action_compute(self, client, parsed_args): - server = client.find_server(parsed_args.server, ignore_missing=False) - client.remove_floating_ip_from_server(server, parsed_args.ip_address) - class RemovePort(command.Command): _description = _("Remove port from server") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "server", @@ -3968,7 +4138,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( @@ -3993,7 +4163,7 @@ def take_action(self, parsed_args): class RemoveNetwork(command.Command): _description = _("Remove all ports of a network from server") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "server", @@ -4007,7 +4177,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( @@ -4033,7 +4203,7 @@ def take_action(self, parsed_args): class RemoveServerSecurityGroup(command.Command): _description = _("Remove security group from server") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -4051,7 +4221,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( @@ -4074,9 +4244,10 @@ def take_action(self, parsed_args): errors = 0 for security_group in security_groups: try: + # SDK wants a SecurityGroup object but will handle a dict compute_client.remove_security_group_from_server( server, - {'name': security_group}, + {'name': security_group}, # type: ignore[arg-type] ) except sdk_exceptions.HttpException as e: errors += 1 @@ -4107,7 +4278,7 @@ class RemoveServerVolume(command.Command): volume from a server with status ``SHELVED`` or ``SHELVED_OFFLOADED``.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -4121,9 +4292,11 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute - volume_client = self.app.client_manager.sdk_connection.volume + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) server = compute_client.find_server( parsed_args.server, @@ -4135,8 +4308,8 @@ def take_action(self, parsed_args): ) compute_client.delete_volume_attachment( - volume, server, + volume, ignore_missing=False, ) @@ -4149,7 +4322,7 @@ class RescueServer(command.Command): server booted from a volume.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -4174,13 +4347,13 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute image_client = self.app.client_manager.image - image_ref = None + image = None if parsed_args.image: - image_ref = image_client.find_image( + image = image_client.find_image( parsed_args.image, ignore_missing=False ).id @@ -4188,7 +4361,7 @@ def take_action(self, parsed_args): parsed_args.server, ignore_missing=False ) compute_client.rescue_server( - server, admin_pass=parsed_args.password, image_ref=image_ref + server, admin_pass=parsed_args.password, image=image ) @@ -4203,14 +4376,14 @@ class ResizeServer(command.Command): release the new server and restart the old one.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', metavar='', help=_('Server (name or ID)'), ) - phase_group = parser.add_mutually_exclusive_group() + phase_group = parser.add_mutually_exclusive_group(required=True) phase_group.add_argument( '--flavor', metavar='', @@ -4241,8 +4414,8 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _show_progress(progress): + def take_action(self, parsed_args: argparse.Namespace) -> None: + def _show_progress(progress: int | None) -> None: if progress: self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.flush() @@ -4299,7 +4472,7 @@ class ResizeConfirm(command.Command): Confirm (verify) success of resize operation and release the old server.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -4308,7 +4481,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( parsed_args.server, ignore_missing=False @@ -4320,7 +4493,7 @@ def take_action(self, parsed_args): class MigrateConfirm(ResizeConfirm): _description = _("DEPRECATED: Use 'server migration confirm' instead.") - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: msg = _( "The 'server migrate confirm' command has been deprecated in " "favour of the 'server migration confirm' command." @@ -4347,7 +4520,7 @@ class ResizeRevert(command.Command): one.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -4356,7 +4529,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( parsed_args.server, ignore_missing=False @@ -4368,7 +4541,7 @@ def take_action(self, parsed_args): class MigrateRevert(ResizeRevert): _description = _("DEPRECATED: Use 'server migration revert' instead.") - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: msg = _( "The 'server migrate revert' command has been deprecated in " "favour of the 'server migration revert' command." @@ -4390,7 +4563,7 @@ class RevertMigration(ResizeRevert): class RestoreServer(command.Command): _description = _("Restore server(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -4400,7 +4573,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute for server in parsed_args.server: server_id = compute_client.find_server( @@ -4413,7 +4586,7 @@ def take_action(self, parsed_args): class ResumeServer(command.Command): _description = _("Resume server(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -4423,7 +4596,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute for server in parsed_args.server: server_id = compute_client.find_server( @@ -4436,7 +4609,7 @@ def take_action(self, parsed_args): class SetServer(command.Command): _description = _("Set server properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -4533,10 +4706,20 @@ def get_parser(self, prog_name): '(supported by --os-compute-api-version 2.90 or above)' ), ) + parser.add_argument( + '--pinned-availability-zone', + metavar='', + help=_( + 'Pin the server to the given availability zone. ' + 'The server must currently be in the given zone. ' + 'To unpin, use "server unset --pinned-availability-zone" ' + '(supported by --os-compute-api-version 2.104 or above)' + ), + ) return parser @staticmethod - def ask_user_yesno(msg): + def ask_user_yesno(msg: str) -> bool: """Ask user Y/N question :param str msg: question text @@ -4549,7 +4732,7 @@ def ask_user_yesno(msg): elif answer in ('n', 'N', 'no'): return False - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( parsed_args.server, ignore_missing=False @@ -4579,6 +4762,14 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) + if parsed_args.pinned_availability_zone: + if not sdk_utils.supports_microversion(compute_client, '2.104'): + msg = _( + '--os-compute-api-version 2.104 or greater is required ' + 'to support the --pinned-availability-zone option' + ) + raise exceptions.CommandError(msg) + update_kwargs = {} if parsed_args.name: @@ -4590,6 +4781,11 @@ def take_action(self, parsed_args): if parsed_args.hostname: update_kwargs['hostname'] = parsed_args.hostname + if parsed_args.pinned_availability_zone: + update_kwargs['pinned_availability_zone'] = ( + parsed_args.pinned_availability_zone + ) + if update_kwargs: compute_client.update_server(server, **update_kwargs) @@ -4647,7 +4843,7 @@ class ShelveServer(command.Command): specified. This is an admin-only operation by default. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'servers', @@ -4673,8 +4869,8 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _show_progress(progress): + def take_action(self, parsed_args: argparse.Namespace) -> None: + def _show_progress(progress: int | None) -> None: if progress: self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.flush() @@ -4747,7 +4943,7 @@ class ShowServer(command.ShowOne): information for the server.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -4773,7 +4969,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute image_client = self.app.client_manager.image @@ -4784,8 +4982,9 @@ def take_action(self, parsed_args): ) if parsed_args.diagnostics: - data = compute_client.get_server_diagnostics(server) - return zip(*sorted(data.items())) + diagnostics = compute_client.get_server_diagnostics(server) + col_headers, col_data = zip(*sorted(diagnostics.items())) + return col_headers, col_data topology = None if parsed_args.topology: @@ -4803,13 +5002,14 @@ def take_action(self, parsed_args): ) if topology: data['topology'] = format_columns.DictColumn(topology) - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class SshServer(command.Command): _description = _("SSH to server") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -4903,7 +5103,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( @@ -4960,8 +5160,8 @@ def take_action(self, parsed_args): ip_address_family, ) - cmd = ' '.join(['ssh', ip_address] + args) - LOG.debug(f"ssh command: {cmd}") + cmd = ' '.join(['ssh', ip_address, *args]) + LOG.debug('ssh command: %s', cmd) # we intentionally pass through user-provided arguments and run this in # the user's shell os.system(cmd) # noqa: S605 @@ -4970,7 +5170,7 @@ def take_action(self, parsed_args): class StartServer(command.Command): _description = _("Start server(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -4989,7 +5189,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute for server in parsed_args.server: server_id = compute_client.find_server( @@ -5005,7 +5205,7 @@ def take_action(self, parsed_args): class StopServer(command.Command): _description = _("Stop server(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -5024,7 +5224,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute for server in parsed_args.server: server_id = compute_client.find_server( @@ -5039,7 +5239,7 @@ def take_action(self, parsed_args): class SuspendServer(command.Command): _description = _("Suspend server(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -5049,7 +5249,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute for server in parsed_args.server: server_id = compute_client.find_server( @@ -5062,7 +5262,7 @@ def take_action(self, parsed_args): class UnlockServer(command.Command): _description = _("Unlock server(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -5072,7 +5272,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute for server in parsed_args.server: server_id = compute_client.find_server( @@ -5085,7 +5285,7 @@ def take_action(self, parsed_args): class UnpauseServer(command.Command): _description = _("Unpause server(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -5095,7 +5295,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute for server in parsed_args.server: server_id = compute_client.find_server( @@ -5108,7 +5308,7 @@ def take_action(self, parsed_args): class UnrescueServer(command.Command): _description = _("Restore server from rescue mode") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -5117,7 +5317,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( parsed_args.server, ignore_missing=False @@ -5128,7 +5328,7 @@ def take_action(self, parsed_args): class UnsetServer(command.Command): _description = _("Unset server properties and tags") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -5182,9 +5382,18 @@ def get_parser(self, prog_name): '(supported by --os-compute-api-version 2.26 or above)' ), ) + parser.add_argument( + '--pinned-availability-zone', + dest='pinned_availability_zone', + action='store_true', + help=_( + 'Unpin the server from its availability zone ' + '(supported by --os-compute-api-version 2.104 or above)' + ), + ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute server = compute_client.find_server( @@ -5206,6 +5415,16 @@ def take_action(self, parsed_args): compute_client.update_server(server, description="") + if parsed_args.pinned_availability_zone: + if not sdk_utils.supports_microversion(compute_client, '2.104'): + msg = _( + '--os-compute-api-version 2.104 or greater is required ' + 'to support the --pinned-availability-zone option' + ) + raise exceptions.CommandError(msg) + + compute_client.update_server(server, pinned_availability_zone=None) + if parsed_args.tags or parsed_args.all_tags: if not sdk_utils.supports_microversion(compute_client, '2.26'): msg = _( @@ -5224,7 +5443,7 @@ def take_action(self, parsed_args): class UnshelveServer(command.Command): _description = _("Unshelve server(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -5270,8 +5489,8 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _show_progress(progress): + def take_action(self, parsed_args: argparse.Namespace) -> None: + def _show_progress(progress: int | None) -> None: if progress: self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.flush() diff --git a/openstackclient/compute/v2/server_backup.py b/openstackclient/compute/v2/server_backup.py index b0395db266..d7985baf70 100644 --- a/openstackclient/compute/v2/server_backup.py +++ b/openstackclient/compute/v2/server_backup.py @@ -15,12 +15,15 @@ """Compute v2 Server action implementations""" +import argparse +from collections.abc import Iterable, Sequence import importlib +from typing import Any -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -32,7 +35,7 @@ class CreateServerBackup(command.ShowOne): "2": "openstackclient.image.v2.image", } - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -65,8 +68,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _show_progress(progress): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + def _show_progress(progress: int | None) -> None: if progress: self.app.stderr.write(f'\rProgress: {progress}') self.app.stderr.flush() @@ -124,4 +129,5 @@ def _show_progress(progress): ] ) info = image_module._format_image(image) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data diff --git a/openstackclient/compute/v2/server_event.py b/openstackclient/compute/v2/server_event.py index 3275df9938..40e8962ea6 100644 --- a/openstackclient/compute/v2/server_event.py +++ b/openstackclient/compute/v2/server_event.py @@ -15,17 +15,21 @@ """Compute v2 Server operation event implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any import uuid from cliff import columns import iso8601 +from openstack.compute.v2 import server_action as _server_action from openstack import exceptions as sdk_exceptions from openstack import utils as sdk_utils -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ @@ -34,7 +38,7 @@ # TODO(stephenfin): Move this to osc_lib since it's useful elsewhere (e.g. # glance) -def is_uuid_like(value) -> bool: +def is_uuid_like(value: str) -> bool: """Returns validation of a value as a UUID. :param val: Value to verify @@ -57,14 +61,15 @@ def is_uuid_like(value) -> bool: return False -class ServerActionEventColumn(columns.FormattableColumn): +class ServerActionEventColumn(columns.FormattableColumn[Any]): """Custom formatter for server action events. Format the :class:`~openstack.compute.v2.server_action.ServerActionEvent` objects as we'd like. """ - def _format_event(self, event): + @staticmethod + def _format_event(event: _server_action.ServerAction) -> dict[str, Any]: hidden_columns = ['id', 'name', 'location'] _, columns = utils.get_osc_show_columns_for_sdk_resource( event, {}, hidden_columns @@ -72,17 +77,19 @@ def _format_event(self, event): data = utils.get_item_properties(event, columns) return dict(zip(columns, data)) - def human_readable(self): + def human_readable(self) -> str: events = [self._format_event(event) for event in self._value] - return utils.format_list_of_dicts(events) + return utils.format_list_of_dicts(events) or "" - def machine_readable(self): + def machine_readable(self) -> Any: events = [self._format_event(event) for event in self._value] return events -def _get_server_event_columns(item, client): - hidden_columns = ['name', 'server_id', 'links', 'location', 'finish_time'] +def _get_server_event_columns( + item: Any, client: Any +) -> tuple[tuple[str, ...], tuple[str, ...]]: + hidden_columns = ['name', 'server_id', 'links', 'location'] if not sdk_utils.supports_microversion(client, '2.58'): # updated_at was introduced in 2.58 @@ -100,7 +107,7 @@ class ListServerEvent(command.Lister): deleted server, specified by ID only. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -138,7 +145,9 @@ def get_parser(self, prog_name): pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute kwargs = {} @@ -164,6 +173,9 @@ def take_action(self, parsed_args): kwargs['limit'] = parsed_args.limit kwargs['paginated'] = False + if parsed_args.max_items is not None: + kwargs['max_items'] = parsed_args.max_items + if parsed_args.changes_since: if not sdk_utils.supports_microversion(compute_client, '2.58'): msg = _( @@ -251,7 +263,7 @@ class ShowServerEvent(command.ShowOne): non-admin users. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -265,7 +277,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute try: @@ -286,6 +300,8 @@ def take_action(self, parsed_args): parsed_args.request_id, server_id, ) + # this is a bug is SDK + assert server_action is not None column_headers, columns = _get_server_event_columns( server_action, diff --git a/openstackclient/compute/v2/server_group.py b/openstackclient/compute/v2/server_group.py index 73567488c1..09c8ab048d 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -15,29 +15,35 @@ """Compute v2 Server Group action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +from cliff import columns from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -_formatters = { +_formatters: dict[str, type[columns.FormattableColumn[Any]]] = { 'member_ids': format_columns.ListColumn, 'policies': format_columns.ListColumn, 'rules': format_columns.DictColumn, } -def _get_server_group_columns(item, client): +def _get_server_group_columns( + item: Any, client: Any +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = {'member_ids': 'members'} hidden_columns = ['metadata', 'location'] @@ -55,7 +61,7 @@ def _get_server_group_columns(item, client): class CreateServerGroup(command.ShowOne): _description = _("Create a new server group.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -92,7 +98,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute if parsed_args.policy in ('soft-affinity', 'soft-anti-affinity'): @@ -142,7 +150,7 @@ def take_action(self, parsed_args): class DeleteServerGroup(command.Command): _description = _("Delete existing server group(s).") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server_group', @@ -152,7 +160,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute result = 0 for group in parsed_args.server_group: @@ -177,7 +185,7 @@ def take_action(self, parsed_args): class ListServerGroup(command.Lister): _description = _("List all server groups.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--all-projects', @@ -196,7 +204,9 @@ def get_parser(self, prog_name): pagination.add_offset_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute kwargs = {} @@ -209,6 +219,8 @@ def take_action(self, parsed_args): if parsed_args.limit: kwargs['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + kwargs['max_items'] = parsed_args.max_items data = compute_client.server_groups(**kwargs) @@ -254,7 +266,7 @@ def take_action(self, parsed_args): class ShowServerGroup(command.ShowOne): _description = _("Display server group details.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server_group', @@ -263,7 +275,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute group = compute_client.find_server_group( parsed_args.server_group, ignore_missing=False diff --git a/openstackclient/compute/v2/server_image.py b/openstackclient/compute/v2/server_image.py index cb19173bc5..5b70ecb0f3 100644 --- a/openstackclient/compute/v2/server_image.py +++ b/openstackclient/compute/v2/server_image.py @@ -15,14 +15,17 @@ """Compute v2 Server action implementations""" +import argparse +from collections.abc import Iterable, Sequence import importlib import logging +from typing import Any from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -37,7 +40,7 @@ class CreateServerImage(command.ShowOne): "2": "openstackclient.image.v2.image", } - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -66,8 +69,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _show_progress(progress): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + def _show_progress(progress: int | None) -> None: if progress: self.app.stdout.write(f'\rProgress: {progress}') self.app.stdout.flush() @@ -85,11 +90,16 @@ def _show_progress(progress): else: image_name = server.name - image_id = compute_client.create_server_image( + image = compute_client.create_server_image( server.id, image_name, parsed_args.properties, - ).id + ) + if not image: + msg = _('Error creating server image: %s') + raise exceptions.CommandError(msg, parsed_args.server) + + image_id = image.id if parsed_args.wait: if utils.wait_for_status( @@ -99,10 +109,8 @@ def _show_progress(progress): ): self.app.stdout.write('\n') else: - LOG.error( - _('Error creating server image: %s'), parsed_args.server - ) - raise exceptions.CommandError + msg = _('Error creating server image: %s') + raise exceptions.CommandError(msg, parsed_args.server) image = image_client.find_image(image_id, ignore_missing=False) @@ -119,4 +127,5 @@ def _show_progress(progress): ) info = image_module._format_image(image) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data diff --git a/openstackclient/compute/v2/server_migration.py b/openstackclient/compute/v2/server_migration.py index c356bd81fc..aa6b75379c 100644 --- a/openstackclient/compute/v2/server_migration.py +++ b/openstackclient/compute/v2/server_migration.py @@ -12,13 +12,16 @@ """Compute v2 Server Migration action implementations""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any import uuid from openstack import utils as sdk_utils -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -27,7 +30,7 @@ class ListMigration(command.Lister): _description = _("""List server migrations""") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--server', @@ -98,7 +101,12 @@ def get_parser(self, prog_name): identity_common.add_user_domain_option_to_parser(parser) return parser - def print_migrations(self, parsed_args, compute_client, migrations): + def print_migrations( + self, + parsed_args: argparse.Namespace, + compute_client: Any, + migrations: Any, + ) -> tuple[list[str], Iterable[tuple[Any, ...]]]: column_headers = [ 'Source Node', 'Dest Node', @@ -153,9 +161,13 @@ def print_migrations(self, parsed_args, compute_client, migrations): (utils.get_item_properties(mig, columns) for mig in migrations), ) - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[list[str], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute - identity_client = self.app.client_manager.identity + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) search_opts = {} @@ -197,6 +209,9 @@ def take_action(self, parsed_args): search_opts['limit'] = parsed_args.limit search_opts['paginated'] = False + if parsed_args.max_items is not None: + search_opts['max_items'] = parsed_args.max_items + if parsed_args.changes_since: if not sdk_utils.supports_microversion(compute_client, "2.59"): msg = _( @@ -223,11 +238,11 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - search_opts['project_id'] = identity_common.find_project( + search_opts['project_id'] = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) if parsed_args.user: if not sdk_utils.supports_microversion(compute_client, "2.80"): @@ -237,18 +252,20 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - search_opts['user_id'] = identity_common.find_user( + search_opts['user_id'] = identity_common.find_user_id_sdk( identity_client, parsed_args.user, parsed_args.user_domain, - ).id + ) migrations = list(compute_client.migrations(**search_opts)) return self.print_migrations(parsed_args, compute_client, migrations) -def _get_migration_by_uuid(compute_client, server_id, migration_uuid): +def _get_migration_by_uuid( + compute_client: Any, server_id: str, migration_uuid: str +) -> Any: for migration in compute_client.server_migrations(server_id): if migration.uuid == migration_uuid: return migration @@ -274,7 +291,7 @@ class ShowMigration(command.ShowOne): these. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -288,7 +305,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute if not sdk_utils.supports_microversion(compute_client, '2.24'): @@ -389,7 +408,7 @@ class AbortMigration(command.Command): This command requires ``--os-compute-api-version`` 2.24 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -403,7 +422,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute if not sdk_utils.supports_microversion(compute_client, '2.24'): @@ -456,7 +475,7 @@ class ForceCompleteMigration(command.Command): This command requires ``--os-compute-api-version`` 2.22 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -468,7 +487,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute if not sdk_utils.supports_microversion(compute_client, '2.22'): diff --git a/openstackclient/compute/v2/server_share.py b/openstackclient/compute/v2/server_share.py new file mode 100644 index 0000000000..040c1acc0a --- /dev/null +++ b/openstackclient/compute/v2/server_share.py @@ -0,0 +1,274 @@ +# Copyright 2020, Red Hat Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Compute v2 Server action implementations""" + +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack import utils as sdk_utils +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient import command +from openstackclient.i18n import _ + + +def _get_server_share_columns( + item: Any, +) -> tuple[tuple[str, ...], tuple[str, ...]]: + # Non admin cannot see uuid and export location, so hide them + if item.uuid is None: + column_map = { + 'share_id': 'Share ID', + 'status': 'Status', + 'tag': 'Tag', + } + hidden_columns = [ + 'id', + 'location', + 'name', + 'uuid', + 'export_location', + 'share_proto', + ] + else: + column_map = { + 'uuid': 'UUID', + 'share_id': 'Share ID', + 'status': 'Status', + 'tag': 'Tag', + 'export_location': 'Export Location', + } + hidden_columns = ['id', 'location', 'name', 'share_proto'] + + return utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns + ) + + +class ListServerShare(command.Lister): + """List all the shares attached to a server. + + Requires ``--os-compute-api-version 2.97`` or later. + """ + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server to list share mapping for (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + compute_client = self.app.client_manager.compute + + if not sdk_utils.supports_microversion(compute_client, '2.97'): + msg = _( + '--os-compute-api-version 2.97 or greater is required ' + 'to support share attachments' + ) + raise exceptions.CommandError(msg) + + server = compute_client.find_server( + parsed_args.server, + ignore_missing=False, + ) + shares = compute_client.share_attachments(server) + + columns = ( + 'share_id', + 'status', + 'tag', + ) + column_headers = ( + 'Share ID', + 'Status', + 'Tag', + ) + + return ( + column_headers, + (utils.get_item_properties(s, columns) for s in shares), + ) + + +class ShowServerShare(command.ShowOne): + """Show detail of a share attachment to a server. + + Requires ``--os-compute-api-version 2.97`` or later. + """ + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server to show share mapping for (name or ID)'), + ) + parser.add_argument( + 'share', + metavar='', + help=_('Share to show details for (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + compute_client = self.app.client_manager.compute + shared_file_system_client = ( + self.app.client_manager.sdk_connection.shared_file_system + ) + + if not sdk_utils.supports_microversion(compute_client, '2.97'): + msg = _( + '--os-compute-api-version 2.97 or greater is required ' + 'to support share attachments' + ) + raise exceptions.CommandError(msg) + + server = compute_client.find_server( + parsed_args.server, + ignore_missing=False, + ) + share = shared_file_system_client.find_share( + parsed_args.share, + ignore_missing=False, + ) + share_attachment = compute_client.get_share_attachment( + server, share.id + ) + + display_columns, columns = _get_server_share_columns( + share_attachment, + ) + data = utils.get_item_properties(share_attachment, columns) + return display_columns, data + + +class AddServerShare(command.ShowOne): + """Add a share to a server. + + Requires ``--os-compute-api-version 2.97`` or later. + """ + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server to add share to (name or ID)'), + ) + parser.add_argument( + 'share', + metavar='', + help=_('Share to add (name or ID)'), + ) + parser.add_argument( + '--tag', + metavar='', + help=_( + 'Optional tag used to mount the share, ' + 'if not provided the share uuid is used as tag by default' + ), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + compute_client = self.app.client_manager.compute + shared_file_system_client = ( + self.app.client_manager.sdk_connection.shared_file_system + ) + + if not sdk_utils.supports_microversion(compute_client, '2.97'): + msg = _( + '--os-compute-api-version 2.97 or greater is required ' + 'to support share attachments' + ) + raise exceptions.CommandError(msg) + + server = compute_client.find_server( + parsed_args.server, + ignore_missing=False, + ) + share = shared_file_system_client.find_share( + parsed_args.share, + ignore_missing=False, + ) + + kwargs: dict[str, Any] = {} + if parsed_args.tag: + kwargs['tag'] = parsed_args.tag + + share_attachment = compute_client.create_share_attachment( + server, share.id, **kwargs + ) + + display_columns, columns = _get_server_share_columns( + share_attachment, + ) + data = utils.get_item_properties(share_attachment, columns) + return display_columns, data + + +class RemoveServerShare(command.Command): + """Remove a share from a server. + + Requires ``--os-compute-api-version 2.97`` or later. + """ + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'server', + metavar='', + help=_('Server to remove share from (name or ID)'), + ) + parser.add_argument( + 'share', + metavar='', + help=_('Share to remove (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + compute_client = self.app.client_manager.compute + shared_file_system_client = ( + self.app.client_manager.sdk_connection.shared_file_system + ) + + if not sdk_utils.supports_microversion(compute_client, '2.97'): + msg = _( + '--os-compute-api-version 2.97 or greater is required ' + 'to support share attachments' + ) + raise exceptions.CommandError(msg) + + server = compute_client.find_server( + parsed_args.server, + ignore_missing=False, + ) + share = shared_file_system_client.find_share( + parsed_args.share, + ignore_missing=False, + ) + compute_client.delete_share_attachment(server, share.id) diff --git a/openstackclient/compute/v2/server_volume.py b/openstackclient/compute/v2/server_volume.py index a27c63f53e..f5ae05b437 100644 --- a/openstackclient/compute/v2/server_volume.py +++ b/openstackclient/compute/v2/server_volume.py @@ -14,18 +14,22 @@ """Compute v2 Server action implementations""" +import argparse +from collections.abc import Iterable +from typing import Any + from openstack import utils as sdk_utils -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ class ListServerVolume(command.Lister): """List all the volumes attached to a server.""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -33,7 +37,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute server = compute_client.find_server( @@ -81,7 +87,7 @@ def take_action(self, parsed_args): class SetServerVolume(command.Command): """Update a volume attachment on the server.""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'server', @@ -113,9 +119,11 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute - volume_client = self.app.client_manager.sdk_connection.volume + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if parsed_args.delete_on_termination is not None: if not sdk_utils.supports_microversion(compute_client, '2.85'): diff --git a/openstackclient/compute/v2/service.py b/openstackclient/compute/v2/service.py index 3660ddd49d..6e50342c8a 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -15,13 +15,17 @@ """Service action implementations""" +import argparse +from collections.abc import Iterable import logging +from typing import Any +from openstack.compute.v2 import service as _service from openstack import utils as sdk_utils -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -31,7 +35,7 @@ class DeleteService(command.Command): _description = _("Delete compute service(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "service", @@ -51,7 +55,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute result = 0 for s in parsed_args.service: @@ -84,7 +88,7 @@ class ListService(command.Lister): deployment.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--host", @@ -107,7 +111,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: compute_client = self.app.client_manager.compute columns: tuple[str, ...] = ( "id", @@ -146,7 +152,7 @@ def take_action(self, parsed_args): class SetService(command.Command): _description = _("Set compute service properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument("host", metavar="", help=_("Name of host")) parser.add_argument( @@ -192,7 +198,9 @@ def get_parser(self, prog_name): return parser @staticmethod - def _find_service_by_host_and_binary(compute_client, host, binary): + def _find_service_by_host_and_binary( + compute_client: Any, host: str, binary: str + ) -> _service.Service: """Utility method to find a compute service by host and binary :param host: the name of the compute service host @@ -220,7 +228,7 @@ def _find_service_by_host_and_binary(compute_client, host, binary): raise exceptions.CommandError(msg) return services[0] - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: compute_client = self.app.client_manager.compute if ( @@ -259,7 +267,9 @@ def take_action(self, parsed_args): if enabled is not None: if enabled: compute_client.enable_service( - service_id, parsed_args.host, parsed_args.service + service_id, + parsed_args.host, + parsed_args.service, ) else: compute_client.disable_service( diff --git a/openstackclient/compute/v2/usage.py b/openstackclient/compute/v2/usage.py index d045c1efe0..06ac6560e0 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -15,19 +15,24 @@ """Usage action implementations""" +import argparse +from collections.abc import Collection, Iterable, Sequence import datetime import functools +from typing import Any from cliff import columns as cliff_columns -from osc_lib.command import command +from openstack import utils as sdk_utils from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common # TODO(stephenfin): This exists in a couple of places and should be moved to a # common module -class ProjectColumn(cliff_columns.FormattableColumn): +class ProjectColumn(cliff_columns.FormattableColumn[str]): """Formattable column for project column. Unlike the parent FormattableColumn class, the initializer of the class @@ -38,32 +43,34 @@ class ProjectColumn(cliff_columns.FormattableColumn): project_cache)`` to use this. """ - def __init__(self, value, project_cache=None): + def __init__( + self, value: str, project_cache: dict[str, str] | None = None + ) -> None: super().__init__(value) self.project_cache = project_cache or {} - def human_readable(self): + def human_readable(self) -> str: project = self._value if not project: return '' - if project in self.project_cache.keys(): - return self.project_cache[project].name + if project in self.project_cache: + return self.project_cache[project] return project -class CountColumn(cliff_columns.FormattableColumn): - def human_readable(self): - return len(self._value) if self._value is not None else None +class CountColumn(cliff_columns.FormattableColumn[Collection[Any]]): + def human_readable(self) -> str: + return str(len(self._value)) if self._value is not None else "" -class FloatColumn(cliff_columns.FormattableColumn): - def human_readable(self): - return float(f"{self._value:.2f}") +class FloatColumn(cliff_columns.FormattableColumn[float]): + def human_readable(self) -> str: + return f"{self._value:.2f}" -def _formatters(project_cache): +def _formatters(project_cache: dict[str, str] | None) -> dict[str, Any]: return { 'project_id': functools.partial( ProjectColumn, project_cache=project_cache @@ -75,21 +82,21 @@ def _formatters(project_cache): } -def _get_usage_marker(usage): +def _get_usage_marker(usage: Any) -> str | None: marker = None if hasattr(usage, 'server_usages') and usage.server_usages: marker = usage.server_usages[-1]['instance_id'] return marker -def _get_usage_list_marker(usage_list): +def _get_usage_list_marker(usage_list: Any) -> str | None: marker = None if usage_list: marker = _get_usage_marker(usage_list[-1]) return marker -def _merge_usage(usage, next_usage): +def _merge_usage(usage: Any, next_usage: Any) -> None: usage.server_usages.extend(next_usage.server_usages) usage.total_hours += next_usage.total_hours usage.total_memory_mb_usage += next_usage.total_memory_mb_usage @@ -97,7 +104,7 @@ def _merge_usage(usage, next_usage): usage.total_local_gb_usage += next_usage.total_local_gb_usage -def _merge_usage_list(usages, next_usage_list): +def _merge_usage_list(usages: Any, next_usage_list: Any) -> None: for next_usage in next_usage_list: if next_usage.project_id in usages: _merge_usage(usages[next_usage.project_id], next_usage) @@ -108,7 +115,7 @@ def _merge_usage_list(usages, next_usage_list): class ListUsage(command.Lister): _description = _("List resource usage per project") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--start", @@ -126,16 +133,22 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - def _format_project(project): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + def _format_project(project: str) -> str: if not project: return "" - if project in project_cache.keys(): - return project_cache[project].name + if project in project_cache: + return project_cache[project] else: return project compute_client = self.app.client_manager.compute + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + columns = ( "project_id", "server_usages", @@ -152,7 +165,7 @@ def _format_project(project): ) date_cli_format = "%Y-%m-%d" - now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + now = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) if parsed_args.start: start = datetime.datetime.strptime( @@ -177,8 +190,8 @@ def _format_project(project): # Cache the project list project_cache = {} try: - for p in self.app.client_manager.identity.projects.list(): - project_cache[p.id] = p + for p in identity_client.projects(): + project_cache[p.id] = p.name except Exception: # noqa: S110 # Just forget it if there's any trouble pass @@ -208,7 +221,7 @@ def _format_project(project): class ShowUsage(command.ShowOne): _description = _("Show resource usage for a single project") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--project", @@ -216,6 +229,7 @@ def get_parser(self, prog_name): default=None, help=_("Name or ID of project to show usage for"), ) + identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( "--start", metavar="", @@ -232,11 +246,16 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: compute_client = self.app.client_manager.compute + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + date_cli_format = "%Y-%m-%d" - now = datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + now = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) if parsed_args.start: start = datetime.datetime.strptime( @@ -251,13 +270,16 @@ def take_action(self, parsed_args): end = now + datetime.timedelta(days=1) if parsed_args.project: - project = utils.find_resource( - identity_client.projects, + project = identity_common.find_project_id_sdk( + identity_client, parsed_args.project, - ).id + parsed_args.project_domain, + ) else: # Get the project from the current auth - project = self.app.client_manager.auth_ref.project_id + _project_id = self.app.client_manager.auth_ref.project_id + assert _project_id is not None # narrow type + project = _project_id usage = compute_client.get_usage( project=project, diff --git a/openstackclient/identity/client.py b/openstackclient/identity/client.py index 707112cd05..aeb1d5304c 100644 --- a/openstackclient/identity/client.py +++ b/openstackclient/identity/client.py @@ -12,7 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import logging +from typing import Any from keystoneclient.v2_0 import client as identity_client_v2 from osc_lib import utils @@ -39,7 +41,7 @@ } -def make_client(instance): +def make_client(instance: Any) -> Any: """Returns an identity service client.""" identity_client = utils.get_client_class( API_NAME, instance._api_version[API_NAME], API_VERSIONS @@ -56,7 +58,9 @@ def make_client(instance): return client -def build_option_parser(parser): +def build_option_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: """Hook to add global options""" parser.add_argument( '--os-identity-api-version', @@ -70,10 +74,11 @@ def build_option_parser(parser): return parser +# We're not going to add type hints to this at this point class IdentityClientv2(identity_client_v2.Client): """Tweak the earlier client class to deal with some changes""" - def __getattr__(self, name): + def __getattr__(self, name: str) -> Any: # Map v3 'projects' back to v2 'tenants' if name == "projects": return self.tenants diff --git a/openstackclient/identity/common.py b/openstackclient/identity/common.py index 412fd71597..b35c1b31b9 100644 --- a/openstackclient/identity/common.py +++ b/openstackclient/identity/common.py @@ -15,30 +15,27 @@ """Common identity code""" +import argparse +from collections.abc import Callable +from typing import Any, cast + from keystoneclient import exceptions as identity_exc from keystoneclient.v3 import domains from keystoneclient.v3 import groups from keystoneclient.v3 import projects from keystoneclient.v3 import users from openstack import exceptions as sdk_exceptions +from openstack.identity import v2 as identity_v2 +from openstack.identity import v3 as identity_v3 +from openstack.identity.v3 import service as _service +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ -def find_service_in_list(service_list, service_id): - """Find a service by id in service list.""" - - for service in service_list: - if service.id == service_id: - return service - raise exceptions.CommandError( - f"No service with a type, name or ID of '{service_id}' exists." - ) - - -def find_service(identity_client, name_type_or_id): +def find_service(identity_client: Any, name_type_or_id: str) -> Any: """Find a service by id, name or type.""" try: @@ -74,7 +71,9 @@ def find_service(identity_client, name_type_or_id): raise exceptions.CommandError(msg % name_type_or_id) -def find_service_sdk(identity_client, name_type_or_id): +def find_service_sdk( + identity_client: identity_v3.Proxy, name_type_or_id: str +) -> _service.Service: """Find a service by id, name or type.""" try: @@ -99,14 +98,15 @@ def find_service_sdk(identity_client, name_type_or_id): if next(services, None): msg = _( - "Multiple service matches found for '%(query)s', use an ID to be more specific." + "Multiple service matches found for '%(query)s', " + "use an ID to be more specific." ) % {"query": name_type_or_id} raise exceptions.CommandError(msg) return service -def get_resource(manager, name_type_or_id): +def get_resource(manager: Any, name_type_or_id: str) -> Any: # NOTE (vishakha): Due to bug #1799153 and for any another related case # where GET resource API does not support the filter by name, # osc_lib.utils.find_resource() method cannot be used because that method @@ -125,7 +125,7 @@ def get_resource(manager, name_type_or_id): raise exceptions.CommandError(msg % name_type_or_id) -def get_resource_by_id(manager, resource_id): +def get_resource_by_id(manager: Any, resource_id: str) -> Any: """Get resource by ID Raises CommandError if the resource is not found @@ -137,7 +137,12 @@ def get_resource_by_id(manager, resource_id): raise exceptions.CommandError(msg.format(resource_id)) -def _get_token_resource(client, resource, parsed_name, parsed_domain=None): +def _get_token_resource( + client: Any, + resource: str, + parsed_name: str, + parsed_domain: str | None = None, +) -> str: """Peek into the user's auth token to get resource IDs Look into a user's token to try and find the ID of a domain, project or @@ -173,25 +178,28 @@ def _get_token_resource(client, resource, parsed_name, parsed_domain=None): if isinstance(obj, list): for item in obj: if item['name'] == parsed_name: - return item['id'] + return cast(str, item['id']) if item['id'] == parsed_name: return parsed_name return parsed_name return obj['id'] if obj['name'] == parsed_name else parsed_name # diaper defense in case parsing the token fails - except Exception: # noqa + except Exception: return parsed_name -def find_domain(identity_client, name_or_id): +def find_domain(identity_client: Any, name_or_id: str) -> domains.Domain: return _find_identity_resource( identity_client.domains, name_or_id, domains.Domain ) def find_domain_id_sdk( - identity_client, name_or_id, *, validate_actor_existence=True -): + identity_client: identity_v3.Proxy, + name_or_id: str, + *, + validate_actor_existence: bool = True, +) -> str: return _find_sdk_id( identity_client.find_domain, name_or_id=name_or_id, @@ -199,7 +207,11 @@ def find_domain_id_sdk( ) -def find_group(identity_client, name_or_id, domain_name_or_id=None): +def find_group( + identity_client: Any, + name_or_id: str, + domain_name_or_id: str | None = None, +) -> groups.Group: if domain_name_or_id is None: return _find_identity_resource( identity_client.groups, name_or_id, groups.Group @@ -214,7 +226,38 @@ def find_group(identity_client, name_or_id, domain_name_or_id=None): ) -def find_project(identity_client, name_or_id, domain_name_or_id=None): +def find_group_id_sdk( + identity_client: identity_v3.Proxy, + name_or_id: str, + domain_name_or_id: str | None = None, + *, + validate_actor_existence: bool = True, +) -> str: + if domain_name_or_id is None: + return _find_sdk_id( + identity_client.find_group, + name_or_id=name_or_id, + validate_actor_existence=validate_actor_existence, + ) + + domain_id = find_domain_id_sdk( + identity_client, + name_or_id=domain_name_or_id, + validate_actor_existence=validate_actor_existence, + ) + return _find_sdk_id( + identity_client.find_group, + name_or_id=name_or_id, + validate_actor_existence=validate_actor_existence, + domain_id=domain_id, + ) + + +def find_project( + identity_client: Any, + name_or_id: str, + domain_name_or_id: str | None = None, +) -> projects.Project: if domain_name_or_id is None: return _find_identity_resource( identity_client.projects, name_or_id, projects.Project @@ -228,7 +271,51 @@ def find_project(identity_client, name_or_id, domain_name_or_id=None): ) -def find_user(identity_client, name_or_id, domain_name_or_id=None): +def find_project_id_sdk( + identity_client: identity_v2.Proxy | identity_v3.Proxy, + name_or_id: str, + domain_name_or_id: str | None = None, + *, + validate_actor_existence: bool = True, + validate_domain_actor_existence: bool | None = None, +) -> str: + if domain_name_or_id is None: + if isinstance(identity_client, identity_v2.Proxy): + return _find_sdk_id( + identity_client.find_tenant, + name_or_id=name_or_id, + validate_actor_existence=validate_actor_existence, + ) + + return _find_sdk_id( + identity_client.find_project, + name_or_id=name_or_id, + validate_actor_existence=validate_actor_existence, + ) + + if validate_domain_actor_existence is None: + validate_domain_actor_existence = validate_actor_existence + + # only v3 supports the concept of domains + identity_client = sdk_utils.ensure_service_version(identity_client, '3') + domain_id = find_domain_id_sdk( + identity_client, + name_or_id=domain_name_or_id, + validate_actor_existence=validate_domain_actor_existence, + ) + return _find_sdk_id( + identity_client.find_project, + name_or_id=name_or_id, + validate_actor_existence=validate_actor_existence, + domain_id=domain_id, + ) + + +def find_user( + identity_client: Any, + name_or_id: str, + domain_name_or_id: str | None = None, +) -> users.User: if domain_name_or_id is None: return _find_identity_resource( identity_client.users, name_or_id, users.User @@ -240,18 +327,21 @@ def find_user(identity_client, name_or_id, domain_name_or_id=None): def find_user_id_sdk( - identity_client, - name_or_id, - domain_name_or_id=None, + identity_client: identity_v2.Proxy | identity_v3.Proxy, + name_or_id: str, + domain_name_or_id: str | None = None, *, - validate_actor_existence=True, -): + validate_actor_existence: bool = True, +) -> str: if domain_name_or_id is None: return _find_sdk_id( identity_client.find_user, name_or_id=name_or_id, validate_actor_existence=validate_actor_existence, ) + + # only v3 supports the concept of domains + identity_client = sdk_utils.ensure_service_version(identity_client, '3') domain_id = find_domain_id_sdk( identity_client, name_or_id=domain_name_or_id, @@ -266,8 +356,11 @@ def find_user_id_sdk( def _find_identity_resource( - identity_client_manager, name_or_id, resource_type, **kwargs -): + identity_client_manager: Any, + name_or_id: str, + resource_type: Any, + **kwargs: Any, +) -> Any: """Find a specific identity resource. Using keystoneclient's manager, attempt to find a specific resource by its @@ -306,8 +399,12 @@ def _find_identity_resource( def _find_sdk_id( - find_command, name_or_id, *, validate_actor_existence=True, **kwargs -): + find_command: Callable[..., Any], + name_or_id: str, + *, + validate_actor_existence: bool = True, + **kwargs: Any, +) -> str: try: resource = find_command( name_or_id=name_or_id, ignore_missing=False, **kwargs @@ -318,19 +415,10 @@ def _find_sdk_id( if not validate_actor_existence: return name_or_id raise exceptions.CommandError from exc - return resource.id - - -def get_immutable_options(parsed_args): - options = {} - if parsed_args.immutable: - options['immutable'] = True - if parsed_args.no_immutable: - options['immutable'] = False - return options + return cast(str, resource.id) -def add_user_domain_option_to_parser(parser): +def add_user_domain_option_to_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--user-domain', metavar='', @@ -342,7 +430,7 @@ def add_user_domain_option_to_parser(parser): ) -def add_group_domain_option_to_parser(parser): +def add_group_domain_option_to_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--group-domain', metavar='', @@ -354,21 +442,21 @@ def add_group_domain_option_to_parser(parser): ) -def add_project_domain_option_to_parser(parser, enhance_help=lambda _h: _h): +def add_project_domain_option_to_parser( + parser: argparse.ArgumentParser, +) -> None: parser.add_argument( '--project-domain', metavar='', - help=enhance_help( - _( - 'Domain the project belongs to (name or ID). This ' - 'can be used in case collisions between project ' - 'names exist.' - ) + help=_( + 'Domain the project belongs to (name or ID). This ' + 'can be used in case collisions between project ' + 'names exist.' ), ) -def add_role_domain_option_to_parser(parser): +def add_role_domain_option_to_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--role-domain', metavar='', @@ -380,7 +468,7 @@ def add_role_domain_option_to_parser(parser): ) -def add_inherited_option_to_parser(parser): +def add_inherited_option_to_parser(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--inherited', action='store_true', @@ -391,18 +479,22 @@ def add_inherited_option_to_parser(parser): ) -def add_resource_option_to_parser(parser): - enable_group = parser.add_mutually_exclusive_group() - enable_group.add_argument( +def add_resource_option_to_parser(parser: argparse.ArgumentParser) -> None: + immutable_group = parser.add_mutually_exclusive_group() + immutable_group.add_argument( '--immutable', action='store_true', + dest='immutable', + default=None, help=_( 'Make resource immutable. An immutable project may not ' 'be deleted or modified except to remove the immutable flag' ), ) - enable_group.add_argument( + immutable_group.add_argument( '--no-immutable', - action='store_true', + action='store_false', + dest='immutable', + default=None, help=_('Make resource mutable (default)'), ) diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index ccac5d04ba..9f52a57371 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -13,21 +13,24 @@ """Identity v2 Service Catalog action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from cliff import columns as cliff_columns -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -class EndpointsColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class EndpointsColumn(cliff_columns.FormattableColumn[Any]): + def human_readable(self) -> str: if not self._value: return "" ret = '' @@ -46,7 +49,9 @@ def human_readable(self): class ListCatalog(command.Lister): _description = _("List services in the service catalog") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: # Trigger auth if it has not happened yet auth_ref = self.app.client_manager.auth_ref if not auth_ref: @@ -74,7 +79,7 @@ def take_action(self, parsed_args): class ShowCatalog(command.ShowOne): _description = _("Display service catalog details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service', @@ -83,7 +88,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: # Trigger auth if it has not happened yet auth_ref = self.app.client_manager.auth_ref if not auth_ref: @@ -107,4 +114,5 @@ def take_action(self, parsed_args): LOG.error(_('service %s not found\n'), parsed_args.service) return ((), ()) - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data diff --git a/openstackclient/identity/v2_0/ec2creds.py b/openstackclient/identity/v2_0/ec2creds.py index 25ee8a75d4..33b16563ba 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -16,12 +16,15 @@ """Identity v2 EC2 Credentials action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -31,7 +34,7 @@ class CreateEC2Creds(command.ShowOne): _description = _("Create EC2 credentials") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--project', @@ -51,7 +54,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity if parsed_args.project: @@ -79,13 +84,14 @@ def take_action(self, parsed_args): if 'tenant_id' in info: info.update({'project_id': info.pop('tenant_id')}) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteEC2Creds(command.Command): _description = _("Delete EC2 credentials") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'access_keys', @@ -100,7 +106,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity if parsed_args.user: @@ -138,7 +144,7 @@ def take_action(self, parsed_args): class ListEC2Creds(command.Lister): _description = _("List EC2 credentials") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--user', @@ -147,7 +153,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: identity_client = self.app.client_manager.identity if parsed_args.user: @@ -179,7 +187,7 @@ def take_action(self, parsed_args): class ShowEC2Creds(command.ShowOne): _description = _("Display EC2 credentials details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'access_key', @@ -193,7 +201,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity if parsed_args.user: @@ -213,4 +223,5 @@ def take_action(self, parsed_args): if 'tenant_id' in info: info.update({'project_id': info.pop('tenant_id')}) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data diff --git a/openstackclient/identity/v2_0/endpoint.py b/openstackclient/identity/v2_0/endpoint.py index db8efede11..5ea22b52e5 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -15,12 +15,15 @@ """Endpoint action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common @@ -31,7 +34,7 @@ class CreateEndpoint(command.ShowOne): _description = _("Create new endpoint") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service', @@ -61,7 +64,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity service = common.find_service(identity_client, parsed_args.service) endpoint = identity_client.endpoints.create( @@ -76,13 +81,14 @@ def take_action(self, parsed_args): info.update(endpoint._info) info['service_name'] = service.name info['service_type'] = service.type - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteEndpoint(command.Command): _description = _("Delete endpoint(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpoints', @@ -92,7 +98,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity result = 0 @@ -121,7 +127,7 @@ def take_action(self, parsed_args): class ListEndpoint(command.Lister): _description = _("List endpoints") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -131,7 +137,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: identity_client = self.app.client_manager.identity columns: tuple[str, ...] = ( @@ -168,7 +176,7 @@ def take_action(self, parsed_args): class ShowEndpoint(command.ShowOne): _description = _("Display endpoint details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpoint_or_service', @@ -180,7 +188,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity data = identity_client.endpoints.list() match = None @@ -196,9 +206,11 @@ def take_action(self, parsed_args): if ep.service_id == service.id: match = ep if match is None: - return None + return ((), ()) + info = {} info.update(match._info) info['service_name'] = service.name info['service_type'] = service.type - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data diff --git a/openstackclient/identity/v2_0/project.py b/openstackclient/identity/v2_0/project.py index a0c3c1e4d3..61ead6c0f8 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -15,15 +15,18 @@ """Identity v2 Project action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from keystoneauth1 import exceptions as ks_exc from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -33,7 +36,7 @@ class CreateProject(command.ShowOne): _description = _("Create new project") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -59,6 +62,7 @@ def get_parser(self, prog_name): parser.add_argument( '--property', metavar='', + dest='properties', action=parseractions.KeyValueAction, help=_( 'Add a property to ' @@ -72,15 +76,17 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity enabled = True if parsed_args.disable: enabled = False kwargs = {} - if parsed_args.property: - kwargs = parsed_args.property.copy() + if parsed_args.properties: + kwargs.update(parsed_args.properties) try: project = identity_client.tenants.create( @@ -101,13 +107,21 @@ def take_action(self, parsed_args): # TODO(stevemar): Remove the line below when we support multitenancy project._info.pop('parent_id', None) - return zip(*sorted(project._info.items())) + col_headers, col_data = zip(*sorted(project._info.items())) + return col_headers, col_data class DeleteProject(command.Command): - _description = _("Delete project(s)") - - def get_parser(self, prog_name): + _description = _( + "Delete project(s). This command will remove specified " + "existing project(s) if an active user is authorized to do " + "this. If there are resources managed by other services " + "(for example, Nova, Neutron, Cinder) associated with " + "specified project(s), delete operation will proceed " + "regardless." + ) + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'projects', @@ -117,7 +131,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity errors = 0 @@ -150,7 +164,7 @@ def take_action(self, parsed_args): class ListProject(command.Lister): _description = _("List projects") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -169,7 +183,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: columns: tuple[str, ...] = ('ID', 'Name') if parsed_args.long: columns += ('Description', 'Enabled') @@ -192,7 +208,7 @@ def take_action(self, parsed_args): class SetProject(command.Command): _description = _("Set project properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'project', @@ -223,6 +239,7 @@ def get_parser(self, prog_name): parser.add_argument( '--property', metavar='', + dest='properties', action=parseractions.KeyValueAction, help=_( 'Set a project property ' @@ -231,7 +248,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity project = utils.find_resource( @@ -248,8 +265,8 @@ def take_action(self, parsed_args): kwargs['enabled'] = True if parsed_args.disable: kwargs['enabled'] = False - if parsed_args.property: - kwargs.update(parsed_args.property) + if parsed_args.properties: + kwargs.update(parsed_args.properties) if 'id' in kwargs: del kwargs['id'] if 'name' in kwargs: @@ -263,7 +280,7 @@ def take_action(self, parsed_args): class ShowProject(command.ShowOne): _description = _("Display project details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'project', @@ -272,7 +289,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity info = {} @@ -315,13 +334,14 @@ def take_action(self, parsed_args): properties[k] = v info['properties'] = format_columns.DictColumn(properties) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class UnsetProject(command.Command): _description = _("Unset project properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'project', @@ -331,6 +351,7 @@ def get_parser(self, prog_name): parser.add_argument( '--property', metavar='', + dest='properties', action='append', default=[], help=_( @@ -340,14 +361,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity project = utils.find_resource( identity_client.tenants, parsed_args.project, ) kwargs = project._info - for key in parsed_args.property: + for key in parsed_args.properties: if key in kwargs: kwargs[key] = None identity_client.tenants.update(project.id, **kwargs) diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index 1faab2e5ac..b2169fd943 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -15,13 +15,16 @@ """Identity v2 Role action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from keystoneauth1 import exceptions as ks_exc -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -31,7 +34,7 @@ class AddRole(command.ShowOne): _description = _("Add role to project:user") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'role', @@ -52,7 +55,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity role = utils.find_resource(identity_client.roles, parsed_args.role) project = utils.find_resource( @@ -68,13 +73,14 @@ def take_action(self, parsed_args): info = {} info.update(role._info) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class CreateRole(command.ShowOne): _description = _("Create new role") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'role_name', @@ -88,7 +94,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity try: role = identity_client.roles.create(parsed_args.role_name) @@ -104,13 +112,14 @@ def take_action(self, parsed_args): info = {} info.update(role._info) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteRole(command.Command): _description = _("Delete role(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'roles', @@ -120,7 +129,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity errors = 0 @@ -153,7 +162,9 @@ def take_action(self, parsed_args): class ListRole(command.Lister): _description = _("List roles") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: identity_client = self.app.client_manager.identity columns = ('ID', 'Name') @@ -175,7 +186,7 @@ def take_action(self, parsed_args): class RemoveRole(command.Command): _description = _("Remove role from project : user") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'role', @@ -196,7 +207,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity role = utils.find_resource(identity_client.roles, parsed_args.role) project = utils.find_resource( @@ -210,7 +221,7 @@ def take_action(self, parsed_args): class ShowRole(command.ShowOne): _description = _("Display role details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'role', @@ -219,10 +230,13 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity role = utils.find_resource(identity_client.roles, parsed_args.role) info = {} info.update(role._info) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data diff --git a/openstackclient/identity/v2_0/role_assignment.py b/openstackclient/identity/v2_0/role_assignment.py index 54d01b4e51..e29f7426e2 100644 --- a/openstackclient/identity/v2_0/role_assignment.py +++ b/openstackclient/identity/v2_0/role_assignment.py @@ -13,17 +13,21 @@ """Identity v2 Assignment action implementations""" -from osc_lib.command import command +import argparse +from collections.abc import Iterable +from typing import Any + from osc_lib import exceptions from osc_lib import utils -from openstackclient.i18n import _ # noqa +from openstackclient import command +from openstackclient.i18n import _ class ListRoleAssignment(command.Lister): _description = _("List role assignments") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--user', @@ -55,7 +59,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: identity_client = self.app.client_manager.identity auth_ref = self.app.client_manager.auth_ref @@ -68,7 +74,7 @@ def take_action(self, parsed_args): parsed_args.user, ) elif parsed_args.authuser: - if auth_ref: + if auth_ref and auth_ref.user_id: user = utils.find_resource( identity_client.users, auth_ref.user_id ) @@ -80,7 +86,7 @@ def take_action(self, parsed_args): parsed_args.project, ) elif parsed_args.authproject: - if auth_ref: + if auth_ref and auth_ref.project_id: project = utils.find_resource( identity_client.projects, auth_ref.project_id ) diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 978e926f3b..417bd48dd8 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -15,12 +15,15 @@ """Service action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common @@ -31,7 +34,7 @@ class CreateService(command.ShowOne): _description = _("Create new service") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'type', @@ -50,7 +53,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity name = parsed_args.name @@ -64,13 +69,14 @@ def take_action(self, parsed_args): info = {} info.update(service._info) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteService(command.Command): _description = _("Delete service(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'services', @@ -80,7 +86,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity result = 0 @@ -110,7 +116,7 @@ def take_action(self, parsed_args): class ListService(command.Lister): _description = _("List services") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -120,7 +126,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: columns: tuple[str, ...] = ('ID', 'Name', 'Type') if parsed_args.long: columns += ('Description',) @@ -134,7 +142,7 @@ def take_action(self, parsed_args): class ShowService(command.ShowOne): _description = _("Display service details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service', @@ -149,7 +157,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity auth_ref = self.app.client_manager.auth_ref @@ -160,8 +170,11 @@ def take_action(self, parsed_args): for service, service_endpoints in endpoints.items(): if service_endpoints: info = {"type": service} - info.update(service_endpoints[0]) - return zip(*sorted(info.items())) + # FIXME(stephenfin): The return type for this in ksa is + # wrong + info.update(service_endpoints[0]) # type: ignore + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data msg = _( "No service catalog with a type, name or ID of '%s' exists." @@ -171,4 +184,5 @@ def take_action(self, parsed_args): service = common.find_service(identity_client, parsed_args.service) info = {} info.update(service._info) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data diff --git a/openstackclient/identity/v2_0/token.py b/openstackclient/identity/v2_0/token.py index fe9436d8b5..e3e91c9f97 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -15,9 +15,13 @@ """Identity v2 Token action implementations""" -from osc_lib.command import command +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib import exceptions +from openstackclient import command from openstackclient.i18n import _ @@ -27,11 +31,13 @@ class IssueToken(command.ShowOne): # scoped token is optional required_scope = False - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: auth_ref = self.app.client_manager.auth_ref if not auth_ref: raise exceptions.AuthorizationFailure( @@ -49,13 +55,14 @@ def take_action(self, parsed_args): data['project_id'] = auth_ref.project_id if auth_ref.user_id: data['user_id'] = auth_ref.user_id - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class RevokeToken(command.Command): _description = _("Revoke existing token") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'token', @@ -64,7 +71,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity identity_client.tokens.delete(parsed_args.token) diff --git a/openstackclient/identity/v2_0/user.py b/openstackclient/identity/v2_0/user.py index c4cd52b8d1..d76ba8d095 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -15,22 +15,25 @@ """Identity v2.0 User action implementations""" +import argparse +from collections.abc import Iterable, Sequence import functools import logging +from typing import Any, cast from cliff import columns as cliff_columns from keystoneauth1 import exceptions as ks_exc -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -class ProjectColumn(cliff_columns.FormattableColumn): +class ProjectColumn(cliff_columns.FormattableColumn[str]): """Formattable column for project column. Unlike the parent FormattableColumn class, the initializer of the @@ -41,16 +44,16 @@ class takes project_cache as the second argument. ``functools.partial(ProjectColumn, project_cache)``. """ - def __init__(self, value, project_cache=None): + def __init__(self, value: str, project_cache: Any = None) -> None: super().__init__(value) self.project_cache = project_cache or {} - def human_readable(self): + def human_readable(self) -> str: project = self._value if not project: return "" if project in self.project_cache.keys(): - return self.project_cache[project].name + return cast(str, self.project_cache[project].name) else: return project @@ -58,7 +61,7 @@ def human_readable(self): class CreateUser(command.ShowOne): _description = _("Create new user") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -104,7 +107,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity if parsed_args.project: @@ -155,13 +160,14 @@ def take_action(self, parsed_args): info = {} info.update(user._info) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteUser(command.Command): _description = _("Delete user(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'users', @@ -171,7 +177,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity errors = 0 @@ -204,7 +210,7 @@ def take_action(self, parsed_args): class ListUser(command.Lister): _description = _("List users") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--project', @@ -219,7 +225,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: identity_client = self.app.client_manager.identity formatters = {} project = None @@ -278,7 +286,7 @@ def take_action(self, parsed_args): class SetUser(command.Command): _description = _("Set user properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'user', @@ -324,7 +332,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity if parsed_args.password_prompt: @@ -376,7 +384,7 @@ def take_action(self, parsed_args): class ShowUser(command.ShowOne): _description = _("Display user details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'user', @@ -385,7 +393,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity info = {} @@ -417,4 +427,5 @@ def take_action(self, parsed_args): if 'tenant_id' in info: info.update({'project_id': info.pop('tenant_id')}) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data diff --git a/openstackclient/identity/v3/access_rule.py b/openstackclient/identity/v3/access_rule.py index 367c649019..bee319379e 100644 --- a/openstackclient/identity/v3/access_rule.py +++ b/openstackclient/identity/v3/access_rule.py @@ -15,12 +15,16 @@ """Identity v3 Access Rule action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common @@ -31,7 +35,7 @@ class DeleteAccessRule(command.Command): _description = _("Delete access rule(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'access_rule', @@ -41,10 +45,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) conn = self.app.client_manager.sdk_connection - user_id = conn.config.get_auth().get_user_id(conn.identity) + auth = conn.config.get_auth() + if auth is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') + user_id = auth.get_user_id(conn.session) + if user_id is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') errors = 0 for ac in parsed_args.access_rule: @@ -69,7 +82,7 @@ def take_action(self, parsed_args): class ListAccessRule(command.Lister): _description = _("List access rules") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--user', @@ -79,15 +92,23 @@ def get_parser(self, prog_name): common.add_user_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) if parsed_args.user: user_id = common.find_user( identity_client, parsed_args.user, parsed_args.user_domain ).id else: conn = self.app.client_manager.sdk_connection - user_id = conn.config.get_auth().get_user_id(conn.identity) + auth = conn.config.get_auth() + if auth is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') + user_id = auth.get_user_id(conn.session) columns = ('ID', 'Service', 'Method', 'Path') data = identity_client.access_rules(user=user_id) @@ -107,7 +128,7 @@ def take_action(self, parsed_args): class ShowAccessRule(command.ShowOne): _description = _("Display access rule details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'access_rule', @@ -116,10 +137,21 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) conn = self.app.client_manager.sdk_connection - user_id = conn.config.get_auth().get_user_id(conn.identity) + auth = conn.config.get_auth() + if auth is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') + user_id = auth.get_user_id(conn.session) + if user_id is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') access_rule = identity_client.get_access_rule( user_id, parsed_args.access_rule diff --git a/openstackclient/identity/v3/application_credential.py b/openstackclient/identity/v3/application_credential.py index f0dd5a58fd..3b7630186a 100644 --- a/openstackclient/identity/v3/application_credential.py +++ b/openstackclient/identity/v3/application_credential.py @@ -15,32 +15,117 @@ """Identity v3 Application Credential action implementations""" +import argparse +from collections.abc import Iterable, Sequence import datetime import json import logging +from typing import Any import uuid -from osc_lib.command import command +from cliff import columns as cliff_columns +from openstack.identity.v3 import ( + application_credential as _application_credential, +) +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common - LOG = logging.getLogger(__name__) +class RolesColumn(cliff_columns.FormattableColumn[Any]): + """Generate a formatted string of role names.""" + + def human_readable(self) -> str: + return utils.format_list(list(r['name'] for r in self._value)) or "" + + +def _format_application_credential( + application_credential: _application_credential.ApplicationCredential, + *, + include_secret: bool = False, +) -> tuple[tuple[str, ...], Iterable[Any]]: + column_headers: tuple[str, ...] = ( + 'ID', + 'Name', + 'Description', + 'Project ID', + 'Roles', + 'Unrestricted', + 'Access Rules', + 'Expires At', + ) + columns: tuple[str, ...] = ( + 'id', + 'name', + 'description', + 'project_id', + 'roles', + 'unrestricted', + 'access_rules', + 'expires_at', + ) + if include_secret: + column_headers += ('Secret',) + columns += ('secret',) + + return ( + column_headers, + utils.get_item_properties( + application_credential, columns, formatters={'roles': RolesColumn} + ), + ) + + +def _format_application_credentials( + application_credentials: Iterable[ + _application_credential.ApplicationCredential + ], +) -> tuple[tuple[str, ...], Any]: + column_headers = ( + 'ID', + 'Name', + 'Description', + 'Project ID', + 'Roles', + 'Unrestricted', + 'Access Rules', + 'Expires At', + ) + columns = ( + 'id', + 'name', + 'description', + 'project_id', + 'roles', + 'unrestricted', + 'access_rules', + 'expires_at', + ) + + return ( + column_headers, + ( + utils.get_item_properties( + x, columns, formatters={'roles': RolesColumn} + ) + for x in application_credentials + ), + ) + + # TODO(stephenfin): Move this to osc_lib since it's useful elsewhere -def is_uuid_like(value) -> bool: +def is_uuid_like(value: str) -> bool: """Returns validation of a value as a UUID. :param val: Value to verify :type val: string :returns: bool - - .. versionchanged:: 1.1.1 - Support non-lowercase UUIDs. """ try: formatted_value = ( @@ -58,7 +143,7 @@ def is_uuid_like(value) -> bool: class CreateApplicationCredential(command.ShowOne): _description = _("Create new application credential") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -76,6 +161,7 @@ def get_parser(self, prog_name): parser.add_argument( '--role', metavar='', + dest='roles', action='append', default=[], help=_( @@ -129,13 +215,25 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) conn = self.app.client_manager.sdk_connection - user_id = conn.config.get_auth().get_user_id(conn.identity) + auth = conn.config.get_auth() + if auth is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') + + user_id = auth.get_user_id(conn.session) + if user_id is None: + msg = _("failed to retrieve auth info for current session") + raise exceptions.CommandError(msg) role_ids = [] - for role in parsed_args.role: + for role in parsed_args.roles: if is_uuid_like(role): role_ids.append({'id': role}) else: @@ -179,38 +277,15 @@ def take_action(self, parsed_args): access_rules=access_rules, ) - # Format roles into something sensible - if application_credential['roles']: - roles = application_credential['roles'] - msg = ' '.join(r['name'] for r in roles) - application_credential['roles'] = msg - - columns = ( - 'id', - 'name', - 'description', - 'project_id', - 'roles', - 'unrestricted', - 'access_rules', - 'expires_at', - 'secret', - ) - return ( - columns, - ( - utils.get_dict_properties( - application_credential, - columns, - ) - ), + return _format_application_credential( + application_credential, include_secret=True ) class DeleteApplicationCredential(command.Command): _description = _("Delete application credentials(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'application_credential', @@ -220,16 +295,27 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + conn = self.app.client_manager.sdk_connection - user_id = conn.config.get_auth().get_user_id(conn.identity) + auth = conn.config.get_auth() + if auth is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') + + user_id = auth.get_user_id(conn.session) + if user_id is None: + msg = _("failed to retrieve auth info for current session") + raise exceptions.CommandError(msg) errors = 0 for ac in parsed_args.application_credential: try: app_cred = identity_client.find_application_credential( - user_id, ac + user_id, ac, ignore_missing=False ) identity_client.delete_application_credential( user_id, app_cred.id @@ -252,11 +338,13 @@ def take_action(self, parsed_args): ) % {'errors': errors, 'total': total} raise exceptions.CommandError(msg) + return None + class ListApplicationCredential(command.Lister): _description = _("List application credentials") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--user', @@ -266,54 +354,39 @@ def get_parser(self, prog_name): common.add_user_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) if parsed_args.user: user_id = common.find_user_id_sdk( identity_client, parsed_args.user, parsed_args.user_domain ) else: conn = self.app.client_manager.sdk_connection - user_id = conn.config.get_auth().get_user_id(conn.identity) - - data = identity_client.application_credentials(user=user_id) - - data_formatted = [] - for ac in data: - # Format roles into something sensible - roles = ac['roles'] - msg = ' '.join(r['name'] for r in roles) - ac['roles'] = msg - - data_formatted.append(ac) - - columns = ( - 'ID', - 'Name', - 'Description', - 'Project ID', - 'Roles', - 'Unrestricted', - 'Access Rules', - 'Expires At', - ) - return ( - columns, - ( - utils.get_item_properties( - s, - columns, - formatters={}, - ) - for s in data_formatted - ), + auth = conn.config.get_auth() + if auth is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') + _user_id = auth.get_user_id(conn.session) + if _user_id is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') + user_id = _user_id + + application_credentials = identity_client.application_credentials( + user=user_id ) + return _format_application_credentials(application_credentials) + class ShowApplicationCredential(command.ShowOne): _description = _("Display application credential details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'application_credential', @@ -322,36 +395,26 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity - conn = self.app.client_manager.sdk_connection - user_id = conn.config.get_auth().get_user_id(conn.identity) - - app_cred = identity_client.find_application_credential( - user_id, parsed_args.application_credential + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' ) - # Format roles into something sensible - roles = app_cred['roles'] - msg = ' '.join(r['name'] for r in roles) - app_cred['roles'] = msg + conn = self.app.client_manager.sdk_connection + auth = conn.config.get_auth() + if auth is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') + + user_id = auth.get_user_id(conn.session) + if user_id is None: + msg = _("failed to retrieve auth info for current session") + raise exceptions.CommandError(msg) - columns = ( - 'id', - 'name', - 'description', - 'project_id', - 'roles', - 'unrestricted', - 'access_rules', - 'expires_at', - ) - return ( - columns, - ( - utils.get_dict_properties( - app_cred, - columns, - ) - ), + application_credential = identity_client.find_application_credential( + user_id, parsed_args.application_credential, ignore_missing=False ) + + return _format_application_credential(application_credential) diff --git a/openstackclient/identity/v3/catalog.py b/openstackclient/identity/v3/catalog.py index a161461ce3..7c85dd1447 100644 --- a/openstackclient/identity/v3/catalog.py +++ b/openstackclient/identity/v3/catalog.py @@ -9,25 +9,27 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# """Identity v3 Service Catalog action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from cliff import columns as cliff_columns -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -class EndpointsColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class EndpointsColumn(cliff_columns.FormattableColumn[Any]): + def human_readable(self) -> str: if not self._value: return "" ret = '' @@ -41,7 +43,9 @@ def human_readable(self): class ListCatalog(command.Lister): _description = _("List services in the service catalog") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: # Trigger auth if it has not happened yet auth_ref = self.app.client_manager.auth_ref if not auth_ref: @@ -69,7 +73,7 @@ def take_action(self, parsed_args): class ShowCatalog(command.ShowOne): _description = _("Display service catalog details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service', @@ -78,7 +82,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: # Trigger auth if it has not happened yet auth_ref = self.app.client_manager.auth_ref if not auth_ref: @@ -102,4 +108,5 @@ def take_action(self, parsed_args): LOG.error(_('service %s not found\n'), parsed_args.service) return ((), ()) - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data diff --git a/openstackclient/identity/v3/consumer.py b/openstackclient/identity/v3/consumer.py index 933f48aa74..874ed3ff2c 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -15,12 +15,15 @@ """Identity v3 Consumer action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -30,7 +33,7 @@ class CreateConsumer(command.ShowOne): _description = _("Create new consumer") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--description', @@ -39,19 +42,22 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity consumer = identity_client.oauth1.consumers.create( parsed_args.description ) consumer._info.pop('links', None) - return zip(*sorted(consumer._info.items())) + col_headers, col_data = zip(*sorted(consumer._info.items())) + return col_headers, col_data class DeleteConsumer(command.Command): _description = _("Delete consumer(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'consumer', @@ -61,7 +67,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity result = 0 for i in parsed_args.consumer: @@ -92,7 +98,9 @@ def take_action(self, parsed_args): class ListConsumer(command.Lister): _description = _("List consumers") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: columns = ('ID', 'Description') data = self.app.client_manager.identity.oauth1.consumers.list() return ( @@ -111,7 +119,7 @@ def take_action(self, parsed_args): class SetConsumer(command.Command): _description = _("Set consumer properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'consumer', @@ -125,7 +133,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity consumer = utils.find_resource( identity_client.oauth1.consumers, parsed_args.consumer @@ -142,7 +150,7 @@ def take_action(self, parsed_args): class ShowConsumer(command.ShowOne): _description = _("Display consumer details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'consumer', @@ -151,11 +159,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity consumer = utils.find_resource( identity_client.oauth1.consumers, parsed_args.consumer ) consumer._info.pop('links', None) - return zip(*sorted(consumer._info.items())) + col_headers, col_data = zip(*sorted(consumer._info.items())) + return col_headers, col_data diff --git a/openstackclient/identity/v3/credential.py b/openstackclient/identity/v3/credential.py index 7d9c7c46c2..9b1078a649 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -15,20 +15,26 @@ """Identity v3 Credential action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.identity.v3 import credential as _credential +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common - LOG = logging.getLogger(__name__) -def _format_credential(credential): +def _format_credential( + credential: _credential.Credential, +) -> tuple[tuple[str, ...], Iterable[Any]]: columns = ( 'blob', 'id', @@ -48,7 +54,7 @@ def _format_credential(credential): class CreateCredential(command.ShowOne): _description = _("Create new credential") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'user', @@ -75,8 +81,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) user_id = identity_client.find_user( parsed_args.user, ignore_missing=False ).id @@ -99,7 +109,7 @@ def take_action(self, parsed_args): class DeleteCredential(command.Command): _description = _("Delete credential(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'credential', @@ -109,8 +119,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) result = 0 for i in parsed_args.credential: try: @@ -137,7 +149,7 @@ def take_action(self, parsed_args): class ListCredential(command.Lister): _description = _("List credentials") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--user', @@ -152,8 +164,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {} if parsed_args.user: @@ -190,7 +206,7 @@ def take_action(self, parsed_args): class SetCredential(command.Command): _description = _("Set credential properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'credential', @@ -224,8 +240,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) user_id = identity_client.find_user( parsed_args.user, ignore_missing=False @@ -250,7 +268,7 @@ def take_action(self, parsed_args): class ShowCredential(command.ShowOne): _description = _("Display credential details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'credential', @@ -259,8 +277,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) credential = identity_client.get_credential(parsed_args.credential) return _format_credential(credential) diff --git a/openstackclient/identity/v3/domain.py b/openstackclient/identity/v3/domain.py index 215f1be8be..91f9c49e8c 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -15,24 +15,55 @@ """Identity v3 Domain action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from keystoneauth1 import exceptions as ks_exc -from osc_lib.command import command +from openstack import exceptions as sdk_exceptions +from openstack.identity.v3 import domain as _domain +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common - LOG = logging.getLogger(__name__) +def _format_domain( + domain: _domain.Domain, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: + columns = ( + 'id', + 'name', + 'is_enabled', + 'description', + 'options', + ) + column_headers = ( + 'id', + 'name', + 'enabled', + 'description', + 'options', + ) + + return ( + column_headers, + utils.get_item_properties( + domain, + columns, + ), + ) + + class CreateDomain(command.ShowOne): _description = _("Create new domain") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -47,12 +78,15 @@ def get_parser(self, prog_name): enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', + dest='is_enabled', action='store_true', + default=True, help=_('Enable domain (default)'), ) enable_group.add_argument( '--disable', - action='store_true', + dest='is_enabled', + action='store_false', help=_('Disable domain'), ) parser.add_argument( @@ -63,39 +97,40 @@ def get_parser(self, prog_name): common.add_resource_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - enabled = True - if parsed_args.disable: - enabled = False + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - options = common.get_immutable_options(parsed_args) + options = {} + if parsed_args.immutable is not None: + options['immutable'] = parsed_args.immutable try: - domain = identity_client.domains.create( + domain = identity_client.create_domain( name=parsed_args.name, description=parsed_args.description, options=options, - enabled=enabled, + is_enabled=parsed_args.is_enabled, ) - except ks_exc.Conflict: + except sdk_exceptions.ConflictException: if parsed_args.or_show: - domain = utils.find_resource( - identity_client.domains, parsed_args.name + domain = identity_client.find_domain( + parsed_args.name, ignore_missing=False ) LOG.info(_('Returning existing domain %s'), domain.name) else: raise - domain._info.pop('links') - return zip(*sorted(domain._info.items())) + return _format_domain(domain) class DeleteDomain(command.Command): _description = _("Delete domain(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'domain', @@ -105,13 +140,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) result = 0 for i in parsed_args.domain: try: - domain = utils.find_resource(identity_client.domains, i) - identity_client.domains.delete(domain.id) + domain = identity_client.find_domain(i, ignore_missing=False) + identity_client.delete_domain(domain.id) except Exception as e: result += 1 LOG.error( @@ -134,7 +171,7 @@ def take_action(self, parsed_args): class ListDomain(command.Lister): _description = _("List domains") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--name', @@ -143,23 +180,30 @@ def get_parser(self, prog_name): ) parser.add_argument( '--enabled', - dest='enabled', + dest='is_enabled', action='store_true', help=_('The domains that are enabled will be returned'), ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {} if parsed_args.name: kwargs['name'] = parsed_args.name - if parsed_args.enabled: - kwargs['enabled'] = True + if parsed_args.is_enabled: + kwargs['is_enabled'] = True + + columns = ('id', 'name', 'is_enabled', 'description') + column_headers = ('ID', 'Name', 'Enabled', 'Description') + data = identity_client.domains(**kwargs) - columns = ('ID', 'Name', 'Enabled', 'Description') - data = self.app.client_manager.identity.domains.list(**kwargs) return ( - columns, + column_headers, ( utils.get_item_properties( s, @@ -174,7 +218,7 @@ def take_action(self, parsed_args): class SetDomain(command.Command): _description = _("Set domain properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'domain', @@ -194,44 +238,45 @@ def get_parser(self, prog_name): enable_group = parser.add_mutually_exclusive_group() enable_group.add_argument( '--enable', + dest='is_enabled', action='store_true', + default=None, help=_('Enable domain'), ) enable_group.add_argument( '--disable', - action='store_true', + dest='is_enabled', + action='store_false', + default=None, help=_('Disable domain'), ) common.add_resource_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - domain = utils.find_resource( - identity_client.domains, parsed_args.domain + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + domain = identity_client.find_domain( + parsed_args.domain, ignore_missing=False ) kwargs = {} if parsed_args.name: kwargs['name'] = parsed_args.name if parsed_args.description: kwargs['description'] = parsed_args.description + if parsed_args.is_enabled is not None: + kwargs['is_enabled'] = parsed_args.is_enabled + if parsed_args.immutable is not None: + kwargs['options'] = {'immutable': parsed_args.immutable} - if parsed_args.enable: - kwargs['enabled'] = True - if parsed_args.disable: - kwargs['enabled'] = False - - options = common.get_immutable_options(parsed_args) - if options: - kwargs['options'] = options - - identity_client.domains.update(domain.id, **kwargs) + identity_client.update_domain(domain.id, **kwargs) class ShowDomain(command.ShowOne): _description = _("Display domain details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'domain', @@ -240,14 +285,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - domain_str = common._get_token_resource( - identity_client, 'domain', parsed_args.domain + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + domain = identity_client.find_domain( + parsed_args.domain, ignore_missing=False ) - domain = utils.find_resource(identity_client.domains, domain_str) - - domain._info.pop('links') - return zip(*sorted(domain._info.items())) + return _format_domain(domain) diff --git a/openstackclient/identity/v3/ec2creds.py b/openstackclient/identity/v3/ec2creds.py index 84700f496d..d090fb646c 100644 --- a/openstackclient/identity/v3/ec2creds.py +++ b/openstackclient/identity/v3/ec2creds.py @@ -12,12 +12,15 @@ """Identity v3 EC2 Credentials action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any, cast -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common @@ -25,7 +28,9 @@ LOG = logging.getLogger(__name__) -def _determine_ec2_user(parsed_args, client_manager): +def _determine_ec2_user( + parsed_args: argparse.Namespace, client_manager: Any +) -> str: """Determine a user several different ways. Assumes parsed_args has user and user_domain arguments. Attempts to find @@ -53,13 +58,13 @@ def _determine_ec2_user(parsed_args, client_manager): else: # Get the user from the current auth user = client_manager.auth_ref.user_id - return user + return cast(str, user) class CreateEC2Creds(command.ShowOne): _description = _("Create EC2 credentials") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--project', @@ -81,7 +86,9 @@ def get_parser(self, prog_name): common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity client_manager = self.app.client_manager user = _determine_ec2_user(parsed_args, client_manager) @@ -115,13 +122,14 @@ def take_action(self, parsed_args): if 'tenant_id' in info: info.update({'project_id': info.pop('tenant_id')}) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteEC2Creds(command.Command): _description = _("Delete EC2 credentials") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'access_key', @@ -137,7 +145,7 @@ def get_parser(self, prog_name): common.add_user_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client_manager = self.app.client_manager user = _determine_ec2_user(parsed_args, client_manager) result = 0 @@ -166,7 +174,7 @@ def take_action(self, parsed_args): class ListEC2Creds(command.Lister): _description = _("List EC2 credentials") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--user', @@ -176,7 +184,9 @@ def get_parser(self, prog_name): common.add_user_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client_manager = self.app.client_manager user = _determine_ec2_user(parsed_args, client_manager) @@ -200,7 +210,7 @@ def take_action(self, parsed_args): class ShowEC2Creds(command.ShowOne): _description = _("Display EC2 credentials details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'access_key', @@ -215,7 +225,9 @@ def get_parser(self, prog_name): common.add_user_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client_manager = self.app.client_manager user = _determine_ec2_user(parsed_args, client_manager) creds = client_manager.identity.ec2.get(user, parsed_args.access_key) @@ -226,4 +238,5 @@ def take_action(self, parsed_args): if 'tenant_id' in info: info.update({'project_id': info.pop('tenant_id')}) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data diff --git a/openstackclient/identity/v3/endpoint.py b/openstackclient/identity/v3/endpoint.py index 1dfa881204..93d236efbb 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -15,20 +15,27 @@ """Identity v3 Endpoint action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.identity.v3 import endpoint as _endpoint +from openstack.identity.v3 import service as _service +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common - LOG = logging.getLogger(__name__) -def _format_endpoint(endpoint, service): +def _format_endpoint( + endpoint: _endpoint.Endpoint, service: _service.Service +) -> tuple[tuple[str, ...], Any]: columns = ( 'is_enabled', 'id', @@ -58,7 +65,7 @@ def _format_endpoint(endpoint, service): class AddProjectToEndpoint(command.Command): _description = _("Associate a project to an endpoint") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpoint', @@ -75,24 +82,28 @@ def get_parser(self, prog_name): common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - endpoint = utils.find_resource(client.endpoints, parsed_args.endpoint) + endpoint_id = identity_client.find_endpoint( + parsed_args.endpoint, ignore_missing=False + ).id - project = common.find_project( - client, parsed_args.project, parsed_args.project_domain + project_id = common.find_project_id_sdk( + identity_client, parsed_args.project, parsed_args.project_domain ) - client.endpoint_filter.add_endpoint_to_project( - project=project.id, endpoint=endpoint.id + identity_client.associate_endpoint_with_project( + project=project_id, endpoint=endpoint_id ) class CreateEndpoint(command.ShowOne): _description = _("Create new endpoint") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service', @@ -131,8 +142,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) service = common.find_service_sdk(identity_client, parsed_args.service) kwargs = {} @@ -154,7 +169,7 @@ def take_action(self, parsed_args): class DeleteEndpoint(command.Command): _description = _("Delete endpoint(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpoint', @@ -164,12 +179,16 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) result = 0 for i in parsed_args.endpoint: try: - endpoint_id = identity_client.find_endpoint(i).id + endpoint_id = identity_client.find_endpoint( + i, ignore_missing=False + ).id identity_client.delete_endpoint(endpoint_id) except Exception as e: result += 1 @@ -193,7 +212,7 @@ def take_action(self, parsed_args): class ListEndpoint(command.Lister): _description = _("List endpoints") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--service', @@ -225,65 +244,95 @@ def get_parser(self, prog_name): common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) endpoint = None if parsed_args.endpoint: - endpoint = identity_client.find_endpoint(parsed_args.endpoint) - project = None + endpoint = identity_client.find_endpoint( + parsed_args.endpoint, ignore_missing=False + ) + + project_domain_id = None + if parsed_args.project_domain: + project_domain_id = common._find_sdk_id( + identity_client.find_domain, + name_or_id=parsed_args.project_domain, + ) + + project_id = None if parsed_args.project: - project = identity_client.find_project( - parsed_args.project, - parsed_args.project_domain, + project_id = common._find_sdk_id( + identity_client.find_project, + name_or_id=common._get_token_resource( + identity_client, 'project', parsed_args.project + ), + domain_id=project_domain_id, ) + # FIXME(stephenfin): This belongs under the project list command if endpoint: column_headers: tuple[str, ...] = ('ID', 'Name') columns: tuple[str, ...] = ('id', 'name') - data = identity_client.endpoint_projects(endpoint=endpoint.id) - else: - column_headers = ( - 'ID', - 'Region', - 'Service Name', - 'Service Type', - 'Enabled', - 'Interface', - 'URL', + projects = identity_client.endpoint_projects(endpoint=endpoint.id) + return ( + column_headers, + ( + utils.get_item_properties( + s, + columns, + formatters={}, + ) + for s in projects + ), ) - columns = ( - 'id', - 'region_id', - 'service_name', - 'service_type', - 'is_enabled', - 'interface', - 'url', + + column_headers = ( + 'ID', + 'Region', + 'Service Name', + 'Service Type', + 'Enabled', + 'Interface', + 'URL', + ) + columns = ( + 'id', + 'region_id', + 'service_name', + 'service_type', + 'is_enabled', + 'interface', + 'url', + ) + kwargs = {} + if parsed_args.service: + service = common.find_service_sdk( + identity_client, parsed_args.service ) - kwargs = {} - if parsed_args.service: - service = common.find_service_sdk( - identity_client, parsed_args.service - ) - kwargs['service_id'] = service.id - if parsed_args.interface: - kwargs['interface'] = parsed_args.interface - if parsed_args.region: - region = identity_client.get_region(parsed_args.region) - kwargs['region_id'] = region.id - - if project: - data = list( - identity_client.project_endpoints(project=project.id) - ) - else: - data = list(identity_client.endpoints(**kwargs)) + kwargs['service_id'] = service.id + if parsed_args.interface: + kwargs['interface'] = parsed_args.interface + if parsed_args.region: + region = identity_client.get_region(parsed_args.region) + kwargs['region_id'] = region.id - for ep in data: - service = identity_client.find_service(ep.service_id) - ep.service_name = getattr(service, 'name', '') - ep.service_type = service.type + data: list[_endpoint.Endpoint] + if project_id: + data = list(identity_client.project_endpoints(project=project_id)) + else: + data = list(identity_client.endpoints(**kwargs)) + + for ep in data: + service = identity_client.find_service( + ep.service_id, ignore_missing=False + ) + setattr(ep, 'service_name', getattr(service, 'name', '')) + setattr(ep, 'service_type', service.type) return ( column_headers, @@ -301,7 +350,7 @@ def take_action(self, parsed_args): class RemoveProjectFromEndpoint(command.Command): _description = _("Dissociate a project from an endpoint") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpoint', @@ -320,24 +369,28 @@ def get_parser(self, prog_name): common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - endpoint = utils.find_resource(client.endpoints, parsed_args.endpoint) + endpoint_id = identity_client.find_endpoint( + parsed_args.endpoint, ignore_missing=False + ).id - project = common.find_project( - client, parsed_args.project, parsed_args.project_domain + project_id = common.find_project_id_sdk( + identity_client, parsed_args.project, parsed_args.project_domain ) - client.endpoint_filter.delete_endpoint_from_project( - project=project.id, endpoint=endpoint.id + identity_client.disassociate_endpoint_from_project( + project=project_id, endpoint=endpoint_id ) class SetEndpoint(command.Command): _description = _("Set endpoint properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpoint', @@ -380,11 +433,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity - endpoint = identity_client.find_endpoint(parsed_args.endpoint) + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + endpoint = identity_client.find_endpoint( + parsed_args.endpoint, ignore_missing=False + ) - kwargs = {} + kwargs: dict[str, object] = {} if parsed_args.service: service = common.find_service_sdk( @@ -415,7 +472,7 @@ def take_action(self, parsed_args): class ShowEndpoint(command.ShowOne): _description = _("Display endpoint details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpoint', @@ -427,9 +484,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity - endpoint = identity_client.find_endpoint(parsed_args.endpoint) + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + endpoint = identity_client.find_endpoint( + parsed_args.endpoint, ignore_missing=False + ) service = common.find_service_sdk(identity_client, endpoint.service_id) diff --git a/openstackclient/identity/v3/endpoint_group.py b/openstackclient/identity/v3/endpoint_group.py index e4611a1673..abbb39eb13 100644 --- a/openstackclient/identity/v3/endpoint_group.py +++ b/openstackclient/identity/v3/endpoint_group.py @@ -13,13 +13,16 @@ """Identity v3 Endpoint Group action implementations""" +import argparse +from collections.abc import Iterable, Sequence import json import logging +from typing import Any -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common @@ -30,7 +33,7 @@ class _FiltersReader: _description = _("Helper class capable of reading filters from files") - def _read_filters(self, path): + def _read_filters(self, path: str) -> Any: """Read and parse rules from path Expect the file to contain a valid JSON structure. @@ -62,7 +65,7 @@ def _read_filters(self, path): class AddProjectToEndpointGroup(command.Command): _description = _("Add a project to an endpoint group") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpointgroup', @@ -77,7 +80,7 @@ def get_parser(self, prog_name): common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.identity endpointgroup = utils.find_resource( @@ -96,7 +99,7 @@ def take_action(self, parsed_args): class CreateEndpointGroup(command.ShowOne, _FiltersReader): _description = _("Create new endpoint group") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -114,7 +117,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity filters = None @@ -130,13 +135,14 @@ def take_action(self, parsed_args): info = {} endpoint_group._info.pop('links') info.update(endpoint_group._info) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteEndpointGroup(command.Command): _description = _("Delete endpoint group(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpointgroup', @@ -146,7 +152,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity result = 0 for i in parsed_args.endpointgroup: @@ -176,7 +182,7 @@ def take_action(self, parsed_args): class ListEndpointGroup(command.Lister): _description = _("List endpoint groups") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) list_group = parser.add_mutually_exclusive_group() list_group.add_argument( @@ -196,7 +202,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.identity endpointgroup = None @@ -241,7 +249,7 @@ def take_action(self, parsed_args): class RemoveProjectFromEndpointGroup(command.Command): _description = _("Remove project from endpoint group") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpointgroup', @@ -256,7 +264,7 @@ def get_parser(self, prog_name): common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.identity endpointgroup = utils.find_resource( @@ -275,7 +283,7 @@ def take_action(self, parsed_args): class SetEndpointGroup(command.Command, _FiltersReader): _description = _("Set endpoint group properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpointgroup', @@ -300,7 +308,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity endpointgroup = utils.find_resource( identity_client.endpoint_groups, parsed_args.endpointgroup @@ -321,7 +329,7 @@ def take_action(self, parsed_args): class ShowEndpointGroup(command.ShowOne): _description = _("Display endpoint group details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'endpointgroup', @@ -330,7 +338,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity endpoint_group = utils.find_resource( identity_client.endpoint_groups, parsed_args.endpointgroup @@ -339,4 +349,5 @@ def take_action(self, parsed_args): info = {} endpoint_group._info.pop('links') info.update(endpoint_group._info) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data diff --git a/openstackclient/identity/v3/federation_protocol.py b/openstackclient/identity/v3/federation_protocol.py index 4e980860db..2dbfac10a4 100644 --- a/openstackclient/identity/v3/federation_protocol.py +++ b/openstackclient/identity/v3/federation_protocol.py @@ -14,22 +14,37 @@ """Identity v3 Protocols actions implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.identity.v3 import federation_protocol as _federation_protocol +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ - LOG = logging.getLogger(__name__) +def _format_protocol( + protocol: _federation_protocol.FederationProtocol, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: + columns = ('name', 'idp_id', 'mapping_id') + column_headers = ('id', 'identity_provider', 'mapping') + return ( + column_headers, + utils.get_item_properties(protocol, columns), + ) + + class CreateProtocol(command.ShowOne): _description = _("Create new federation protocol") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'federation_protocol', @@ -57,28 +72,26 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - protocol = identity_client.federation.protocols.create( - protocol_id=parsed_args.federation_protocol, - identity_provider=parsed_args.identity_provider, - mapping=parsed_args.mapping, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' ) - info = dict(protocol._info) - # NOTE(marek-denis): Identity provider is not included in a response - # from Keystone, however it should be listed to the user. Add it - # manually to the output list, simply reusing value provided by the - # user. - info['identity_provider'] = parsed_args.identity_provider - info['mapping'] = info.pop('mapping_id') - info.pop('links', None) - return zip(*sorted(info.items())) + + protocol = identity_client.create_federation_protocol( + name=parsed_args.federation_protocol, + idp=parsed_args.identity_provider, + mapping_id=parsed_args.mapping, + ) + + return _format_protocol(protocol) class DeleteProtocol(command.Command): _description = _("Delete federation protocol(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'federation_protocol', @@ -98,13 +111,18 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + result = 0 for i in parsed_args.federation_protocol: try: - identity_client.federation.protocols.delete( - parsed_args.identity_provider, i + identity_client.delete_federation_protocol( + idp=parsed_args.identity_provider, + protocol=i, + ignore_missing=False, ) except Exception as e: result += 1 @@ -128,7 +146,7 @@ def take_action(self, parsed_args): class ListProtocols(command.Lister): _description = _("List federation protocols") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--identity-provider', @@ -139,10 +157,14 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - protocols = identity_client.federation.protocols.list( + protocols = identity_client.federation_protocols( parsed_args.identity_provider ) columns = ('id', 'mapping') @@ -157,7 +179,7 @@ def take_action(self, parsed_args): class SetProtocol(command.Command): _description = _("Set federation protocol properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'federation_protocol', @@ -180,28 +202,27 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - protocol = identity_client.federation.protocols.update( - parsed_args.identity_provider, - parsed_args.federation_protocol, - parsed_args.mapping, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' ) - info = dict(protocol._info) - # NOTE(marek-denis): Identity provider is not included in a response - # from Keystone, however it should be listed to the user. Add it - # manually to the output list, simply reusing value provided by the - # user. - info['identity_provider'] = parsed_args.identity_provider - info['mapping'] = info.pop('mapping_id') - return zip(*sorted(info.items())) + + kwargs = {'idp_id': parsed_args.identity_provider} + if parsed_args.federation_protocol: + kwargs['name'] = parsed_args.federation_protocol + if parsed_args.mapping: + kwargs['mapping_id'] = parsed_args.mapping + + protocol = identity_client.update_federation_protocol(**kwargs) + return _format_protocol(protocol) class ShowProtocol(command.ShowOne): _description = _("Display federation protocol details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'federation_protocol', @@ -219,13 +240,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - protocol = identity_client.federation.protocols.get( - parsed_args.identity_provider, parsed_args.federation_protocol + protocol = identity_client.get_federation_protocol( + idp=parsed_args.identity_provider, + protocol=parsed_args.federation_protocol, ) - info = dict(protocol._info) - info['mapping'] = info.pop('mapping_id') - info.pop('links', None) - return zip(*sorted(info.items())) + return _format_protocol(protocol) diff --git a/openstackclient/identity/v3/group.py b/openstackclient/identity/v3/group.py index d0112064b1..766f31886b 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -15,24 +15,49 @@ """Group action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from keystoneauth1 import exceptions as ks_exc -from osc_lib.command import command +from openstack import exceptions as sdk_exc +from openstack.identity.v3 import group as _group +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common - LOG = logging.getLogger(__name__) +def _format_group( + group: _group.Group, +) -> tuple[tuple[str, ...], Iterable[Any]]: + columns = ( + 'description', + 'domain_id', + 'id', + 'name', + ) + column_headers = ( + 'description', + 'domain_id', + 'id', + 'name', + ) + return ( + column_headers, + utils.get_item_properties(group, columns), + ) + + class AddUserToGroup(command.Command): _description = _("Add user to group") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group', @@ -52,20 +77,22 @@ def get_parser(self, prog_name): common.add_user_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - group_id = common.find_group( + group_id = common.find_group_id_sdk( identity_client, parsed_args.group, parsed_args.group_domain - ).id + ) result = 0 for i in parsed_args.user: try: - user_id = common.find_user( + user_id = common.find_user_id_sdk( identity_client, i, parsed_args.user_domain - ).id - identity_client.users.add_to_group(user_id, group_id) + ) + identity_client.add_user_to_group(user_id, group_id) except Exception as e: result += 1 msg = _("%(user)s not added to group %(group)s: %(e)s") % { @@ -92,7 +119,7 @@ def take_action(self, parsed_args): class CheckUserInGroup(command.Command): _description = _("Check user membership in group") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group', @@ -108,39 +135,50 @@ def get_parser(self, prog_name): common.add_user_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - user_id = common.find_user( - identity_client, parsed_args.user, parsed_args.user_domain - ).id - group_id = common.find_group( - identity_client, parsed_args.group, parsed_args.group_domain - ).id + user_id = common.find_user_id_sdk( + identity_client, + parsed_args.user, + parsed_args.user_domain, + validate_actor_existence=False, + ) + group_id = common.find_group_id_sdk( + identity_client, + parsed_args.group, + parsed_args.group_domain, + validate_actor_existence=False, + ) + user_in_group = False try: - identity_client.users.check_in_group(user_id, group_id) - except ks_exc.http.HTTPClientError as e: - if e.http_status == 403 or e.http_status == 404: - msg = _("%(user)s not in group %(group)s\n") % { - 'user': parsed_args.user, - 'group': parsed_args.group, - } - self.app.stderr.write(msg) - else: - raise e - else: + user_in_group = identity_client.check_user_in_group( + user_id, group_id + ) + except sdk_exc.ForbiddenException: + # Assume False if forbidden + pass + if user_in_group: msg = _("%(user)s in group %(group)s\n") % { 'user': parsed_args.user, 'group': parsed_args.group, } self.app.stdout.write(msg) + else: + msg = _("%(user)s not in group %(group)s\n") % { + 'user': parsed_args.user, + 'group': parsed_args.group, + } + self.app.stderr.write(msg) class CreateGroup(command.ShowOne): _description = _("Create new group") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -164,36 +202,49 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - domain = None + kwargs = {} + if parsed_args.name: + kwargs['name'] = parsed_args.name + if parsed_args.description: + kwargs['description'] = parsed_args.description if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain).id + kwargs['domain_id'] = common.find_domain_id_sdk( + identity_client, parsed_args.domain + ) try: - group = identity_client.groups.create( - name=parsed_args.name, - domain=domain, - description=parsed_args.description, - ) - except ks_exc.Conflict: + group = identity_client.create_group(**kwargs) + except sdk_exc.ConflictException: if parsed_args.or_show: - group = utils.find_resource( - identity_client.groups, parsed_args.name, domain_id=domain - ) + if parsed_args.domain: + group = identity_client.find_group( + parsed_args.name, + domain_id=parsed_args.domain, + ignore_missing=False, + ) + else: + group = identity_client.find_group( + parsed_args.name, + ignore_missing=False, + ) LOG.info(_('Returning existing group %s'), group.name) else: raise - group._info.pop('links') - return zip(*sorted(group._info.items())) + return _format_group(group) class DeleteGroup(command.Command): _description = _("Delete group(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'groups', @@ -208,16 +259,18 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) errors = 0 for group in parsed_args.groups: try: - group_obj = common.find_group( + group_id = common.find_group_id_sdk( identity_client, group, parsed_args.domain ) - identity_client.groups.delete(group_obj.id) + identity_client.delete_group(group_id) except Exception as e: errors += 1 LOG.error( @@ -240,7 +293,7 @@ def take_action(self, parsed_args): class ListGroup(command.Lister): _description = _("List groups") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--domain', @@ -261,30 +314,39 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) domain = None if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain).id + domain = common.find_domain_id_sdk( + identity_client, parsed_args.domain + ) - if parsed_args.user: - user = common.find_user( + if not parsed_args.user: + if domain: + data = list(identity_client.groups(domain_id=domain)) + else: + data = list(identity_client.groups()) + else: + user = common.find_user_id_sdk( identity_client, parsed_args.user, parsed_args.user_domain, - ).id - else: - user = None + ) + # NOTE(0weng): The API doesn't actually support filtering + # additionally by domain_id, so this doesn't really do + # anything. + data = list(identity_client.user_groups(user)) # List groups columns: tuple[str, ...] = ('ID', 'Name') if parsed_args.long: columns += ('Domain ID', 'Description') - data = identity_client.groups.list( - domain=domain, - user=user, - ) return ( columns, @@ -302,7 +364,7 @@ def take_action(self, parsed_args): class RemoveUserFromGroup(command.Command): _description = _("Remove user from group") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group', @@ -322,20 +384,22 @@ def get_parser(self, prog_name): common.add_user_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - group_id = common.find_group( + group_id = common.find_group_id_sdk( identity_client, parsed_args.group, parsed_args.group_domain - ).id + ) result = 0 for i in parsed_args.user: try: - user_id = common.find_user( + user_id = common.find_user_id_sdk( identity_client, i, parsed_args.user_domain - ).id - identity_client.users.remove_from_group(user_id, group_id) + ) + identity_client.remove_user_from_group(user_id, group_id) except Exception as e: result += 1 msg = _("%(user)s not removed from group %(group)s: %(e)s") % { @@ -362,7 +426,7 @@ def take_action(self, parsed_args): class SetGroup(command.Command): _description = _("Set group properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group', @@ -386,9 +450,11 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - group = common.find_group( + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + group = common.find_group_id_sdk( identity_client, parsed_args.group, parsed_args.domain ) kwargs = {} @@ -397,13 +463,13 @@ def take_action(self, parsed_args): if parsed_args.description: kwargs['description'] = parsed_args.description - identity_client.groups.update(group.id, **kwargs) + identity_client.update_group(group, **kwargs) class ShowGroup(command.ShowOne): _description = _("Display group details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group', @@ -417,14 +483,23 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - group = common.find_group( - identity_client, - parsed_args.group, - domain_name_or_id=parsed_args.domain, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' ) - group._info.pop('links') - return zip(*sorted(group._info.items())) + if parsed_args.domain: + domain = common.find_domain_id_sdk( + identity_client, parsed_args.domain + ) + group = identity_client.find_group( + parsed_args.group, domain_id=domain, ignore_missing=False + ) + else: + group = identity_client.find_group( + parsed_args.group, ignore_missing=False + ) + + return _format_group(group) diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index d0c3243628..bf28a8dcfc 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -13,13 +13,18 @@ """Identity v3 IdentityProvider action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +from openstack.identity.v3 import identity_provider as _identity_provider +from openstack import utils as sdk_utils from osc_lib.cli import format_columns -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common @@ -27,10 +32,37 @@ LOG = logging.getLogger(__name__) +def _format_identity_provider( + idp: _identity_provider.IdentityProvider, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: + columns = ( + 'authorization_ttl', + 'description', + 'domain_id', + 'is_enabled', + 'name', + 'remote_ids', + ) + column_headers = ( + 'authorization_ttl', + 'description', + 'domain_id', + 'enabled', + 'id', + 'remote_ids', + ) + return ( + column_headers, + utils.get_item_properties( + idp, columns, formatters={'remote_ids': format_columns.ListColumn} + ), + ) + + class CreateIdentityProvider(command.ShowOne): _description = _("Create new identity provider") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'identity_provider_id', @@ -41,6 +73,7 @@ def get_parser(self, prog_name): identity_remote_id_provider.add_argument( '--remote-id', metavar='', + dest='remote_ids', action='append', help=_( 'Remote IDs to associate with the Identity Provider ' @@ -97,27 +130,34 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + kwargs = {'is_enabled': parsed_args.enabled} + if parsed_args.identity_provider_id: + kwargs['id'] = parsed_args.identity_provider_id + if parsed_args.description: + kwargs['description'] = parsed_args.description + if parsed_args.remote_id_file: file_content = utils.read_blob_file_contents( parsed_args.remote_id_file ) remote_ids = file_content.splitlines() - remote_ids = list(map(str.strip, remote_ids)) - else: - remote_ids = ( - parsed_args.remote_id if parsed_args.remote_id else None - ) + kwargs['remote_ids'] = list(map(str.strip, remote_ids)) + elif parsed_args.remote_ids: + kwargs['remote_ids'] = parsed_args.remote_ids - domain_id = None if parsed_args.domain: - domain_id = common.find_domain( - identity_client, parsed_args.domain - ).id + kwargs['domain_id'] = common.find_domain_id_sdk( + identity_client, + parsed_args.domain, + validate_actor_existence=False, + ) - # TODO(pas-ha) actually check for 3.14 microversion - kwargs = {} auth_ttl = parsed_args.authorization_ttl if auth_ttl is not None: if auth_ttl < 0: @@ -127,25 +167,15 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) kwargs['authorization_ttl'] = auth_ttl - idp = identity_client.federation.identity_providers.create( - id=parsed_args.identity_provider_id, - remote_ids=remote_ids, - description=parsed_args.description, - domain_id=domain_id, - enabled=parsed_args.enabled, - **kwargs, - ) + idp = identity_client.create_identity_provider(**kwargs) - idp._info.pop('links', None) - remote_ids = format_columns.ListColumn(idp._info.pop('remote_ids', [])) - idp._info['remote_ids'] = remote_ids - return zip(*sorted(idp._info.items())) + return _format_identity_provider(idp) class DeleteIdentityProvider(command.Command): _description = _("Delete identity provider(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'identity_provider', @@ -155,17 +185,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) result = 0 for i in parsed_args.identity_provider: try: - identity_client.federation.identity_providers.delete(i) + identity_client.delete_identity_provider(i) except Exception as e: result += 1 LOG.error( _( - "Failed to delete identity providers with " + "Failed to delete identity provider with " "name or ID '%(provider)s': %(e)s" ), {'provider': i, 'e': e}, @@ -182,34 +214,39 @@ def take_action(self, parsed_args): class ListIdentityProvider(command.Lister): _description = _("List identity providers") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--id', metavar='', - help=_('The Identity Providers’ ID attribute'), + help=_('Filter identity providers by ID'), ) parser.add_argument( '--enabled', dest='enabled', action='store_true', - help=_('The Identity Providers that are enabled will be returned'), + help=_('List only enabled identity providers'), ) return parser - def take_action(self, parsed_args): - columns = ('ID', 'Enabled', 'Domain ID', 'Description') - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + columns = ('id', 'is_enabled', 'domain_id', 'description') + column_headers = ('ID', 'Enabled', 'Domain ID', 'Description') + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {} if parsed_args.id: kwargs['id'] = parsed_args.id if parsed_args.enabled: - kwargs['enabled'] = True + kwargs['is_enabled'] = True - data = identity_client.federation.identity_providers.list(**kwargs) + data = identity_client.identity_providers(**kwargs) return ( - columns, + column_headers, ( utils.get_item_properties( s, @@ -224,7 +261,7 @@ def take_action(self, parsed_args): class SetIdentityProvider(command.Command): _description = _("Set identity provider properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'identity_provider', @@ -240,6 +277,7 @@ def get_parser(self, prog_name): identity_remote_id_provider.add_argument( '--remote-id', metavar='', + dest='remote_ids', action='append', help=_( 'Remote IDs to associate with the Identity Provider ' @@ -277,8 +315,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - federation_client = self.app.client_manager.identity.federation + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) # Always set remote_ids if either is passed in if parsed_args.remote_id_file: @@ -287,21 +327,22 @@ def take_action(self, parsed_args): ) remote_ids = file_content.splitlines() remote_ids = list(map(str.strip, remote_ids)) - elif parsed_args.remote_id: - remote_ids = parsed_args.remote_id + elif parsed_args.remote_ids: + remote_ids = parsed_args.remote_ids # Setup keyword args for the client kwargs = {} if parsed_args.description: kwargs['description'] = parsed_args.description if parsed_args.enable: - kwargs['enabled'] = True + kwargs['is_enabled'] = True if parsed_args.disable: - kwargs['enabled'] = False - if parsed_args.remote_id_file or parsed_args.remote_id: + kwargs['is_enabled'] = False + if parsed_args.remote_id_file or parsed_args.remote_ids: kwargs['remote_ids'] = remote_ids - # TODO(pas-ha) actually check for 3.14 microversion + # NOTE(0weng): This is now possible in SDK! An option should be added. + # Original comment: # TODO(pas-ha) make it possible to reset authorization_ttl # back to None value. # Currently not possible as filter_kwargs decorator in @@ -316,7 +357,7 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) kwargs['authorization_ttl'] = auth_ttl - federation_client.identity_providers.update( + identity_client.update_identity_provider( parsed_args.identity_provider, **kwargs ) @@ -324,7 +365,7 @@ def take_action(self, parsed_args): class ShowIdentityProvider(command.ShowOne): _description = _("Display identity provider details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'identity_provider', @@ -333,15 +374,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - idp = utils.find_resource( - identity_client.federation.identity_providers, - parsed_args.identity_provider, - id=parsed_args.identity_provider, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + idp = identity_client.get_identity_provider( + parsed_args.identity_provider ) - idp._info.pop('links', None) - remote_ids = format_columns.ListColumn(idp._info.pop('remote_ids', [])) - idp._info['remote_ids'] = remote_ids - return zip(*sorted(idp._info.items())) + return _format_identity_provider(idp) diff --git a/openstackclient/identity/v3/implied_role.py b/openstackclient/identity/v3/implied_role.py index 3958896b09..4587c450e4 100644 --- a/openstackclient/identity/v3/implied_role.py +++ b/openstackclient/identity/v3/implied_role.py @@ -15,17 +15,22 @@ """Identity v3 Implied Role action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -def _get_role_ids(identity_client, parsed_args): +def _get_role_ids( + identity_client: Any, parsed_args: argparse.Namespace +) -> tuple[Any, Any]: """Return prior and implied role id(s) If prior and implied role id(s) are retrievable from identity @@ -50,7 +55,7 @@ def _get_role_ids(identity_client, parsed_args): class CreateImpliedRole(command.ShowOne): _description = _("Creates an association between prior and implied roles") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'role', @@ -65,7 +70,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity (prior_role_id, implied_role_id) = _get_role_ids( identity_client, parsed_args @@ -74,13 +81,16 @@ def take_action(self, parsed_args): prior_role_id, implied_role_id ) response._info.pop('links', None) - return zip(*sorted([(k, v['id']) for k, v in response._info.items()])) + col_headers, col_data = zip( + *sorted([(k, v['id']) for k, v in response._info.items()]) + ) + return col_headers, col_data class DeleteImpliedRole(command.Command): _description = _("Deletes an association between prior and implied roles") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'role', @@ -95,7 +105,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity (prior_role_id, implied_role_id) = _get_role_ids( identity_client, parsed_args @@ -112,12 +122,14 @@ class ListImpliedRole(command.Lister): 'Implied Role Name', ] - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) return parser - def take_action(self, parsed_args): - def _list_implied(response): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + def _list_implied(response: Any) -> Iterable[tuple[Any, ...]]: for rule in response: for implies in rule.implies: yield ( diff --git a/openstackclient/identity/v3/limit.py b/openstackclient/identity/v3/limit.py index 7f2fe885d1..e46218f572 100644 --- a/openstackclient/identity/v3/limit.py +++ b/openstackclient/identity/v3/limit.py @@ -13,22 +13,51 @@ """Limits action implementations.""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.identity.v3 import limit as _limit +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common as common_utils LOG = logging.getLogger(__name__) +def _format_limit( + limit: _limit.Limit, +) -> tuple[tuple[str, ...], Iterable[Any]]: + columns = ( + "description", + "id", + "project_id", + "region_id", + "resource_limit", + "resource_name", + "service_id", + ) + column_headers = ( + "description", + "id", + "project_id", + "region_id", + "resource_limit", + "resource_name", + "service_id", + ) + return (column_headers, utils.get_item_properties(limit, columns)) + + class CreateLimit(command.ShowOne): _description = _("Create a limit") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--description', @@ -46,6 +75,7 @@ def get_parser(self, prog_name): required=True, help=_('Project to associate the resource limit to'), ) + common_utils.add_project_domain_option_to_parser(parser) parser.add_argument( '--service', metavar='', @@ -66,54 +96,44 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - project = common_utils.find_project( - identity_client, parsed_args.project + kwargs = { + "resource_name": parsed_args.resource_name, + "resource_limit": parsed_args.resource_limit, + } + if parsed_args.description: + kwargs["description"] = parsed_args.description + + kwargs["project_id"] = common_utils.find_project_id_sdk( + identity_client, + parsed_args.project, + domain_name_or_id=parsed_args.project_domain, ) - service = common_utils.find_service( + + kwargs["service_id"] = common_utils.find_service_sdk( identity_client, parsed_args.service - ) - region = None + ).id + if parsed_args.region: - if 'None' not in parsed_args.region: - # NOTE (vishakha): Due to bug #1799153 and for any another - # related case where GET resource API does not support the - # filter by name, osc_lib.utils.find_resource() method cannot - # be used because that method try to fall back to list all the - # resource if requested resource cannot be get via name. Which - # ends up with NoUniqueMatch error. - # So osc_lib.utils.find_resource() function cannot be used for - # 'regions', using common_utils.get_resource() instead. - region = common_utils.get_resource( - identity_client.regions, parsed_args.region - ) - else: - self.log.warning( - _( - "Passing 'None' to indicate no region is deprecated. " - "Instead, don't pass --region." - ) - ) + kwargs["region_id"] = identity_client.get_region( + parsed_args.region + ).id - limit = identity_client.limits.create( - project, - service, - parsed_args.resource_name, - parsed_args.resource_limit, - description=parsed_args.description, - region=region, - ) + limit = identity_client.create_limit(**kwargs) - limit._info.pop('links', None) - return zip(*sorted(limit._info.items())) + return _format_limit(limit) class ListLimit(command.Lister): _description = _("List limits") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--service', @@ -136,50 +156,45 @@ def get_parser(self, prog_name): metavar='', help=_('List resource limits associated with project'), ) + common_utils.add_project_domain_option_to_parser(parser) + return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - service = None + kwargs: dict[str, object] = {} if parsed_args.service: - service = common_utils.find_service( + kwargs["service_id"] = common_utils.find_service_sdk( identity_client, parsed_args.service ) - region = None + if parsed_args.region: - if 'None' not in parsed_args.region: - # NOTE (vishakha): Due to bug #1799153 and for any another - # related case where GET resource API does not support the - # filter by name, osc_lib.utils.find_resource() method cannot - # be used because that method try to fall back to list all the - # resource if requested resource cannot be get via name. Which - # ends up with NoUniqueMatch error. - # So osc_lib.utils.find_resource() function cannot be used for - # 'regions', using common_utils.get_resource() instead. - region = common_utils.get_resource( - identity_client.regions, parsed_args.region - ) - else: - self.log.warning( - _( - "Passing 'None' to indicate no region is deprecated. " - "Instead, don't pass --region." - ) - ) + kwargs["region_id"] = identity_client.get_region( + parsed_args.region + ).id - project = None if parsed_args.project: - project = utils.find_resource( - identity_client.projects, parsed_args.project + project_domain_id = None + if parsed_args.project_domain: + project_domain_id = common_utils.find_domain_id_sdk( + identity_client, parsed_args.project_domain + ) + + kwargs["project_id"] = common_utils._find_sdk_id( + identity_client.find_project, + name_or_id=parsed_args.project, + domain_id=project_domain_id, ) - limits = identity_client.limits.list( - service=service, - resource_name=parsed_args.resource_name, - region=region, - project=project, - ) + if parsed_args.resource_name: + kwargs["resource_name"] = parsed_args.resource_name + + limits = identity_client.limits(**kwargs) columns = ( 'ID', @@ -199,7 +214,7 @@ def take_action(self, parsed_args): class ShowLimit(command.ShowOne): _description = _("Display limit details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'limit_id', @@ -208,17 +223,20 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - limit = identity_client.limits.get(parsed_args.limit_id) - limit._info.pop('links', None) - return zip(*sorted(limit._info.items())) + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + limit = identity_client.get_limit(parsed_args.limit_id) + return _format_limit(limit) class SetLimit(command.ShowOne): _description = _("Update information about a limit") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'limit_id', @@ -239,40 +257,48 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - limit = identity_client.limits.update( - parsed_args.limit_id, - description=parsed_args.description, - resource_limit=parsed_args.resource_limit, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' ) - limit._info.pop('links', None) + kwargs = {} + if parsed_args.description: + kwargs["description"] = parsed_args.description + if parsed_args.resource_limit: + kwargs["resource_limit"] = parsed_args.resource_limit + limit = identity_client.update_limit(parsed_args.limit_id, **kwargs) - return zip(*sorted(limit._info.items())) + return _format_limit(limit) class DeleteLimit(command.Command): _description = _("Delete a limit") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'limit_id', metavar='', nargs="+", - help=_('Limit to delete (ID)'), + help=_( + 'Limit to delete (ID) ' + '(repeat option to remove multiple limits)' + ), ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) errors = 0 for limit_id in parsed_args.limit_id: try: - identity_client.limits.delete(limit_id) + identity_client.delete_limit(limit_id) except Exception as e: errors += 1 LOG.error( diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py index 8c2d0bf8df..00cb84a5eb 100644 --- a/openstackclient/identity/v3/mapping.py +++ b/openstackclient/identity/v3/mapping.py @@ -15,23 +15,39 @@ """Identity v3 federation mapping action implementations""" +import argparse +from collections.abc import Iterable, Sequence import json import logging +from typing import Any -from osc_lib.command import command +from openstack.identity.v3 import mapping as _mapping +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) +def _format_mapping( + mapping: _mapping.Mapping, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: + columns = ('id', 'rules', 'schema_version') + column_headers = ('id', 'rules', 'schema_version') + return ( + column_headers, + utils.get_item_properties(mapping, columns), + ) + + class _RulesReader: _description = _("Helper class capable of reading rules from files") - def _read_rules(self, path): + def _read_rules(self, path: str) -> Any: """Read and parse rules from path Expect the file to contain a valid JSON structure. @@ -82,7 +98,9 @@ def _read_rules(self, path): return rules @staticmethod - def add_federated_schema_version_option(parser): + def add_federated_schema_version_option( + parser: argparse.ArgumentParser, + ) -> None: parser.add_argument( '--schema-version', metavar='', @@ -100,7 +118,7 @@ def add_federated_schema_version_option(parser): class CreateMapping(command.ShowOne, _RulesReader): _description = _("Create new mapping") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'mapping', @@ -116,24 +134,27 @@ def get_parser(self, prog_name): _RulesReader.add_federated_schema_version_option(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) rules = self._read_rules(parsed_args.rules) - mapping = identity_client.federation.mappings.create( - mapping_id=parsed_args.mapping, + mapping = identity_client.create_mapping( + id=parsed_args.mapping, rules=rules, schema_version=parsed_args.schema_version, ) - mapping._info.pop('links', None) - return zip(*sorted(mapping._info.items())) + return _format_mapping(mapping) class DeleteMapping(command.Command): _description = _("Delete mapping(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'mapping', @@ -143,12 +164,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) result = 0 for i in parsed_args.mapping: try: - identity_client.federation.mappings.delete(i) + identity_client.delete_mapping(i, ignore_missing=False) except Exception as e: result += 1 LOG.error( @@ -171,12 +194,16 @@ def take_action(self, parsed_args): class ListMapping(command.Lister): _description = _("List mappings") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: # NOTE(marek-denis): Since rules can be long and tedious I have decided # to only list ids of the mappings. If somebody wants to check the # rules, (s)he should show specific ones. - identity_client = self.app.client_manager.identity - data = identity_client.federation.mappings.list() + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + data = identity_client.mappings() columns = ('ID', 'schema_version') items = [utils.get_item_properties(s, columns) for s in data] return (columns, items) @@ -185,7 +212,7 @@ def take_action(self, parsed_args): class SetMapping(command.Command, _RulesReader): _description = _("Set mapping properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'mapping', @@ -201,24 +228,24 @@ def get_parser(self, prog_name): _RulesReader.add_federated_schema_version_option(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) rules = self._read_rules(parsed_args.rules) - mapping = identity_client.federation.mappings.update( + identity_client.update_mapping( mapping=parsed_args.mapping, rules=rules, schema_version=parsed_args.schema_version, ) - mapping._info.pop('links', None) - class ShowMapping(command.ShowOne): _description = _("Display mapping details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'mapping', @@ -227,10 +254,13 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - mapping = identity_client.federation.mappings.get(parsed_args.mapping) + mapping = identity_client.get_mapping(parsed_args.mapping) - mapping._info.pop('links', None) - return zip(*sorted(mapping._info.items())) + return _format_mapping(mapping) diff --git a/openstackclient/identity/v3/policy.py b/openstackclient/identity/v3/policy.py index d5b8fec6be..238af13260 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -15,22 +15,35 @@ """Identity v3 Policy action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.identity.v3 import policy as _policy +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) +def _format_policy( + policy: _policy.Policy, +) -> tuple[tuple[str, ...], tuple[str, ...]]: + columns = ('id', 'blob', 'type') + column_headers = ('id', 'rules', 'type') + return (column_headers, utils.get_item_properties(policy, columns)) + + class CreatePolicy(command.ShowOne): _description = _("Create new policy") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--type', @@ -48,23 +61,25 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: blob = utils.read_blob_file_contents(parsed_args.rules) - identity_client = self.app.client_manager.identity - policy = identity_client.policies.create( + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + policy = identity_client.create_policy( blob=blob, type=parsed_args.type ) - policy._info.pop('links') - policy._info.update({'rules': policy._info.pop('blob')}) - return zip(*sorted(policy._info.items())) + return _format_policy(policy) class DeletePolicy(command.Command): _description = _("Delete policy(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'policy', @@ -74,12 +89,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) result = 0 for i in parsed_args.policy: try: - identity_client.policies.delete(i) + identity_client.delete_policy(i, ignore_missing=False) except Exception as e: result += 1 LOG.error( @@ -102,7 +119,7 @@ def take_action(self, parsed_args): class ListPolicy(command.Lister): _description = _("List policies") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -112,30 +129,35 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: columns: tuple[str, ...] = ('ID', 'Type') column_headers: tuple[str, ...] = columns if parsed_args.long: columns += ('Blob',) column_headers += ('Rules',) - data = self.app.client_manager.identity.policies.list() + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + data = identity_client.policies() return ( column_headers, - ( + [ utils.get_item_properties( s, columns, formatters={}, ) for s in data - ), + ], ) class SetPolicy(command.Command): _description = _("Set policy properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'policy', @@ -154,26 +176,26 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - blob = None + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + + kwargs = {} if parsed_args.rules: - blob = utils.read_blob_file_contents(parsed_args.rules) + kwargs['blob'] = utils.read_blob_file_contents(parsed_args.rules) - kwargs = {} - if blob: - kwargs['blob'] = blob if parsed_args.type: kwargs['type'] = parsed_args.type - identity_client.policies.update(parsed_args.policy, **kwargs) + identity_client.update_policy(parsed_args.policy, **kwargs) class ShowPolicy(command.ShowOne): _description = _("Display policy details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'policy', @@ -182,12 +204,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - policy = utils.find_resource( - identity_client.policies, parsed_args.policy + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' ) + policy = identity_client.get_policy(parsed_args.policy) - policy._info.pop('links') - policy._info.update({'rules': policy._info.pop('blob')}) - return zip(*sorted(policy._info.items())) + return _format_policy(policy) diff --git a/openstackclient/identity/v3/project.py b/openstackclient/identity/v3/project.py index a057fcbd38..b5f6d28825 100644 --- a/openstackclient/identity/v3/project.py +++ b/openstackclient/identity/v3/project.py @@ -15,14 +15,20 @@ """Project action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from keystoneauth1 import exceptions as ks_exc +from openstack import exceptions as sdk_exc +from openstack.identity.v3 import project as _project +from openstack import utils as sdk_utils from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common from openstackclient.identity.v3 import tag @@ -30,10 +36,27 @@ LOG = logging.getLogger(__name__) +def _format_project( + project: _project.Project, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: + # NOTE(0weng): Projects allow unknown attributes in the body, so extract + # the column names separately. + column_headers, columns = utils.get_osc_show_columns_for_sdk_resource( + project, + {'is_enabled': 'enabled'}, + ['links', 'location', 'parents_as_ids', 'subtree_as_ids'], + ) + + return ( + column_headers, + utils.get_item_properties(project, columns), + ) + + class CreateProject(command.ShowOne): _description = _("Create new project") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -59,16 +82,21 @@ def get_parser(self, prog_name): enable_group.add_argument( '--enable', action='store_true', + dest='enabled', + default=True, help=_('Enable project'), ) enable_group.add_argument( '--disable', - action='store_true', + action='store_false', + dest='enabled', + default=True, help=_('Disable project'), ) parser.add_argument( '--property', metavar='', + dest='properties', action=parseractions.KeyValueAction, help=_( 'Add a property to ' @@ -84,29 +112,18 @@ def get_parser(self, prog_name): tag.add_tag_option_to_parser_for_create(parser, _('project')) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - domain = None - if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain).id - - parent = None - if parsed_args.parent: - parent = utils.find_resource( - identity_client.projects, - parsed_args.parent, - ).id + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - enabled = True - if parsed_args.disable: - enabled = False + kwargs = {} - options = common.get_immutable_options(parsed_args) + if parsed_args.properties: + kwargs = parsed_args.properties.copy() - kwargs = {} - if parsed_args.property: - kwargs = parsed_args.property.copy() if 'is_domain' in kwargs.keys(): if kwargs['is_domain'].lower() == "true": kwargs['is_domain'] = True @@ -115,37 +132,68 @@ def take_action(self, parsed_args): elif kwargs['is_domain'].lower() == "none": kwargs['is_domain'] = None - kwargs['tags'] = list(set(parsed_args.tags)) + if parsed_args.description: + kwargs['description'] = parsed_args.description + + if parsed_args.name: + kwargs['name'] = parsed_args.name + + domain = None + if parsed_args.domain: + domain = common.find_domain_id_sdk( + identity_client, parsed_args.domain + ) + kwargs['domain_id'] = domain + + if parsed_args.parent: + kwargs['parent_id'] = common.find_project_id_sdk( + identity_client, + parsed_args.parent, + domain_name_or_id=domain, + ) + + kwargs['is_enabled'] = parsed_args.enabled + + if parsed_args.tags: + kwargs['tags'] = list(set(parsed_args.tags)) + + if parsed_args.immutable is not None: + kwargs['options'] = {'immutable': parsed_args.immutable} try: - project = identity_client.projects.create( - name=parsed_args.name, - domain=domain, - parent=parent, - description=parsed_args.description, - enabled=enabled, - options=options, + project = identity_client.create_project( **kwargs, ) - except ks_exc.Conflict: + except sdk_exc.ConflictException: if parsed_args.or_show: - project = utils.find_resource( - identity_client.projects, - parsed_args.name, - domain_id=domain, - ) + if parsed_args.domain: + project = identity_client.find_project( + parsed_args.name, + domain_id=domain, + ignore_missing=False, + ) + else: + project = identity_client.find_project( + parsed_args.name, ignore_missing=False + ) LOG.info(_('Returning existing project %s'), project.name) else: raise - project._info.pop('links') - return zip(*sorted(project._info.items())) + return _format_project(project) class DeleteProject(command.Command): - _description = _("Delete project(s)") - - def get_parser(self, prog_name): + _description = _( + "Delete project(s). This command will remove specified " + "existing project(s) if an active user is authorized to do " + "this. If there are resources managed by other services " + "(for example, Nova, Neutron, Cinder) associated with " + "specified project(s), delete operation will proceed " + "regardless." + ) + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'projects', @@ -160,24 +208,22 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - domain = None - if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain) errors = 0 for project in parsed_args.projects: try: - if domain is not None: - project_obj = utils.find_resource( - identity_client.projects, project, domain_id=domain.id - ) - else: - project_obj = utils.find_resource( - identity_client.projects, project - ) - identity_client.projects.delete(project_obj.id) + project = common.find_project_id_sdk( + identity_client, + project, + domain_name_or_id=parsed_args.domain, + validate_actor_existence=True, + validate_domain_actor_existence=False, + ) + identity_client.delete_project(project) except Exception as e: errors += 1 LOG.error( @@ -200,7 +246,7 @@ def take_action(self, parsed_args): class ListProject(command.Lister): _description = _("List projects") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--domain', @@ -240,80 +286,115 @@ def get_parser(self, prog_name): 'keys and directions.' ), ) + parser.add_argument( + '--enabled', + action='store_true', + dest='is_enabled', + default=None, + help=_('List only enabled projects'), + ) + parser.add_argument( + '--disabled', + action='store_false', + dest='is_enabled', + default=None, + help=_('List only disabled projects'), + ) tag.add_tag_filtering_option_to_parser(parser, _('projects')) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - columns: tuple[str, ...] = ('ID', 'Name') + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + + column_headers: tuple[str, ...] = ('ID', 'Name') if parsed_args.long: - columns += ('Domain ID', 'Description', 'Enabled') + column_headers += ('Domain ID', 'Description', 'Enabled') + + columns: tuple[str, ...] = ('id', 'name') + if parsed_args.long: + columns += ('domain_id', 'description', 'is_enabled') + kwargs = {} domain_id = None if parsed_args.domain: - domain_id = common.find_domain( + domain_id = common.find_domain_id_sdk( identity_client, parsed_args.domain - ).id - kwargs['domain'] = domain_id + ) + kwargs['domain_id'] = domain_id if parsed_args.parent: - parent_id = common.find_project( - identity_client, parsed_args.parent - ).id - kwargs['parent'] = parent_id + parent_id = common.find_project_id_sdk( + identity_client, + parsed_args.parent, + domain_name_or_id=domain_id, + ) + kwargs['parent_id'] = parent_id + user = None if parsed_args.user: if parsed_args.domain: - user_id = utils.find_resource( - identity_client.users, + user = common.find_user_id_sdk( + identity_client, parsed_args.user, - domain_id=domain_id, - ).id + domain_name_or_id=domain_id, + ) else: - user_id = utils.find_resource( - identity_client.users, parsed_args.user - ).id + user = common.find_user_id_sdk( + identity_client, + parsed_args.user, + ) - kwargs['user'] = user_id + if parsed_args.is_enabled is not None: + kwargs['is_enabled'] = parsed_args.is_enabled + + if parsed_args.limit is not None: + kwargs['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + kwargs['max_items'] = parsed_args.max_items + if parsed_args.marker is not None: + kwargs['marker'] = parsed_args.marker tag.get_tag_filtering_args(parsed_args, kwargs) if parsed_args.my_projects: # NOTE(adriant): my-projects supersedes all the other filters. - kwargs = {'user': self.app.client_manager.auth_ref.user_id} + kwargs = {} + user = self.app.client_manager.auth_ref.user_id - try: - data = identity_client.projects.list(**kwargs) - except ks_exc.Forbidden: - # NOTE(adriant): if no filters, assume a forbidden is non-admin - # wanting their own project list. - if not kwargs: - user = self.app.client_manager.auth_ref.user_id - data = identity_client.projects.list(user=user) - else: - raise + if not user: + try: + data = list(identity_client.projects(**kwargs)) + except sdk_exc.ForbiddenException: + # NOTE(adriant): if no filters, assume a forbidden is non-admin + # wanting their own project list. + if not kwargs: + user = self.app.client_manager.auth_ref.user_id + assert user is not None, 'this should not happen' + data = list(identity_client.user_projects(user)) + else: + raise + else: + data = list(identity_client.user_projects(user, **kwargs)) if parsed_args.sort: - data = utils.sort_items(data, parsed_args.sort) + data = list(utils.sort_items(data, parsed_args.sort)) return ( - columns, - ( - utils.get_item_properties( - s, - columns, - formatters={}, - ) - for s in data - ), + column_headers, + (utils.get_item_properties(s, columns) for s in data), ) class SetProject(command.Command): _description = _("Set project properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'project', @@ -339,16 +420,21 @@ def get_parser(self, prog_name): enable_group.add_argument( '--enable', action='store_true', + dest='enabled', + default=None, help=_('Enable project'), ) enable_group.add_argument( '--disable', - action='store_true', + action='store_false', + dest='enabled', + default=None, help=_('Disable project'), ) parser.add_argument( '--property', metavar='', + dest='properties', action=parseractions.KeyValueAction, help=_( 'Set a property on ' @@ -359,11 +445,9 @@ def get_parser(self, prog_name): tag.add_tag_option_to_parser_for_set(parser, _('project')) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - project = common.find_project( - identity_client, parsed_args.project, parsed_args.domain + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' ) kwargs = {} @@ -371,24 +455,62 @@ def take_action(self, parsed_args): kwargs['name'] = parsed_args.name if parsed_args.description: kwargs['description'] = parsed_args.description - if parsed_args.enable: - kwargs['enabled'] = True - if parsed_args.disable: - kwargs['enabled'] = False - options = common.get_immutable_options(parsed_args) - if options: - kwargs['options'] = options - if parsed_args.property: - kwargs.update(parsed_args.property) - tag.update_tags_in_args(parsed_args, project, kwargs) + if parsed_args.enabled is not None: + kwargs['enabled'] = parsed_args.enabled + if parsed_args.immutable is not None: + kwargs['options'] = {'immutable': parsed_args.immutable} + if parsed_args.properties: + kwargs.update(parsed_args.properties) - identity_client.projects.update(project.id, **kwargs) + if parsed_args.domain: + domain = common.find_domain_id_sdk( + identity_client, + parsed_args.domain, + validate_actor_existence=False, + ) + project = identity_client.find_project( + parsed_args.project, + domain_id=domain, + ignore_missing=True, + ) + else: + project = identity_client.find_project( + parsed_args.project, + ignore_missing=True, + ) + + if ( + parsed_args.tags + or parsed_args.remove_tags + or parsed_args.clear_tags + ): + existing_tags = [] + if project: + existing_tags = project.tags + + if parsed_args.clear_tags: + kwargs['tags'] = [] + else: + existing_tags_set = set(existing_tags) + if parsed_args.remove_tags: + tags = sorted( + existing_tags_set - set(parsed_args.remove_tags) + ) + if parsed_args.tags: + tags = sorted( + existing_tags_set.union(set(parsed_args.tags)) + ) + kwargs['tags'] = tags + + project_id = project.id if project else parsed_args.project + + identity_client.update_project(project_id, **kwargs) class ShowProject(command.ShowOne): _description = _("Display project details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'project', @@ -414,32 +536,41 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - project_str = common._get_token_resource( - identity_client, 'project', parsed_args.project, parsed_args.domain + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' ) + kwargs: dict[str, Any] = {} + + domain = None if parsed_args.domain: - domain = common.find_domain(identity_client, parsed_args.domain) - project = utils.find_resource( - identity_client.projects, project_str, domain_id=domain.id - ) - else: - project = utils.find_resource( - identity_client.projects, project_str + domain = common.find_domain_id_sdk( + identity_client, parsed_args.domain ) - if parsed_args.parents or parsed_args.children: - # NOTE(RuiChen): utils.find_resource() can't pass kwargs, - # if id query hit the result at first, so call - # identity manager.get() with kwargs directly. - project = identity_client.projects.get( - project.id, - parents_as_ids=parsed_args.parents, - subtree_as_ids=parsed_args.children, - ) + kwargs['domain_id'] = domain + + # Get project id first; otherwise, find_project() can't find + # parents/children if only project name was given + project = common.find_project_id_sdk( + identity_client, + parsed_args.project, + domain_name_or_id=domain, + validate_actor_existence=False, + validate_domain_actor_existence=False, + ) + + # Include these options as query parameters if they are provided + if parsed_args.parents: + kwargs['parents_as_ids'] = True + if parsed_args.children: + kwargs['subtree_as_ids'] = True + + project_obj = identity_client.find_project( + project, **kwargs, ignore_missing=False + ) - project._info.pop('links') - return zip(*sorted(project._info.items())) + return _format_project(project_obj) diff --git a/openstackclient/identity/v3/region.py b/openstackclient/identity/v3/region.py index 40d3819819..777a248b87 100644 --- a/openstackclient/identity/v3/region.py +++ b/openstackclient/identity/v3/region.py @@ -13,19 +13,25 @@ """Identity v3 Region action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.identity.v3 import region as _region +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ - LOG = logging.getLogger(__name__) -def _format_region(region): +def _format_region( + region: _region.Region, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: columns = ('id', 'description', 'parent_region_id') column_headers = ('region', 'description', 'parent_region') return ( @@ -37,7 +43,7 @@ def _format_region(region): class CreateRegion(command.ShowOne): _description = _("Create new region") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) # NOTE(stevemar): The API supports an optional region ID, but that # seems like poor UX, we will only support user-defined IDs. @@ -58,8 +64,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) region = identity_client.create_region( id=parsed_args.region, @@ -73,7 +83,7 @@ def take_action(self, parsed_args): class DeleteRegion(command.Command): _description = _("Delete region(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'region', @@ -83,8 +93,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) result = 0 for i in parsed_args.region: try: @@ -108,7 +120,7 @@ def take_action(self, parsed_args): class ListRegion(command.Lister): _description = _("List regions") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--parent-region', @@ -117,8 +129,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {} if parsed_args.parent_region: @@ -144,7 +160,7 @@ def take_action(self, parsed_args): class SetRegion(command.Command): _description = _("Set region properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'region', @@ -163,8 +179,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {} if parsed_args.description: @@ -178,7 +196,7 @@ def take_action(self, parsed_args): class ShowRegion(command.ShowOne): _description = _("Display region details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'region', @@ -187,8 +205,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) region = identity_client.get_region(parsed_args.region) diff --git a/openstackclient/identity/v3/registered_limit.py b/openstackclient/identity/v3/registered_limit.py index 34ce2101f5..f5890cb9ec 100644 --- a/openstackclient/identity/v3/registered_limit.py +++ b/openstackclient/identity/v3/registered_limit.py @@ -13,22 +13,52 @@ """Registered limits action implementations.""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.identity.v3 import registered_limit as _registered_limit +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common as common_utils LOG = logging.getLogger(__name__) +def _format_registered_limit( + registered_limit: _registered_limit.RegisteredLimit, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: + columns = ( + 'default_limit', + 'description', + 'id', + 'region_id', + 'resource_name', + 'service_id', + ) + column_headers = ( + 'default_limit', + 'description', + 'id', + 'region_id', + 'resource_name', + 'service_id', + ) + return ( + column_headers, + utils.get_item_properties(registered_limit, columns), + ) + + class CreateRegisteredLimit(command.ShowOne): _description = _("Create a registered limit") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--description', @@ -44,7 +74,10 @@ def get_parser(self, prog_name): '--service', metavar='', required=True, - help=_('Service responsible for the resource to limit (required)'), + help=_( + 'Service responsible for the resource to limit (required) ' + '(name or ID)' + ), ) parser.add_argument( '--default-limit', @@ -60,66 +93,62 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - - service = utils.find_resource( - identity_client.services, parsed_args.service + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' ) - region = None + + kwargs = {} + + if parsed_args.description: + kwargs["description"] = parsed_args.description + + kwargs["service_id"] = common_utils.find_service_sdk( + identity_client, parsed_args.service + ).id + if parsed_args.region: - if 'None' not in parsed_args.region: - # NOTE (vishakha): Due to bug #1799153 and for any another - # related case where GET resource API does not support the - # filter by name, osc_lib.utils.find_resource() method cannot - # be used because that method try to fall back to list all the - # resource if requested resource cannot be get via name. Which - # ends up with NoUniqueMatch error. - # So osc_lib.utils.find_resource() function cannot be used for - # 'regions', using common_utils.get_resource() instead. - region = common_utils.get_resource( - identity_client.regions, parsed_args.region - ) - else: - self.log.warning( - _( - "Passing 'None' to indicate no region is deprecated. " - "Instead, don't pass --region." - ) - ) + kwargs["region_id"] = identity_client.get_region( + parsed_args.region + ).id - registered_limit = identity_client.registered_limits.create( - service, - parsed_args.resource_name, - parsed_args.default_limit, - description=parsed_args.description, - region=region, - ) + kwargs["resource_name"] = parsed_args.resource_name + kwargs["default_limit"] = parsed_args.default_limit - registered_limit._info.pop('links', None) - return zip(*sorted(registered_limit._info.items())) + registered_limit = identity_client.create_registered_limit(**kwargs) + + return _format_registered_limit(registered_limit) class DeleteRegisteredLimit(command.Command): _description = _("Delete a registered limit") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( - 'registered_limit_id', - metavar='', + 'registered_limits', + metavar='', nargs="+", - help=_('Registered limit to delete (ID)'), + help=_( + 'Registered limit(s) to delete (ID) ' + '(repeat option to remove multiple registered limits)' + ), ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) errors = 0 - for registered_limit_id in parsed_args.registered_limit_id: + for registered_limit_id in parsed_args.registered_limits: try: - identity_client.registered_limits.delete(registered_limit_id) + identity_client.delete_registered_limit( + registered_limit_id, ignore_missing=False + ) except Exception as e: errors += 1 from pprint import pprint @@ -134,7 +163,7 @@ def take_action(self, parsed_args): ) if errors > 0: - total = len(parsed_args.registered_limit_id) + total = len(parsed_args.registered_limits) msg = _( "%(errors)s of %(total)s registered limits failed to delete." ) % {'errors': errors, 'total': total} @@ -144,12 +173,14 @@ def take_action(self, parsed_args): class ListRegisteredLimit(command.Lister): _description = _("List registered limits") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--service', metavar='', - help=_('Service responsible for the resource to limit'), + help=_( + 'Service responsible for the resource to limit (name or ID)' + ), ) parser.add_argument( '--resource-name', @@ -164,41 +195,27 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - service = None + kwargs = {} if parsed_args.service: - service = common_utils.find_service( + kwargs["service_id"] = common_utils.find_service_sdk( identity_client, parsed_args.service - ) - region = None + ).id if parsed_args.region: - if 'None' not in parsed_args.region: - # NOTE (vishakha): Due to bug #1799153 and for any another - # related case where GET resource API does not support the - # filter by name, osc_lib.utils.find_resource() method cannot - # be used because that method try to fall back to list all the - # resource if requested resource cannot be get via name. Which - # ends up with NoUniqueMatch error. - # So osc_lib.utils.find_resource() function cannot be used for - # 'regions', using common_utils.get_resource() instead. - region = common_utils.get_resource( - identity_client.regions, parsed_args.region - ) - else: - self.log.warning( - _( - "Passing 'None' to indicate no region is deprecated. " - "Instead, don't pass --region." - ) - ) + kwargs["region_id"] = identity_client.get_region( + parsed_args.region + ).id - registered_limits = identity_client.registered_limits.list( - service=service, - resource_name=parsed_args.resource_name, - region=region, - ) + if parsed_args.resource_name: + kwargs["resource_name"] = parsed_args.resource_name + + registered_limits = identity_client.registered_limits(**kwargs) columns = ( 'ID', @@ -217,7 +234,7 @@ def take_action(self, parsed_args): class SetRegisteredLimit(command.ShowOne): _description = _("Update information about a registered limit") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'registered_limit_id', @@ -228,9 +245,9 @@ def get_parser(self, prog_name): '--service', metavar='', help=_( - 'Service to be updated responsible for the resource to ' - 'limit. Either --service, --resource-name or --region must ' - 'be different than existing value otherwise it will be ' + 'Service to be updated responsible for the resource to limit ' + '(name or ID). Either --service, --resource-name or --region ' + 'must be different than existing value otherwise it will be ' 'duplicate entry' ), ) @@ -267,51 +284,44 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - service = None + kwargs = {} if parsed_args.service: - service = common_utils.find_service( + kwargs["service_id"] = common_utils.find_service_sdk( identity_client, parsed_args.service - ) + ).id + + if parsed_args.resource_name: + kwargs["resource_name"] = parsed_args.resource_name + + if parsed_args.default_limit: + kwargs["default_limit"] = parsed_args.default_limit + + if parsed_args.description: + kwargs["description"] = parsed_args.description - region = None if parsed_args.region: - if 'None' not in parsed_args.region: - # NOTE (vishakha): Due to bug #1799153 and for any another - # related case where GET resource API does not support the - # filter by name, osc_lib.utils.find_resource() method cannot - # be used because that method try to fall back to list all the - # resource if requested resource cannot be get via name. Which - # ends up with NoUniqueMatch error. - # So osc_lib.utils.find_resource() function cannot be used for - # 'regions', using common_utils.get_resource() instead. - region = common_utils.get_resource( - identity_client.regions, parsed_args.region - ) - else: - self.log.warning( - _("Passing 'None' to indicate no region is deprecated.") - ) + kwargs["region_id"] = identity_client.get_region( + parsed_args.region + ).id - registered_limit = identity_client.registered_limits.update( - parsed_args.registered_limit_id, - service=service, - resource_name=parsed_args.resource_name, - default_limit=parsed_args.default_limit, - description=parsed_args.description, - region=region, + registered_limit = identity_client.update_registered_limit( + parsed_args.registered_limit_id, **kwargs ) - registered_limit._info.pop('links', None) - return zip(*sorted(registered_limit._info.items())) + return _format_registered_limit(registered_limit) class ShowRegisteredLimit(command.ShowOne): _description = _("Display registered limit details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'registered_limit_id', @@ -320,10 +330,13 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - registered_limit = identity_client.registered_limits.get( + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + registered_limit = identity_client.get_registered_limit( parsed_args.registered_limit_id ) - registered_limit._info.pop('links', None) - return zip(*sorted(registered_limit._info.items())) + return _format_registered_limit(registered_limit) diff --git a/openstackclient/identity/v3/role.py b/openstackclient/identity/v3/role.py index 61e30e5e0d..1875c56481 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -15,13 +15,18 @@ """Identity v3 Role action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from openstack import exceptions as sdk_exc -from osc_lib.command import command +from openstack.identity.v3 import role as _role +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common @@ -29,7 +34,7 @@ LOG = logging.getLogger(__name__) -def _format_role(role): +def _format_role(role: _role.Role) -> tuple[tuple[str, ...], Iterable[Any]]: columns = ( "id", "name", @@ -48,7 +53,9 @@ def _format_role(role): ) -def _add_identity_and_resource_options_to_parser(parser): +def _add_identity_and_resource_options_to_parser( + parser: argparse.ArgumentParser, +) -> None: system_or_domain_or_project = parser.add_mutually_exclusive_group() system_or_domain_or_project.add_argument( '--system', @@ -82,32 +89,14 @@ def _add_identity_and_resource_options_to_parser(parser): common.add_inherited_option_to_parser(parser) -def _find_sdk_id( - find_command, name_or_id, validate_actor_existence=True, **kwargs -): - try: - resource = find_command( - name_or_id=name_or_id, ignore_missing=False, **kwargs - ) - - # Mimic the behavior of - # openstackclient.identity.common._find_identity_resource() - # and ignore if we don't have permission to find a resource. - except sdk_exc.ForbiddenException: - return name_or_id - except sdk_exc.ResourceNotFound as exc: - if not validate_actor_existence: - return name_or_id - raise exceptions.CommandError from exc - return resource.id - - def _process_identity_and_resource_options( - parsed_args, identity_client, validate_actor_existence=True -): - def _find_user(): + parsed_args: argparse.Namespace, + identity_client: Any, + validate_actor_existence: bool = True, +) -> dict[str, Any]: + def _find_user() -> Any: domain_id = ( - _find_sdk_id( + common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.user_domain, validate_actor_existence=validate_actor_existence, @@ -115,16 +104,16 @@ def _find_user(): if parsed_args.user_domain else None ) - return _find_sdk_id( + return common._find_sdk_id( identity_client.find_user, name_or_id=parsed_args.user, validate_actor_existence=validate_actor_existence, domain_id=domain_id, ) - def _find_group(): + def _find_group() -> Any: domain_id = ( - _find_sdk_id( + common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.group_domain, validate_actor_existence=validate_actor_existence, @@ -132,16 +121,16 @@ def _find_group(): if parsed_args.group_domain else None ) - return _find_sdk_id( + return common._find_sdk_id( identity_client.find_group, name_or_id=parsed_args.group, validate_actor_existence=validate_actor_existence, domain_id=domain_id, ) - def _find_project(): + def _find_project() -> Any: domain_id = ( - _find_sdk_id( + common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.project_domain, validate_actor_existence=validate_actor_existence, @@ -149,7 +138,7 @@ def _find_project(): if parsed_args.project_domain else None ) - return _find_sdk_id( + return common._find_sdk_id( identity_client.find_project, name_or_id=parsed_args.project, validate_actor_existence=validate_actor_existence, @@ -162,7 +151,7 @@ def _find_project(): kwargs['system'] = parsed_args.system elif parsed_args.user and parsed_args.domain: kwargs['user'] = _find_user() - kwargs['domain'] = _find_sdk_id( + kwargs['domain'] = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.domain, validate_actor_existence=validate_actor_existence, @@ -175,7 +164,7 @@ def _find_project(): kwargs['system'] = parsed_args.system elif parsed_args.group and parsed_args.domain: kwargs['group'] = _find_group() - kwargs['domain'] = _find_sdk_id( + kwargs['domain'] = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.domain, validate_actor_existence=validate_actor_existence, @@ -200,7 +189,7 @@ class AddRole(command.Command): "system, a domain, or a project" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'role', @@ -211,8 +200,10 @@ def get_parser(self, prog_name): common.add_role_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) if ( not parsed_args.user @@ -228,10 +219,10 @@ def take_action(self, parsed_args): domain_id = None if parsed_args.role_domain: - domain_id = _find_sdk_id( + domain_id = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.role_domain ) - role = _find_sdk_id( + role = common._find_sdk_id( identity_client.find_role, name_or_id=parsed_args.role, domain_id=domain_id, @@ -298,7 +289,7 @@ def take_action(self, parsed_args): class CreateRole(command.ShowOne): _description = _("Create new role") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -323,20 +314,27 @@ def get_parser(self, prog_name): common.add_resource_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - create_kwargs = {} + create_kwargs: dict[str, Any] = {} if parsed_args.domain: - create_kwargs['domain_id'] = _find_sdk_id( + create_kwargs['domain_id'] = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.domain ) if parsed_args.name: create_kwargs['name'] = parsed_args.name + if parsed_args.description: create_kwargs['description'] = parsed_args.description - create_kwargs['options'] = common.get_immutable_options(parsed_args) + + if parsed_args.immutable is not None: + create_kwargs['options'] = {"immutable": parsed_args.immutable} try: role = identity_client.create_role(**create_kwargs) @@ -358,7 +356,7 @@ def take_action(self, parsed_args): class DeleteRole(command.Command): _description = _("Delete role(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'roles', @@ -373,18 +371,20 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) domain_id = None if parsed_args.domain: - domain_id = _find_sdk_id( + domain_id = common._find_sdk_id( identity_client.find_domain, parsed_args.domain ) errors = 0 for role in parsed_args.roles: try: - role_id = _find_sdk_id( + role_id = common._find_sdk_id( identity_client.find_role, name_or_id=role, domain_id=domain_id, @@ -412,7 +412,7 @@ def take_action(self, parsed_args): class ListRole(command.Lister): _description = _("List roles") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--domain', @@ -421,19 +421,26 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) if parsed_args.domain: domain = identity_client.find_domain( name_or_id=parsed_args.domain, + ignore_missing=False, ) data = identity_client.roles(domain_id=domain.id) return ( ('ID', 'Name', 'Domain'), ( - utils.get_item_properties(s, ('id', 'name')) - + (domain.name,) + ( + *utils.get_item_properties(s, ('id', 'name')), + domain.name, + ) for s in data ), ) @@ -451,7 +458,7 @@ class RemoveRole(command.Command): "Removes a role assignment from system/domain/project : user/group" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'role', @@ -463,8 +470,10 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) if ( not parsed_args.user and not parsed_args.domain @@ -479,11 +488,11 @@ def take_action(self, parsed_args): domain_id = None if parsed_args.role_domain: - domain_id = _find_sdk_id( + domain_id = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.role_domain, ) - role = _find_sdk_id( + role = common._find_sdk_id( identity_client.find_role, name_or_id=parsed_args.role, domain_id=domain_id, @@ -543,7 +552,7 @@ def take_action(self, parsed_args): class SetRole(command.Command): _description = _("Set role properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'role', @@ -568,8 +577,10 @@ def get_parser(self, prog_name): common.add_resource_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) update_kwargs = {} if parsed_args.description: @@ -579,14 +590,16 @@ def take_action(self, parsed_args): domain_id = None if parsed_args.domain: - domain_id = _find_sdk_id( + domain_id = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.domain, ) update_kwargs["domain_id"] = domain_id - update_kwargs["options"] = common.get_immutable_options(parsed_args) - role = _find_sdk_id( + if parsed_args.immutable is not None: + update_kwargs["options"] = {"immutable": parsed_args.immutable} + + role = common._find_sdk_id( identity_client.find_role, name_or_id=parsed_args.role, domain_id=domain_id, @@ -599,7 +612,7 @@ def take_action(self, parsed_args): class ShowRole(command.ShowOne): _description = _("Display role details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'role', @@ -613,12 +626,16 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) domain_id = None if parsed_args.domain: - domain_id = _find_sdk_id( + domain_id = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.domain, ) diff --git a/openstackclient/identity/v3/role_assignment.py b/openstackclient/identity/v3/role_assignment.py index 12e1527b67..e0d702d074 100644 --- a/openstackclient/identity/v3/role_assignment.py +++ b/openstackclient/identity/v3/role_assignment.py @@ -13,15 +13,23 @@ """Identity v3 Assignment action implementations""" -from openstack import exceptions as sdk_exceptions -from osc_lib.command import command +import argparse +from collections.abc import Iterable +from typing import Any +from openstack.identity.v3 import role_assignment as _role_assignment +from openstack import utils as sdk_utils +from osc_lib import exceptions + +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common -def _format_role_assignment_(assignment, include_names): - def _get_names(attr): +def _format_role_assignment_( + assignment: _role_assignment.RoleAssignment, include_names: bool +) -> tuple[Any, ...]: + def _get_names(attr: Any) -> str: return ( ( attr['name'] @@ -36,7 +44,7 @@ def _get_names(attr): else '' ) - def _get_ids(attr): + def _get_ids(attr: Any) -> str: return attr['id'] or '' if attr else '' func = _get_names if include_names else _get_ids @@ -51,19 +59,10 @@ def _get_ids(attr): ) -def _find_sdk_id(find_command, name_or_id, **kwargs): - try: - return find_command( - name_or_id=name_or_id, ignore_missing=False, **kwargs - ).id - except sdk_exceptions.ForbiddenException: - return name_or_id - - class ListRoleAssignment(command.Lister): _description = _("List role assignments") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--effective', @@ -128,19 +127,23 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) auth_ref = self.app.client_manager.auth_ref role_id = None role_domain_id = None if parsed_args.role_domain: - role_domain_id = _find_sdk_id( + role_domain_id = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.role_domain, ) if parsed_args.role: - role_id = _find_sdk_id( + role_id = common._find_sdk_id( identity_client.find_role, name_or_id=parsed_args.role, domain_id=role_domain_id, @@ -148,21 +151,23 @@ def take_action(self, parsed_args): user_domain_id = None if parsed_args.user_domain: - user_domain_id = _find_sdk_id( + user_domain_id = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.user_domain, ) user_id = None if parsed_args.user: - user_id = _find_sdk_id( + user_id = common._find_sdk_id( identity_client.find_user, name_or_id=parsed_args.user, domain_id=user_domain_id, ) elif parsed_args.authuser: if auth_ref: - user_id = _find_sdk_id( + if auth_ref.user_id is None: + raise exceptions.CommandError('missing auth info') + user_id = common._find_sdk_id( identity_client.find_user, name_or_id=auth_ref.user_id, ) @@ -173,21 +178,21 @@ def take_action(self, parsed_args): domain_id = None if parsed_args.domain: - domain_id = _find_sdk_id( + domain_id = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.domain, ) project_domain_id = None if parsed_args.project_domain: - project_domain_id = _find_sdk_id( + project_domain_id = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.project_domain, ) project_id = None if parsed_args.project: - project_id = _find_sdk_id( + project_id = common._find_sdk_id( identity_client.find_project, name_or_id=common._get_token_resource( identity_client, 'project', parsed_args.project @@ -196,21 +201,23 @@ def take_action(self, parsed_args): ) elif parsed_args.authproject: if auth_ref: - project_id = _find_sdk_id( + if auth_ref.project_id is None: + raise exceptions.CommandError('missing auth info') + project_id = common._find_sdk_id( identity_client.find_project, name_or_id=auth_ref.project_id, ) group_domain_id = None if parsed_args.group_domain: - group_domain_id = _find_sdk_id( + group_domain_id = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.group_domain, ) group_id = None if parsed_args.group: - group_id = _find_sdk_id( + group_id = common._find_sdk_id( identity_client.find_group, name_or_id=parsed_args.group, domain_id=group_domain_id, @@ -244,7 +251,7 @@ def take_action(self, parsed_args): data_parsed = [] for assignment in data: data_parsed.append( - _format_role_assignment_(assignment, include_names) + _format_role_assignment_(assignment, bool(include_names)) ) return columns, tuple(data_parsed) diff --git a/openstackclient/identity/v3/service.py b/openstackclient/identity/v3/service.py index e8f483266e..3339843793 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -15,20 +15,26 @@ """Identity v3 Service action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.identity.v3 import service as _service +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common - LOG = logging.getLogger(__name__) -def _format_service(service): +def _format_service( + service: _service.Service, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: columns = ( 'id', 'name', @@ -56,7 +62,7 @@ def _format_service(service): class CreateService(command.ShowOne): _description = _("Create new service") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'type', @@ -90,8 +96,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) service = identity_client.create_service( name=parsed_args.name, @@ -106,7 +116,7 @@ def take_action(self, parsed_args): class DeleteService(command.Command): _description = _("Delete service(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service', @@ -116,8 +126,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) result = 0 for i in parsed_args.service: try: @@ -145,7 +157,7 @@ def take_action(self, parsed_args): class ListService(command.Lister): _description = _("List services") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -155,8 +167,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) columns: tuple[str, ...] = ('id', 'name', 'type') column_headers: tuple[str, ...] = ('ID', 'Name', 'Type') @@ -175,7 +191,7 @@ def take_action(self, parsed_args): class SetService(command.Command): _description = _("Set service properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service', @@ -214,8 +230,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) service = common.find_service_sdk(identity_client, parsed_args.service) kwargs = {} @@ -234,7 +252,7 @@ def take_action(self, parsed_args): class ShowService(command.ShowOne): _description = _("Display service details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service', @@ -243,8 +261,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) service = common.find_service_sdk(identity_client, parsed_args.service) diff --git a/openstackclient/identity/v3/service_provider.py b/openstackclient/identity/v3/service_provider.py index 5b49cffaf7..a5a7d11692 100644 --- a/openstackclient/identity/v3/service_provider.py +++ b/openstackclient/identity/v3/service_provider.py @@ -13,19 +13,25 @@ """Service Provider action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.identity.v3 import service_provider as _service_provider +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ - LOG = logging.getLogger(__name__) -def _format_service_provider(sp): +def _format_service_provider( + sp: _service_provider.ServiceProvider, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: column_headers = ( 'id', 'enabled', @@ -51,7 +57,7 @@ def _format_service_provider(sp): class CreateServiceProvider(command.ShowOne): _description = _("Create new service provider") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service_provider_id', @@ -98,8 +104,12 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): - service_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + service_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {} @@ -125,7 +135,7 @@ def take_action(self, parsed_args): class DeleteServiceProvider(command.Command): _description = _("Delete service provider(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service_provider', @@ -135,8 +145,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - service_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + service_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) result = 0 for i in parsed_args.service_provider: try: @@ -162,8 +174,12 @@ def take_action(self, parsed_args): class ListServiceProvider(command.Lister): _description = _("List service providers") - def take_action(self, parsed_args): - service_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + service_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) data = service_client.service_providers() column_headers = ( @@ -191,7 +207,7 @@ def take_action(self, parsed_args): class SetServiceProvider(command.ShowOne): _description = _("Set service provider properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service_provider', @@ -233,8 +249,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - service_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + service_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {} @@ -261,7 +281,7 @@ def take_action(self, parsed_args): class ShowServiceProvider(command.ShowOne): _description = _("Display service provider details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'service_provider', @@ -270,8 +290,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - service_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + service_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) service_provider = service_client.find_service_provider( parsed_args.service_provider, ignore_missing=False, diff --git a/openstackclient/identity/v3/tag.py b/openstackclient/identity/v3/tag.py index 0909fd122b..078f4bcd55 100644 --- a/openstackclient/identity/v3/tag.py +++ b/openstackclient/identity/v3/tag.py @@ -11,16 +11,27 @@ # under the License. import argparse +from collections.abc import Sequence +from typing import Any from openstackclient.i18n import _ class _CommaListAction(argparse.Action): - def __call__(self, parser, namespace, values, option_string=None): - setattr(namespace, self.dest, values.split(',')) + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: str | Sequence[Any] | None, + option_string: str | None = None, + ) -> None: + values = values.split(',') if isinstance(values, str) else values + setattr(namespace, self.dest, values) -def add_tag_filtering_option_to_parser(parser, collection_name): +def add_tag_filtering_option_to_parser( + parser: argparse.ArgumentParser, collection_name: str +) -> None: parser.add_argument( '--tags', metavar='[,,...]', @@ -63,7 +74,9 @@ def add_tag_filtering_option_to_parser(parser, collection_name): ) -def get_tag_filtering_args(parsed_args, args): +def get_tag_filtering_args( + parsed_args: argparse.Namespace, args: dict[str, Any] +) -> None: if parsed_args.tags: args['tags'] = ','.join(parsed_args.tags) if parsed_args.tags_any: @@ -74,7 +87,9 @@ def get_tag_filtering_args(parsed_args, args): args['not-tags-any'] = ','.join(parsed_args.not_tags_any) -def add_tag_option_to_parser_for_create(parser, resource_name): +def add_tag_option_to_parser_for_create( + parser: argparse.ArgumentParser, resource_name: str +) -> None: tag_group = parser.add_mutually_exclusive_group() tag_group.add_argument( '--tag', @@ -89,7 +104,9 @@ def add_tag_option_to_parser_for_create(parser, resource_name): ) -def add_tag_option_to_parser_for_set(parser, resource_name): +def add_tag_option_to_parser_for_set( + parser: argparse.ArgumentParser, resource_name: str +) -> None: parser.add_argument( '--tag', action='append', @@ -114,6 +131,7 @@ def add_tag_option_to_parser_for_set(parser, resource_name): parser.add_argument( '--remove-tag', action='append', + dest='remove_tags', metavar='', default=[], help=_( @@ -122,14 +140,3 @@ def add_tag_option_to_parser_for_set(parser, resource_name): ) % resource_name, ) - - -def update_tags_in_args(parsed_args, obj, args): - if parsed_args.clear_tags: - args['tags'] = [] - obj.tags = [] - if parsed_args.remove_tag: - args['tags'] = sorted(set(obj.tags) - set(parsed_args.remove_tag)) - return - if parsed_args.tags: - args['tags'] = sorted(set(obj.tags).union(set(parsed_args.tags))) diff --git a/openstackclient/identity/v3/token.py b/openstackclient/identity/v3/token.py index 5500ce1d85..eaf1219c21 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -15,10 +15,14 @@ """Identity v3 Token action implementations""" -from osc_lib.command import command +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common @@ -26,7 +30,7 @@ class AuthorizeRequestToken(command.ShowOne): _description = _("Authorize a request token") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--request-key', @@ -37,6 +41,7 @@ def get_parser(self, prog_name): parser.add_argument( '--role', metavar='', + dest='roles', action='append', default=[], required=True, @@ -47,12 +52,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity # NOTE(stevemar): We want a list of role ids roles = [] - for role in parsed_args.role: + for role in parsed_args.roles: role_id = utils.find_resource( identity_client.roles, role, @@ -63,13 +70,14 @@ def take_action(self, parsed_args): parsed_args.request_key, roles ) - return zip(*sorted(verifier_pin._info.items())) + col_headers, col_data = zip(*sorted(verifier_pin._info.items())) + return col_headers, col_data class CreateAccessToken(command.ShowOne): _description = _("Create an access token") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--consumer-key', @@ -103,7 +111,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: token_client = self.app.client_manager.identity.oauth1.access_tokens access_token = token_client.create( parsed_args.consumer_key, @@ -112,13 +122,14 @@ def take_action(self, parsed_args): parsed_args.request_secret, parsed_args.verifier, ) - return zip(*sorted(access_token._info.items())) + col_headers, col_data = zip(*sorted(access_token._info.items())) + return col_headers, col_data class CreateRequestToken(command.ShowOne): _description = _("Create a request token") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--consumer-key', @@ -147,7 +158,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: identity_client = self.app.client_manager.identity if parsed_args.domain: @@ -167,7 +180,8 @@ def take_action(self, parsed_args): request_token = token_client.create( parsed_args.consumer_key, parsed_args.consumer_secret, project.id ) - return zip(*sorted(request_token._info.items())) + col_headers, col_data = zip(*sorted(request_token._info.items())) + return col_headers, col_data class IssueToken(command.ShowOne): @@ -176,11 +190,13 @@ class IssueToken(command.ShowOne): # scoped token is optional required_scope = False - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: auth_ref = self.app.client_manager.auth_ref if not auth_ref: raise exceptions.AuthorizationFailure( @@ -206,13 +222,14 @@ def take_action(self, parsed_args): # deployment system. When that happens, this will have to relay # scope information and IDs like we do for projects and domains. data['system'] = 'all' - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class RevokeToken(command.Command): _description = _("Revoke existing token") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'token', @@ -221,7 +238,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: identity_client = self.app.client_manager.identity identity_client.tokens.revoke_token(parsed_args.token) diff --git a/openstackclient/identity/v3/trust.py b/openstackclient/identity/v3/trust.py index df8e2f4ea5..a475b9bb12 100644 --- a/openstackclient/identity/v3/trust.py +++ b/openstackclient/identity/v3/trust.py @@ -13,23 +13,29 @@ """Identity v3 Trust action implementations""" +import argparse +from collections.abc import Iterable, Sequence import datetime import itertools import logging +from typing import Any from openstack import exceptions as sdk_exceptions -from osc_lib.command import command +from openstack.identity.v3 import trust as _trust +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common - LOG = logging.getLogger(__name__) -def _format_trust(trust): +def _format_trust( + trust: _trust.Trust, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: columns = ( 'expires_at', 'id', @@ -51,7 +57,7 @@ def _format_trust(trust): class CreateTrust(command.ShowOne): _description = _("Create new trust") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'trustor', @@ -112,10 +118,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - kwargs = {} + kwargs: dict[str, Any] = {} # NOTE(stevemar): Find the two users, project and roles that # are necessary for making a trust usable, the API dictates that @@ -179,7 +189,9 @@ def take_action(self, parsed_args): roles = [] for role in parsed_args.roles: try: - role_id = identity_client.find_role(role).id + role_id = identity_client.find_role( + role, ignore_missing=False + ).id except sdk_exceptions.ForbiddenException: role_id = role roles.append({"id": role_id}) @@ -201,7 +213,7 @@ def take_action(self, parsed_args): class DeleteTrust(command.Command): _description = _("Delete trust(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'trust', @@ -211,8 +223,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) errors = 0 for trust in parsed_args.trust: @@ -243,7 +257,7 @@ def take_action(self, parsed_args): class ListTrust(command.Lister): _description = _("List trusts") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--trustor', @@ -273,8 +287,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) auth_ref = self.app.client_manager.auth_ref if parsed_args.authuser and any( @@ -355,9 +373,10 @@ def take_action(self, parsed_args): except sdk_exceptions.ForbiddenException: trustee = parsed_args.trustee - data = identity_client.trusts( - trustor_user_id=trustor, - trustee_user_id=trustee, + data = list( + identity_client.trusts( + trustor_user_id=trustor, trustee_user_id=trustee + ) ) column_headers = ( @@ -393,7 +412,7 @@ def take_action(self, parsed_args): class ShowTrust(command.ShowOne): _description = _("Display trust details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'trust', @@ -402,8 +421,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) trust = identity_client.find_trust( parsed_args.trust, ignore_missing=False ) diff --git a/openstackclient/identity/v3/unscoped_saml.py b/openstackclient/identity/v3/unscoped_saml.py index d26035d35e..9ac65a3359 100644 --- a/openstackclient/identity/v3/unscoped_saml.py +++ b/openstackclient/identity/v3/unscoped_saml.py @@ -17,16 +17,22 @@ the user can list domains and projects they are allowed to access, and request a scoped token.""" -from osc_lib.command import command +import argparse +from collections.abc import Iterable +from typing import Any + from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ class ListAccessibleDomains(command.Lister): _description = _("List accessible domains") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: columns = ('ID', 'Enabled', 'Name', 'Description') identity_client = self.app.client_manager.identity data = identity_client.federation.domains.list() @@ -46,7 +52,9 @@ def take_action(self, parsed_args): class ListAccessibleProjects(command.Lister): _description = _("List accessible projects") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: columns = ('ID', 'Domain ID', 'Enabled', 'Name') identity_client = self.app.client_manager.identity data = identity_client.federation.projects.list() diff --git a/openstackclient/identity/v3/user.py b/openstackclient/identity/v3/user.py index 5be69bc652..808f0cf146 100644 --- a/openstackclient/identity/v3/user.py +++ b/openstackclient/identity/v3/user.py @@ -15,23 +15,27 @@ """Identity v3 User action implementations""" +import argparse +from collections.abc import Iterable, Sequence import copy import logging -import typing as ty +from typing import Any from openstack import exceptions as sdk_exc -from osc_lib.command import command +from openstack.identity.v3 import user as _user +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common - LOG = logging.getLogger(__name__) -def _format_user(user): +def _format_user(user: _user.User) -> tuple[tuple[str, ...], tuple[Any, ...]]: columns = ( 'default_project_id', 'domain_id', @@ -41,6 +45,7 @@ def _format_user(user): 'name', 'description', 'password_expires_at', + 'options', ) column_headers = ( 'default_project_id', @@ -51,6 +56,7 @@ def _format_user(user): 'name', 'description', 'password_expires_at', + 'options', ) return ( column_headers, @@ -58,8 +64,10 @@ def _format_user(user): ) -def _get_options_for_user(identity_client, parsed_args): - options: dict[str, ty.Any] = {} +def _get_options_for_user( + identity_client: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: + options: dict[str, Any] = {} if parsed_args.ignore_lockout_failure_attempts: options['ignore_lockout_failure_attempts'] = True if parsed_args.no_ignore_lockout_failure_attempts: @@ -80,16 +88,16 @@ def _get_options_for_user(identity_client, parsed_args): options['multi_factor_auth_enabled'] = True if parsed_args.disable_multi_factor_auth: options['multi_factor_auth_enabled'] = False - if parsed_args.multi_factor_auth_rule: + if parsed_args.multi_factor_auth_rules: auth_rules = [ - rule.split(",") for rule in parsed_args.multi_factor_auth_rule + rule.split(",") for rule in parsed_args.multi_factor_auth_rules ] if auth_rules: options['multi_factor_auth_rules'] = auth_rules return options -def _add_user_options(parser): +def _add_user_options(parser: argparse.ArgumentParser) -> None: # Add additional user options parser.add_argument( @@ -173,7 +181,8 @@ def _add_user_options(parser): parser.add_argument( '--multi-factor-auth-rule', metavar='', - action="append", + dest='multi_factor_auth_rules', + action='append', default=[], help=_( 'Set multi-factor auth rules. For example, to set a rule ' @@ -188,7 +197,7 @@ def _add_user_options(parser): class CreateUser(command.ShowOne): _description = _("Create new user") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -247,10 +256,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - kwargs = {} + kwargs: dict[str, Any] = {} domain_id = None if parsed_args.domain: @@ -289,13 +302,16 @@ def take_action(self, parsed_args): elif parsed_args.password_prompt: password = utils.get_password(self.app.stdin) - if not parsed_args.password: + if not password: LOG.warning( _( "No password was supplied, authentication will fail " "when a user does not have a password." ) ) + else: + kwargs['password'] = password + options = _get_options_for_user(identity_client, parsed_args) if options: kwargs['options'] = options @@ -304,7 +320,6 @@ def take_action(self, parsed_args): user = identity_client.create_user( is_enabled=is_enabled, name=parsed_args.name, - password=password, **kwargs, ) except sdk_exc.ConflictException: @@ -328,7 +343,7 @@ def take_action(self, parsed_args): class DeleteUser(command.Command): _description = _("Delete user(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'users', @@ -343,8 +358,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) domain = None if parsed_args.domain: @@ -388,7 +405,7 @@ def take_action(self, parsed_args): class ListUser(command.Lister): _description = _("List users") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--domain', @@ -412,15 +429,47 @@ def get_parser(self, prog_name): default=False, help=_('List additional fields in output'), ) + parser.add_argument( + '--enabled', + action='store_true', + dest='is_enabled', + default=None, + help=_( + 'List only enabled users, does nothing with ' + '--project and --group' + ), + ) + parser.add_argument( + '--disabled', + action='store_false', + dest='is_enabled', + default=None, + help=_( + 'List only disabled users, does nothing with ' + '--project and --group' + ), + ) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + if parsed_args.project and ( + parsed_args.limit is not None or parsed_args.marker is not None + ): + msg = _('--limit and --marker are not supported with --project') + raise exceptions.CommandError(msg) + + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) domain = None if parsed_args.domain: domain = identity_client.find_domain( name_or_id=parsed_args.domain, + ignore_missing=False, ).id group = None @@ -431,6 +480,18 @@ def take_action(self, parsed_args): ignore_missing=False, ).id + if parsed_args.is_enabled is not None: + enabled = parsed_args.is_enabled + + pagination_kwargs: dict[str, Any] = {} + if parsed_args.limit is not None: + pagination_kwargs['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + pagination_kwargs['max_items'] = parsed_args.max_items + if parsed_args.marker is not None: + pagination_kwargs['marker'] = parsed_args.marker + + data: list[_user.User] if parsed_args.project: if domain is not None: project = identity_client.find_project( @@ -444,15 +505,13 @@ def take_action(self, parsed_args): ignore_missing=False, ).id - assignments = identity_client.role_assignments_filter( - project=project - ) - # NOTE(stevemar): If a user has more than one role on a project # then they will have two entries in the returned data. Since we # are looking for any role, let's just track unique user IDs. user_ids = set() - for assignment in assignments: + for assignment in identity_client.role_assignments( + scope_project_id=project + ): if assignment.user: user_ids.add(assignment.user['id']) @@ -462,16 +521,29 @@ def take_action(self, parsed_args): for user_id in user_ids: user = identity_client.find_user(user_id, ignore_missing=False) data.append(user) - elif parsed_args.group: - data = identity_client.group_users( - domain_id=domain, - group=group, + assert group is not None + data = list( + identity_client.group_users( + domain_id=domain, group=group, **pagination_kwargs + ) ) else: - data = identity_client.users( - domain_id=domain, - ) + if parsed_args.is_enabled is not None: + data = list( + identity_client.users( + domain_id=domain, + is_enabled=enabled, + **pagination_kwargs, + ) + ) + else: + data = list( + identity_client.users( + domain_id=domain, + **pagination_kwargs, + ) + ) # Column handling if parsed_args.long: @@ -508,7 +580,7 @@ def take_action(self, parsed_args): class SetUser(command.Command): _description = _("Set user properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'user', @@ -570,8 +642,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) if parsed_args.password_prompt: parsed_args.password = utils.get_password(self.app.stdin) @@ -643,7 +717,7 @@ class SetPasswordUser(command.Command): required_scope = False - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--password', @@ -657,10 +731,20 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) conn = self.app.client_manager.sdk_connection - user_id = conn.config.get_auth().get_user_id(conn.identity) + auth = conn.config.get_auth() + if auth is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') + + user_id = auth.get_user_id(conn.session) + if user_id is None: + # this will never happen + raise exceptions.CommandError('invalid authentication info') # FIXME(gyee): there are two scenarios: # @@ -702,7 +786,7 @@ def take_action(self, parsed_args): ) ) - identity_client.update_user( + identity_client.update_password( user=user_id, current_password=current_password, password=password, @@ -712,7 +796,7 @@ def take_action(self, parsed_args): class ShowUser(command.ShowOne): _description = _("Display user details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'user', @@ -726,8 +810,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.sdk_connection.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) user_str = common._get_token_resource( identity_client, 'user', parsed_args.user, parsed_args.domain diff --git a/openstackclient/image/client.py b/openstackclient/image/client.py index f75b3e6820..4e66080dd3 100644 --- a/openstackclient/image/client.py +++ b/openstackclient/image/client.py @@ -12,7 +12,9 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import logging +from typing import Any from osc_lib import utils @@ -27,7 +29,7 @@ API_VERSIONS = ('1', '2') -def make_client(instance): +def make_client(instance: Any) -> Any: """Returns an image service client.""" LOG.debug( 'Image client initialized using OpenStack SDK: %s', @@ -36,7 +38,9 @@ def make_client(instance): return instance.sdk_connection.image -def build_option_parser(parser): +def build_option_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: """Hook to add global options""" parser.add_argument( '--os-image-api-version', @@ -48,6 +52,6 @@ def build_option_parser(parser): return parser -def check_api_version(check_version): +def check_api_version(check_version: str) -> bool: # SDK supports auto-negotiation for us: always return True return True diff --git a/openstackclient/image/v1/image.py b/openstackclient/image/v1/image.py index 490b4122f6..569b782545 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -19,15 +19,17 @@ import logging import os import sys +from collections.abc import Iterable, Sequence +from typing import Any from cliff import columns as cliff_columns from osc_lib.api import utils as api_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ CONTAINER_CHOICES = ["ami", "ari", "aki", "bare", "docker", "ova", "ovf"] @@ -50,7 +52,7 @@ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns(item: Any) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = {'is_protected': 'protected', 'owner_id': 'owner'} hidden_columns = [ 'location', @@ -67,11 +69,8 @@ def _get_columns(item): ) -_formatters = {} - - -class HumanReadableSizeColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class HumanReadableSizeColumn(cliff_columns.FormattableColumn[int]): + def human_readable(self) -> str: """Return a formatted visibility string :rtype: @@ -84,8 +83,8 @@ def human_readable(self): return '' -class VisibilityColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class VisibilityColumn(cliff_columns.FormattableColumn[bool]): + def human_readable(self) -> str: """Return a formatted visibility string :rtype: @@ -101,7 +100,7 @@ def human_readable(self): class CreateImage(command.ShowOne): _description = _("Create/upload an image") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "name", @@ -241,7 +240,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image # Build an attribute dict from the parsed args, only include @@ -297,18 +298,16 @@ def take_action(self, parsed_args): if not parsed_args.location and not parsed_args.copy_from: if parsed_args.volume: volume_client = self.app.client_manager.volume - source_volume = utils.find_resource( - volume_client.volumes, - parsed_args.volume, + source_volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False ) - response, body = volume_client.volumes.upload_to_image( - source_volume.id, - parsed_args.force, + info = source_volume.upload_to_image( + volume_client, parsed_args.name, - parsed_args.container_format, - parsed_args.disk_format, + force=parsed_args.force, + container_format=parsed_args.container_format, + disk_format=parsed_args.disk_format, ) - info = body['os-volume_upload_image'] elif parsed_args.file: # Send an open file handle to glanceclient so it will # do a chunked transfer @@ -316,10 +315,10 @@ def take_action(self, parsed_args): else: # Read file from stdin if not sys.stdin.isatty(): - if os.name == "nt": + if sys.platform == "win32": import msvcrt - msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) # type: ignore + msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) if hasattr(sys.stdin, 'buffer'): kwargs['data'] = sys.stdin.buffer else: @@ -340,9 +339,12 @@ def take_action(self, parsed_args): if image: display_columns, columns = _get_columns(image) - _formatters['properties'] = format_columns.DictColumn data = utils.get_item_properties( - image, columns, formatters=_formatters + image, + columns, + formatters={ + 'properties': format_columns.DictColumn, + }, ) return (display_columns, data) elif info: @@ -350,13 +352,15 @@ def take_action(self, parsed_args): info['properties'] = format_columns.DictColumn( info.get('properties', {}) ) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data + return ((), ()) class DeleteImage(command.Command): _description = _("Delete image(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "images", @@ -366,7 +370,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: result = 0 image_client = self.app.client_manager.image for image in parsed_args.images: @@ -395,7 +399,7 @@ def take_action(self, parsed_args): class ListImage(command.Lister): _description = _("List available images") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( @@ -452,7 +456,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: image_client = self.app.client_manager.image kwargs = {} @@ -493,19 +499,19 @@ def take_action(self, parsed_args): column_headers = columns # List of image data received - data = list(image_client.images(**kwargs)) + images = list(image_client.images(**kwargs)) if parsed_args.property: # NOTE(dtroyer): coerce to a list to subscript it in py3 - attr, value = list(parsed_args.property.items())[0] + attr, value = next(iter(parsed_args.property.items())) api_utils.simple_filter( - data, + images, attr=attr, value=value, property_field='properties', ) - data = utils.sort_items(data, parsed_args.sort) + data = utils.sort_items(images, parsed_args.sort) return ( column_headers, @@ -526,8 +532,18 @@ def take_action(self, parsed_args): class SaveImage(command.Command): _description = _("Save an image locally") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) + parser.add_argument( + "--chunk-size", + type=int, + default=1024, + metavar="", + help=_( + "Size in bytes to read from the wire and buffer at one " + "time (default: 1024)" + ), + ) parser.add_argument( "--file", metavar="", @@ -540,7 +556,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image image = image_client.find_image( parsed_args.image, ignore_missing=False @@ -550,13 +566,18 @@ def take_action(self, parsed_args): if output_file is None: output_file = getattr(sys.stdout, "buffer", sys.stdout) - image_client.download_image(image.id, stream=True, output=output_file) + image_client.download_image( + image.id, + stream=True, + output=output_file, + chunk_size=parsed_args.chunk_size, + ) class SetImage(command.Command): _description = _("Set image properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "image", @@ -686,7 +707,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image kwargs = {} @@ -742,20 +763,19 @@ def take_action(self, parsed_args): if not parsed_args.location and not parsed_args.copy_from: if parsed_args.volume: volume_client = self.app.client_manager.volume - source_volume = utils.find_resource( - volume_client.volumes, - parsed_args.volume, + source_volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False ) - volume_client.volumes.upload_to_image( - source_volume.id, - parsed_args.force, + source_volume.upload_to_image( + volume_client, parsed_args.image, - ( + force=parsed_args.force, + container_format=( parsed_args.container_format if parsed_args.container_format else image.container_format ), - ( + disk_format=( parsed_args.disk_format if parsed_args.disk_format else image.disk_format @@ -769,10 +789,10 @@ def take_action(self, parsed_args): # Read file from stdin if sys.stdin.isatty() is not True: if parsed_args.stdin: - if os.name == "nt": + if sys.platform == "win32": import msvcrt - msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) # type: ignore + msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) if hasattr(sys.stdin, 'buffer'): kwargs['data'] = sys.stdin.buffer else: @@ -803,7 +823,7 @@ def take_action(self, parsed_args): class ShowImage(command.ShowOne): _description = _("Display image details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--human-readable", @@ -818,17 +838,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image image = image_client.find_image( parsed_args.image, ignore_missing=False ) + formatters: dict[str, type[cliff_columns.FormattableColumn[Any]]] = { + 'properties': format_columns.DictColumn, + } if parsed_args.human_readable: - _formatters['size'] = HumanReadableSizeColumn + formatters['size'] = HumanReadableSizeColumn display_columns, columns = _get_columns(image) - _formatters['properties'] = format_columns.DictColumn - data = utils.get_item_properties( - image, columns, formatters=_formatters - ) + data = utils.get_item_properties(image, columns, formatters=formatters) return (display_columns, data) diff --git a/openstackclient/image/v2/cache.py b/openstackclient/image/v2/cache.py index 33a2f4fade..7d7516f628 100644 --- a/openstackclient/image/v2/cache.py +++ b/openstackclient/image/v2/cache.py @@ -13,21 +13,24 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse +from collections.abc import Iterable, Sequence import copy import datetime import logging +from typing import Any -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -def _format_image_cache(cached_images): +def _format_image_cache(cached_images: dict[str, Any]) -> list[dict[str, Any]]: """Format image cache to make it more consistent with OSC operations.""" image_list = [] @@ -37,14 +40,18 @@ def _format_image_cache(cached_images): image_obj = copy.deepcopy(image) image_obj['state'] = 'cached' image_obj['last_accessed'] = ( - datetime.datetime.utcfromtimestamp( - image['last_accessed'] - ).isoformat() + datetime.datetime.fromtimestamp( + image['last_accessed'], tz=datetime.UTC + ) + .replace(tzinfo=None) + .isoformat() ) image_obj['last_modified'] = ( - datetime.datetime.utcfromtimestamp( - image['last_modified'] - ).isoformat() + datetime.datetime.fromtimestamp( + image['last_modified'], tz=datetime.UTC + ) + .replace(tzinfo=None) + .isoformat() ) image_list.append(image_obj) elif item == "queued_images": @@ -66,11 +73,13 @@ def _format_image_cache(cached_images): class ListCachedImage(command.Lister): _description = _("Get Cache State") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: image_client = self.app.client_manager.image # List of Cache data received @@ -107,7 +116,7 @@ def take_action(self, parsed_args): class QueueCachedImage(command.Command): _description = _("Queue image(s) for caching.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "images", @@ -117,7 +126,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image failures = 0 @@ -147,7 +156,7 @@ def take_action(self, parsed_args): class DeleteCachedImage(command.Command): _description = _("Delete image(s) from cache") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "images", @@ -157,7 +166,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: failures = 0 image_client = self.app.client_manager.image for image in parsed_args.images: @@ -186,7 +195,7 @@ def take_action(self, parsed_args): class ClearCachedImage(command.Command): _description = _("Clear all images from cache, queue or both") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--cache", @@ -206,7 +215,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image target = parsed_args.target diff --git a/openstackclient/image/v2/image.py b/openstackclient/image/v2/image.py index 7dbd1609f9..e6062122cf 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -17,22 +17,25 @@ import argparse from base64 import b64encode +from collections.abc import Iterable, Sequence import copy import logging import os import sys -import typing as ty +from typing import Any +import urllib.parse from openstack import exceptions as sdk_exceptions from openstack.image import image_signer +from openstack.image.v2 import image as _image from openstack import utils as sdk_utils from osc_lib.api import utils as api_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.common import progressbar from openstackclient.i18n import _ @@ -54,16 +57,31 @@ "iso", "ploop", ] +# A list of openstacksdk Image object attributes (values) that named +# differently from actual properties stored by Glance (keys). +IMAGE_ATTRIBUTES_CUSTOM_NAMES = { + 'os_hidden': 'is_hidden', + 'protected': 'is_protected', + 'os_hash_algo': 'hash_algo', + 'os_hash_value': 'hash_value', + 'img_config_drive': 'needs_config_drive', + 'os_secure_boot': 'needs_secure_boot', + 'hw_vif_multiqueue_enabled': 'is_hw_vif_multiqueue_enabled', + 'hw_boot_menu': 'is_hw_boot_menu_enabled', + 'auto_disk_config': 'has_auto_disk_config', +} MEMBER_STATUS_CHOICES = ["accepted", "pending", "rejected", "all"] LOG = logging.getLogger(__name__) -def _format_image(image, human_readable=False): +def _format_image( + image: _image.Image, human_readable: bool = False +) -> dict[str, object]: """Format an image to make it more consistent with OSC operations.""" info = {} - properties = {} + properties: dict[str, object] = {} # the only fields we're not including is "links", "tags" and the properties fields_to_show = [ @@ -84,29 +102,32 @@ def _format_image(image, human_readable=False): 'virtual_size', 'min_ram', 'schema', + 'is_hidden', + 'hash_algo', + 'hash_value', ] # TODO(gtema/anybody): actually it should be possible to drop this method, # since SDK already delivers a proper object - image = image.to_dict(ignore_none=True, original_names=True) + data = image.to_dict(ignore_none=True, original_names=True) # split out the usual key and the properties which are top-level - for key in image: + for key in data: if key in fields_to_show: - info[key] = image.get(key) + info[key] = data[key] elif key == 'tags': continue # handle this later elif key == 'properties': # NOTE(gtema): flatten content of properties - properties.update(image.get(key)) + properties.update(data[key]) elif key != 'location': - properties[key] = image.get(key) + properties[key] = data[key] if human_readable: - info['size'] = utils.format_size(image['size']) + info['size'] = utils.format_size(data['size']) # format the tags if they are there - info['tags'] = format_columns.ListColumn(image.get('tags')) + info['tags'] = format_columns.ListColumn(data.get('tags') or []) # add properties back into the dictionary as a top-level key if properties: @@ -120,7 +141,7 @@ def _format_image(image, human_readable=False): } -def _get_member_columns(item): +def _get_member_columns(item: Any) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = {'image_id': 'image_id'} hidden_columns = ['id', 'location', 'name'] return utils.get_osc_show_columns_for_sdk_resource( @@ -130,7 +151,7 @@ def _get_member_columns(item): ) -def get_data_from_stdin(): +def get_data_from_stdin() -> Any: # distinguish cases where: # (1) stdin is not valid (as in cron jobs): # openstack ... <&- @@ -149,10 +170,10 @@ def get_data_from_stdin(): image = sys.stdin if hasattr(sys.stdin, 'buffer'): image = sys.stdin.buffer - if os.name == "nt": + if sys.platform == "win32": import msvcrt - msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) # type: ignore + msvcrt.setmode(sys.stdin.fileno(), os.O_BINARY) return image else: @@ -160,7 +181,7 @@ def get_data_from_stdin(): return None -def _add_is_protected_args(parser): +def _add_is_protected_args(parser: argparse.ArgumentParser) -> None: protected_group = parser.add_mutually_exclusive_group() protected_group.add_argument( "--protected", @@ -178,7 +199,7 @@ def _add_is_protected_args(parser): ) -def _add_visibility_args(parser): +def _add_visibility_args(parser: argparse.ArgumentParser) -> None: public_group = parser.add_mutually_exclusive_group() public_group.add_argument( "--public", @@ -224,7 +245,7 @@ def _add_visibility_args(parser): class AddProjectToImage(command.ShowOne): _description = _("Associate project with image") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "image", @@ -239,15 +260,17 @@ def get_parser(self, prog_name): identity_common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) image = image_client.find_image( parsed_args.image, @@ -270,7 +293,7 @@ class CreateImage(command.ShowOne): deadopts = ('size', 'location', 'copy-from', 'checksum', 'store') - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) # TODO(bunting): There are additional arguments that v1 supported # that v2 either doesn't support or supports weirdly. @@ -423,13 +446,15 @@ def get_parser(self, prog_name): ) return parser - def _take_action_image(self, parsed_args): - identity_client = self.app.client_manager.identity + def _take_action_image( + self, parsed_args: argparse.Namespace + ) -> dict[str, Any]: image_client = self.app.client_manager.image + identity_client = self.app.client_manager.sdk_connection.identity # Build an attribute dict from the parsed args, only include # attributes that were actually set on the command line - kwargs: dict[str, ty.Any] = {'allow_duplicates': True} + kwargs: dict[str, Any] = {'allow_duplicates': True} copy_attrs = ( 'name', 'id', @@ -466,11 +491,11 @@ def _take_action_image(self, parsed_args): kwargs['visibility'] = parsed_args.visibility if parsed_args.project: - kwargs['owner_id'] = identity_common.find_project( + kwargs['owner_id'] = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) if parsed_args.use_import: kwargs['use_import'] = True @@ -534,7 +559,7 @@ def _take_action_image(self, parsed_args): sign_cert_id = parsed_args.sign_cert_id signer = image_signer.ImageSigner() try: - pw = utils.get_password( + pw: str | None = utils.get_password( self.app.stdin, prompt=( "Please enter private key password, leave " @@ -545,12 +570,11 @@ def _take_action_image(self, parsed_args): if not pw or len(pw) < 1: pw = None - else: - # load_private_key() requires the password to be - # passed as bytes - pw = pw.encode() - signer.load_private_key(sign_key_path, password=pw) + signer.load_private_key( + sign_key_path, + password=pw.encode() if pw else None, + ) except Exception: msg = _( "Error during sign operation: private key " @@ -576,8 +600,12 @@ def _take_action_image(self, parsed_args): image = image_client.get_image(image) return _format_image(image) - def _take_action_volume(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def _take_action_volume( + self, parsed_args: argparse.Namespace + ) -> dict[str, Any]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) unsupported_opts = { # 'name', # 'name' is a positional argument and will always exist @@ -606,12 +634,12 @@ def _take_action_volume(self, parsed_args): ) # TODO(stephenfin): These should be an error in a future # version - LOG.warning(msg % opt_name) + LOG.warning(msg, opt_name) source_volume = volume_client.find_volume( parsed_args.volume, ignore_missing=False ) - kwargs: dict[str, ty.Any] = { + kwargs: dict[str, Any] = { 'visibility': None, 'protected': None, } @@ -627,7 +655,7 @@ def _take_action_volume(self, parsed_args): kwargs['visibility'] = parsed_args.visibility or 'private' kwargs['protected'] = parsed_args.is_protected or False - response = volume_client.upload_volume_to_image( + response: dict[str, Any] = volume_client.upload_volume_to_image( source_volume.id, parsed_args.name, force=parsed_args.force, @@ -643,7 +671,9 @@ def _take_action_volume(self, parsed_args): return info - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: for deadopt in self.deadopts: if getattr(parsed_args, deadopt.replace('-', '_'), None): msg = _( @@ -657,13 +687,14 @@ def take_action(self, parsed_args): else: info = self._take_action_image(parsed_args) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteImage(command.Command): _description = _("Delete image(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "images", @@ -680,7 +711,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: result = 0 image_client = self.app.client_manager.image for image in parsed_args.images: @@ -716,7 +747,7 @@ def take_action(self, parsed_args): class ListImage(command.Lister): _description = _("List available images") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) public_group = parser.add_mutually_exclusive_group() public_group.add_argument( @@ -845,15 +876,19 @@ def get_parser(self, prog_name): pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: image_client = self.app.client_manager.image + identity_client = self.app.client_manager.sdk_connection.identity kwargs = {} if parsed_args.visibility is not None: kwargs['visibility'] = parsed_args.visibility if parsed_args.limit: kwargs['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + kwargs['max_items'] = parsed_args.max_items if parsed_args.marker: kwargs['marker'] = image_client.find_image( parsed_args.marker, @@ -869,11 +904,11 @@ def take_action(self, parsed_args): kwargs['tag'] = parsed_args.tag project_id = None if parsed_args.project: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) kwargs['owner'] = project_id if parsed_args.is_hidden: kwargs['is_hidden'] = parsed_args.is_hidden @@ -889,6 +924,8 @@ def take_action(self, parsed_args): 'visibility', 'is_protected', 'owner_id', + 'hash_algo', + 'hash_value', 'tags', ) column_headers: tuple[str, ...] = ( @@ -902,6 +939,8 @@ def take_action(self, parsed_args): 'Visibility', 'Protected', 'Project', + 'Hash Algorithm', + 'Hash Value', 'Tags', ) else: @@ -912,18 +951,19 @@ def take_action(self, parsed_args): if 'limit' in kwargs: # Disable automatic pagination in SDK kwargs['paginated'] = False - data = list(image_client.images(**kwargs)) + + images = list(image_client.images(**kwargs)) if parsed_args.property: for attr, value in parsed_args.property.items(): api_utils.simple_filter( - data, + images, attr=attr, value=value, property_field='properties', ) - data = utils.sort_items(data, parsed_args.sort, str) + data = utils.sort_items(images, parsed_args.sort, str) return ( column_headers, @@ -941,7 +981,7 @@ def take_action(self, parsed_args): class ListImageProjects(command.Lister): _description = _("List projects associated with image") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "image", @@ -951,7 +991,9 @@ def get_parser(self, prog_name): identity_common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: image_client = self.app.client_manager.image columns: tuple[str, ...] = ("Image ID", "Member ID", "Status") @@ -977,7 +1019,7 @@ def take_action(self, parsed_args): class RemoveProjectImage(command.Command): _description = _("Disassociate project with image") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "image", @@ -992,15 +1034,15 @@ def get_parser(self, prog_name): identity_common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) image = image_client.find_image( parsed_args.image, @@ -1013,7 +1055,7 @@ def take_action(self, parsed_args): class ShowProjectImage(command.ShowOne): _description = _("Show a particular project associated with image") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "image", @@ -1028,7 +1070,9 @@ def get_parser(self, prog_name): identity_common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image image = image_client.find_image( @@ -1050,8 +1094,18 @@ def take_action(self, parsed_args): class SaveImage(command.Command): _description = _("Save an image locally") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) + parser.add_argument( + "--chunk-size", + type=int, + default=1024, + metavar="", + help=_( + "Size in bytes to read from the wire and buffer at one " + "time (default: 1024)" + ), + ) parser.add_argument( "--file", metavar="", @@ -1065,7 +1119,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image image = image_client.find_image( parsed_args.image, @@ -1076,7 +1130,12 @@ def take_action(self, parsed_args): if output_file is None: output_file = getattr(sys.stdout, "buffer", sys.stdout) - image_client.download_image(image.id, stream=True, output=output_file) + image_client.download_image( + image.id, + stream=True, + output=output_file, + chunk_size=parsed_args.chunk_size, + ) class SetImage(command.Command): @@ -1084,7 +1143,7 @@ class SetImage(command.Command): deadopts = ('visibility',) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) # TODO(bunting): There are additional arguments that v1 supported # --size - does not exist in v2 @@ -1260,9 +1319,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image + identity_client = self.app.client_manager.sdk_connection.identity for deadopt in self.deadopts: if getattr(parsed_args, f"dead_{deadopt.replace('-', '_')}", None): @@ -1280,11 +1339,11 @@ def take_action(self, parsed_args): ) project_id = None if parsed_args.project: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) # handle activation status changes @@ -1304,6 +1363,7 @@ def take_action(self, parsed_args): # our own membership if not project_id: project_id = self.app.client_manager.auth_ref.project_id + assert project_id is not None, 'this should not happen' image_client.update_member( image=image.id, member=project_id, @@ -1357,7 +1417,10 @@ def take_action(self, parsed_args): if parsed_args.visibility is not None: kwargs['visibility'] = parsed_args.visibility - if parsed_args.project: + # Only set owner_id if --project is used WITHOUT membership flags + # When --project is used with --accept/--reject/--pending, it should + # only identify which member's status to update, not change ownership + if parsed_args.project and not parsed_args.membership: # We already did the project lookup above kwargs['owner_id'] = project_id @@ -1382,7 +1445,7 @@ def take_action(self, parsed_args): class ShowImage(command.ShowOne): _description = _("Display image details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--human-readable", @@ -1397,7 +1460,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image image = image_client.find_image( @@ -1406,13 +1471,14 @@ def take_action(self, parsed_args): ) info = _format_image(image, parsed_args.human_readable) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class UnsetImage(command.Command): _description = _("Unset image tags and properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "image", @@ -1443,7 +1509,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image image = image_client.find_image( parsed_args.image, @@ -1462,7 +1528,7 @@ def take_action(self, parsed_args): ) tagret += 1 - kwargs: dict[str, ty.Any] = {} + kwargs: dict[str, Any] = {} if parsed_args.properties: for k in parsed_args.properties: if k in image: @@ -1473,10 +1539,15 @@ def take_action(self, parsed_args): # out, what was changed inside # NOTE: ping gtema to improve that in SDK new_props = kwargs.get( - 'properties', image.get('properties').copy() + 'properties', image['properties'].copy() ) new_props.pop(k, None) kwargs['properties'] = new_props + elif ( + k in IMAGE_ATTRIBUTES_CUSTOM_NAMES + and IMAGE_ATTRIBUTES_CUSTOM_NAMES[k] in image + ): + delattr(image, IMAGE_ATTRIBUTES_CUSTOM_NAMES[k]) else: LOG.error( _( @@ -1525,7 +1596,7 @@ class StageImage(command.Command): "(Glance 16.0.0 (Queens))" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -1555,7 +1626,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image image = image_client.find_image( @@ -1575,7 +1646,7 @@ def take_action(self, parsed_args): else: fp = get_data_from_stdin() - kwargs: dict[str, ty.Any] = {} + kwargs: dict[str, Any] = {} if parsed_args.progress and parsed_args.filename: # NOTE(stephenfin): we only show a progress bar if the user @@ -1601,7 +1672,7 @@ class ImportImage(command.ShowOne): "(Glance 16.0.0 (Queens))" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -1675,6 +1746,7 @@ def get_parser(self, prog_name): ) stores_group.add_argument( '--all-stores', + action='store_true', help=_( "Make image available to all stores " "(either '--store' or '--all-stores' required with the " @@ -1711,7 +1783,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image try: @@ -1744,6 +1818,12 @@ def take_action(self, parsed_args): "'--method=web-download'" ) raise exceptions.CommandError(msg) + _parsed = urllib.parse.urlparse(parsed_args.uri) + if not all({_parsed.scheme, _parsed.netloc}): + msg = _("'%(uri)s' is not a valid url") + raise exceptions.CommandError( + msg % {'uri': parsed_args.uri}, + ) else: if parsed_args.uri: msg = _( @@ -1835,7 +1915,8 @@ def take_action(self, parsed_args): ) info = _format_image(image) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class StoresInfo(command.Lister): @@ -1843,7 +1924,7 @@ class StoresInfo(command.Lister): "Get available backends (only valid with Multi-Backend support)" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--detail", @@ -1856,7 +1937,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: image_client = self.app.client_manager.image try: columns: tuple[str, ...] = ("id", "description", "is_default") diff --git a/openstackclient/image/v2/info.py b/openstackclient/image/v2/info.py index 469b15ac29..b31871dae8 100644 --- a/openstackclient/image/v2/info.py +++ b/openstackclient/image/v2/info.py @@ -11,16 +11,22 @@ # under the License. +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib.cli import format_columns -from osc_lib.command import command +from openstackclient import command from openstackclient.i18n import _ class ImportInfo(command.ShowOne): _description = _("Show available import methods") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image import_info = image_client.get_import_info() diff --git a/openstackclient/image/v2/metadef_namespaces.py b/openstackclient/image/v2/metadef_namespaces.py index f753286c44..0de82899a1 100644 --- a/openstackclient/image/v2/metadef_namespaces.py +++ b/openstackclient/image/v2/metadef_namespaces.py @@ -15,13 +15,17 @@ """Image V2 Action Implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +from openstack.image.v2 import metadef_namespace as _metadef_namespace from osc_lib.cli import format_columns -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ _formatters = { @@ -31,7 +35,9 @@ LOG = logging.getLogger(__name__) -def _format_namespace(namespace): +def _format_namespace( + namespace: _metadef_namespace.MetadefNamespace, +) -> dict[str, Any]: info = {} fields_to_show = [ @@ -41,23 +47,22 @@ def _format_namespace(namespace): 'namespace', 'owner', 'protected', + 'tags', 'schema', 'updated_at', 'visibility', ] - namespace = namespace.to_dict(ignore_none=True, original_names=True) + data = namespace.to_dict(ignore_none=True, original_names=True) # split out the usual key and the properties which are top-level - for key in namespace: + for key in data: if key in fields_to_show: - info[key] = namespace.get(key) + info[key] = data.get(key) elif key == "resource_type_associations": - info[key] = [ - resource_type['name'] for resource_type in namespace.get(key) - ] + info[key] = [resource_type['name'] for resource_type in data[key]] elif key == 'properties': - info['properties'] = list(namespace.get(key).keys()) + info['properties'] = list(data[key].keys()) return info @@ -65,7 +70,7 @@ def _format_namespace(namespace): class CreateMetadefNamespace(command.ShowOne): _description = _("Create a metadef namespace") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -114,7 +119,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image filter_keys = ['namespace', 'display_name', 'description'] kwargs = {} @@ -133,13 +140,14 @@ def take_action(self, parsed_args): data = image_client.create_metadef_namespace(**kwargs) info = _format_namespace(data) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteMetadefNamespace(command.Command): _description = _("Delete metadef namespace") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -149,7 +157,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image result = 0 @@ -179,7 +187,7 @@ def take_action(self, parsed_args): class ListMetadefNamespace(command.Lister): _description = _("List metadef namespaces") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--resource-types", @@ -193,7 +201,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: image_client = self.app.client_manager.image filter_keys = ['resource_types', 'visibility'] kwargs = {} @@ -221,7 +231,7 @@ def take_action(self, parsed_args): class SetMetadefNamespace(command.Command): _description = _("Set metadef namespace properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -270,9 +280,20 @@ def get_parser(self, prog_name): dest="is_protected", help=_("Allow metadef namespace to be deleted (default)"), ) + parser.add_argument( + "--tag", + metavar="", + action='append', + default=[], + dest='tags', + help=_( + "Set a tag on this metadef namespace " + "(repeat option to set multiple tags)" + ), + ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image namespace = parsed_args.namespace @@ -293,11 +314,26 @@ def take_action(self, parsed_args): image_client.update_metadef_namespace(namespace, **kwargs) + errors = 0 + for tag in parsed_args.tags: + try: + image_client.add_tag_to_metadef_namespace(namespace, tag) + except Exception: + LOG.error(_("Tag set failed for tag %s"), tag) + errors += 1 + + if errors > 0: + msg = _("Failed to set %(errors)s of %(total)s tags.") % { + 'errors': errors, + 'total': len(parsed_args.tags), + } + raise exceptions.CommandError(msg) + class ShowMetadefNamespace(command.ShowOne): _description = _("Show a metadef namespace") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -306,7 +342,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image namespace = parsed_args.namespace @@ -314,4 +352,61 @@ def take_action(self, parsed_args): data = image_client.get_metadef_namespace(namespace) info = _format_namespace(data) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data + + +class UnsetMetadefNamespace(command.Command): + _description = _("Unset metadef namespace tags") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "namespace", + metavar="", + help=_("Metadef namespace to modify (name)"), + ) + tag_group = parser.add_mutually_exclusive_group(required=True) + tag_group.add_argument( + "--tag", + metavar="", + action='append', + default=[], + dest='tags', + help=_( + "Unset a tag on this metadef namespace " + "(repeat option to unset multiple tags)" + ), + ) + tag_group.add_argument( + "--all-tags", + action="store_true", + default=False, + help=_("Unset all metadef tags"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + image_client = self.app.client_manager.image + + namespace = image_client.get_metadef_namespace(parsed_args.namespace) + + errors = 0 + if parsed_args.all_tags: + image_client.remove_tags_from_metadef_namespace(namespace) + elif parsed_args.tags: + for tag in parsed_args.tags: + try: + image_client.remove_tag_from_metadef_namespace( + namespace, tag + ) + except Exception: + LOG.error(_("tag unset failed for tag %s"), tag) + errors += 1 + + if errors > 0: + msg = _("Failed to unset %(errors)s of %(total)s tags.") % { + 'errors': errors, + 'total': len(parsed_args.tags), + } + raise exceptions.CommandError(msg) diff --git a/openstackclient/image/v2/metadef_objects.py b/openstackclient/image/v2/metadef_objects.py index 04c0fa55f1..2467e1d003 100644 --- a/openstackclient/image/v2/metadef_objects.py +++ b/openstackclient/image/v2/metadef_objects.py @@ -15,19 +15,25 @@ """Image V2 Action Implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.image.v2 import metadef_object as _metadef_object from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -def _format_object(md_object): +def _format_object( + md_object: _metadef_object.MetadefObject, +) -> tuple[tuple[str, ...], Iterable[Any]]: fields_to_show = ( 'created_at', 'description', @@ -40,17 +46,14 @@ def _format_object(md_object): return ( fields_to_show, - utils.get_item_properties( - md_object, - fields_to_show, - ), + utils.get_item_properties(md_object, fields_to_show), ) class CreateMetadefObjects(command.ShowOne): _description = _("Create a metadef object") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--namespace", @@ -64,7 +67,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image namespace = image_client.get_metadef_namespace( @@ -83,7 +88,7 @@ def take_action(self, parsed_args): class ShowMetadefObjects(command.ShowOne): _description = _("Show a particular metadef object") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -97,7 +102,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image namespace = parsed_args.namespace @@ -113,7 +120,7 @@ def take_action(self, parsed_args): class DeleteMetadefObject(command.Command): _description = _("Delete metadata definitions object(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -123,16 +130,23 @@ def get_parser(self, prog_name): parser.add_argument( "objects", metavar="", - nargs="+", - help=_("Metadef object(s) to delete (name)"), + nargs="*", + help=_( + "Metadef object(s) to delete (name) " + "(omit this argument to delete all objects in the namespace)" + ), ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image namespace = parsed_args.namespace + if not parsed_args.objects: + image_client.delete_all_metadef_objects(namespace) + return + result = 0 for obj in parsed_args.objects: try: @@ -160,7 +174,7 @@ def take_action(self, parsed_args): class ListMetadefObjects(command.Lister): _description = _("List metadef objects inside a specific namespace.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -169,7 +183,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: image_client = self.app.client_manager.image namespace = parsed_args.namespace @@ -192,7 +208,7 @@ def take_action(self, parsed_args): class SetMetadefObject(command.Command): _description = _("Update a metadef object") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -210,7 +226,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image object = image_client.get_metadef_object( @@ -230,7 +246,7 @@ class ShowMetadefObjectProperty(command.ShowOne): "Describe a specific metadata definitions property inside an object." ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -248,7 +264,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image namespace_name = parsed_args.namespace @@ -268,4 +286,5 @@ def take_action(self, parsed_args): } raise exceptions.CommandError(msg) - return zip(*sorted(prop.items())) + col_headers, col_data = zip(*sorted(prop.items())) + return col_headers, col_data diff --git a/openstackclient/image/v2/metadef_properties.py b/openstackclient/image/v2/metadef_properties.py index 40440c0299..1f1667a026 100644 --- a/openstackclient/image/v2/metadef_properties.py +++ b/openstackclient/image/v2/metadef_properties.py @@ -12,23 +12,29 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse +from collections.abc import Iterable, Sequence import json import logging +from typing import Any -from osc_lib.command import command +from openstack.image.v2 import metadef_property as _metadef_property from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -def _format_property(prop): - prop = prop.to_dict(ignore_none=True, original_names=True) +def _format_property( + prop: _metadef_property.MetadefProperty, +) -> dict[str, Any]: + data = prop.to_dict(ignore_none=True, original_names=True) return { - key: prop[key] + key: data[key] for key in [ 'namespace_name', 'name', @@ -50,14 +56,14 @@ def _format_property(prop): 'max_items', 'allow_additional_items', ] - if key in prop + if key in data } class CreateMetadefProperty(command.ShowOne): _description = _("Create a metadef property") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--name", @@ -85,7 +91,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image kwargs = { @@ -108,13 +116,14 @@ def take_action(self, parsed_args): ) info = _format_property(data) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteMetadefProperty(command.Command): _description = _("Delete metadef propert(ies)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -124,14 +133,22 @@ def get_parser(self, prog_name): parser.add_argument( "properties", metavar="", - nargs="+", - help=_("Metadef propert(ies) to delete (name)"), + nargs="*", + help=_( + "Metadef properties to delete (name) " + "(omit this argument to delete all properties " + "in the namespace)" + ), ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image + if not parsed_args.properties: + image_client.delete_all_metadef_properties(parsed_args.namespace) + return + result = 0 for prop in parsed_args.properties: try: @@ -166,7 +183,7 @@ def take_action(self, parsed_args): class ListMetadefProperties(command.Lister): _description = _("List metadef properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -175,7 +192,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: image_client = self.app.client_manager.image props = image_client.metadef_properties(parsed_args.namespace) columns = ['name', 'title', 'type'] @@ -194,7 +213,7 @@ def take_action(self, parsed_args): class SetMetadefProperty(command.Command): _description = _("Update metadef namespace property") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--name", @@ -222,7 +241,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image # We need to pass the values for *all* attributes as kwargs to @@ -259,7 +278,7 @@ def take_action(self, parsed_args): class ShowMetadefProperty(command.ShowOne): _description = _("Show a particular metadef property") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -273,7 +292,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image data = image_client.get_metadef_property( parsed_args.property, @@ -281,4 +302,5 @@ def take_action(self, parsed_args): ) info = _format_property(data) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data diff --git a/openstackclient/image/v2/metadef_resource_type_association.py b/openstackclient/image/v2/metadef_resource_type_association.py index 306783cdda..da3f33762f 100644 --- a/openstackclient/image/v2/metadef_resource_type_association.py +++ b/openstackclient/image/v2/metadef_resource_type_association.py @@ -10,18 +10,21 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns(item: Any) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns @@ -31,7 +34,7 @@ def _get_columns(item): class CreateMetadefResourceTypeAssociation(command.ShowOne): _description = _("Create metadef resource type association") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "namespace", @@ -56,7 +59,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image kwargs = {} @@ -79,7 +84,7 @@ def take_action(self, parsed_args): class DeleteMetadefResourceTypeAssociation(command.Command): _description = _("Delete metadef resource type association") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "metadef_namespace", @@ -107,7 +112,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: image_client = self.app.client_manager.image result = 0 @@ -160,7 +165,7 @@ def take_action(self, parsed_args): class ListMetadefResourceTypeAssociations(command.Lister): _description = _("List metadef resource type associations") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "metadef_namespace", @@ -169,7 +174,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image data = image_client.metadef_resource_type_associations( parsed_args.metadef_namespace, diff --git a/openstackclient/image/v2/metadef_resource_types.py b/openstackclient/image/v2/metadef_resource_types.py index 3477df35e2..4df71c950b 100644 --- a/openstackclient/image/v2/metadef_resource_types.py +++ b/openstackclient/image/v2/metadef_resource_types.py @@ -12,16 +12,22 @@ """Image V2 Action Implementations""" -from osc_lib.command import command +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ class ListMetadefResourceTypes(command.Lister): _description = _("List metadef resource types") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: image_client = self.app.client_manager.image data = image_client.metadef_resource_types() columns = ['Name'] diff --git a/openstackclient/image/v2/task.py b/openstackclient/image/v2/task.py index 1b7170f63b..88b06de37d 100644 --- a/openstackclient/image/v2/task.py +++ b/openstackclient/image/v2/task.py @@ -10,18 +10,20 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack.image.v2 import task as _task from osc_lib.cli import format_columns -from osc_lib.command import command from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ -_formatters = { - 'tags': format_columns.ListColumn, -} - -def _format_task(task): +def _format_task(task: _task.Task) -> dict[str, Any]: """Format an task to make it more consistent with OSC operations.""" info = {} @@ -63,7 +65,7 @@ def _format_task(task): class ShowTask(command.ShowOne): _description = _('Display task details') - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -74,19 +76,22 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: image_client = self.app.client_manager.image task = image_client.get_task(parsed_args.task) info = _format_task(task) - return zip(*sorted(info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class ListTask(command.Lister): _description = _('List tasks') - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -110,22 +115,7 @@ def get_parser(self, prog_name): 'by comma)' ), ) - parser.add_argument( - '--limit', - metavar='', - type=int, - help=_('Maximum number of tasks to display.'), - ) - parser.add_argument( - '--marker', - metavar='', - help=_( - 'The last task of the previous page. ' - 'Display list of tasks after marker. ' - 'Display all tasks if not specified. ' - '(name or ID)' - ), - ) + pagination.add_marker_pagination_option_to_parser(parser) parser.add_argument( '--type', metavar='', @@ -146,7 +136,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: image_client = self.app.client_manager.image columns = ('id', 'type', 'status', 'owner_id') @@ -173,7 +165,13 @@ def take_action(self, parsed_args): return ( column_headers, ( - utils.get_item_properties(s, columns, formatters=_formatters) + utils.get_item_properties( + s, + columns, + formatters={ + 'tags': format_columns.ListColumn, + }, + ) for s in data ), ) diff --git a/openstackclient/locale/tr_TR/LC_MESSAGES/openstackclient.po b/openstackclient/locale/tr_TR/LC_MESSAGES/openstackclient.po index f49640ecc2..480a0e9f14 100644 --- a/openstackclient/locale/tr_TR/LC_MESSAGES/openstackclient.po +++ b/openstackclient/locale/tr_TR/LC_MESSAGES/openstackclient.po @@ -3,7 +3,7 @@ msgid "" msgstr "" "Project-Id-Version: python-openstackclient VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2025-04-01 18:07+0000\n" +"POT-Creation-Date: 2026-04-15 02:22+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" @@ -1005,9 +1005,6 @@ msgstr "Politika(ları/yı) sil" msgid "Delete port(s)" msgstr "Bağlantı noktalarını sil" -msgid "Delete project(s)" -msgstr "Proje(yi/leri) sil" - msgid "Delete public or private key(s)" msgstr "Genel veya özel anahtarları sil" @@ -2019,10 +2016,6 @@ msgstr "Bu disk bölümü türü için şifreleme bilgisini ayarlama başarısı msgid "Failed to set flavor access to project: %s" msgstr "Projeye flavor erişiminin ayarlanması başarısız: %s" -#, python-format -msgid "Failed to set image property: %s" -msgstr "İmaj özelliği ayarlarken hata oluştu: %s" - #, python-format msgid "Failed to set snapshot property: %s" msgstr "Anlık görüntü özelliğini ayarlama başarısız: %s" @@ -2035,10 +2028,6 @@ msgstr "Anlık görüntü durumunu ayarlama başarısız: %s" msgid "Failed to set volume bootable property: %s" msgstr "Disk bölümü ön yükleme özelliği ayarlanırken hata oluştu: %s" -#, python-format -msgid "Failed to set volume property: %s" -msgstr "Disk özelliklerini ayarlama işlemi başarısız: %s" - #, python-format msgid "Failed to set volume read-only access mode flag: %s" msgstr "Disk bölümü salt okunur erişim kipi bayrağı ayarlanamadı: %s" @@ -2083,14 +2072,6 @@ msgstr "" msgid "Failed to unset flavor property: %s" msgstr "Flavor özelliğini kaldırma işlemi başarısız: %s" -#, python-format -msgid "Failed to unset image property: %s" -msgstr "İmaj özelliği ayarını kaldırma başarısız: %s" - -#, python-format -msgid "Failed to unset volume property: %s" -msgstr "Disk bölümü özelliği ayarlarını kaldırma başarısız: %s" - #, python-format msgid "Failed to update snapshot name or description: %s" msgstr "Anlık görüntü adı veya açıklaması güncellenemedi: %s" @@ -2548,14 +2529,6 @@ msgstr "EC2 kimlik bilgilerini listele" msgid "List IP availability for network" msgstr "Ağ için IP kullanılabilirliğini listele" -msgid "List IP availability of given IP version networks (default is 4)" -msgstr "" -"Verilen IP sürüm ağlarının IP kullanılabilirliğini listele (varsayılan sürüm " -"4)" - -msgid "List IP availability of given project (name or ID)" -msgstr "Verilen projenin IP kullanılabilirliğini listeleyin (isim veya ID)" - msgid "List Network QoS rules" msgstr "Ağ QoS kurallarını listele" @@ -2583,24 +2556,6 @@ msgstr "Çıktıdaki ek alanları listele" msgid "List address scopes" msgstr "Adres kapsamlarını listele" -msgid "List address scopes according to their project (name or ID)" -msgstr "Adres kapsamlarını projelerine göre listele (ad veya kimlik)" - -msgid "List address scopes not shared between projects" -msgstr "Projeler arasında paylaşılmayan adres kapsamlarını listele" - -msgid "List address scopes of given IP version networks (4 or 6)" -msgstr "Verilen IP sürüm ağlarının adres kapsamlarını listele (4 veya 6)" - -msgid "List address scopes shared between projects" -msgstr "Projeler arasında paylaşılan adres kapsamlarını listele" - -msgid "List agents hosting a network (name or ID)" -msgstr "Bir ağa ev sahipliği yapan ajanları listele (isim veya ID)" - -msgid "List agents hosting this router (name or ID)" -msgstr "Bu yönlendiriciyi barındıran ajanları listele (isim veya ID)" - msgid "List all aggregates" msgstr "Tüm kümeleri listele" @@ -2646,18 +2601,12 @@ msgstr "Kapları listele" msgid "List credentials" msgstr "Kimlik bilgilerini listele" -msgid "List disabled networks" -msgstr "Devre dışı bırakılan ağları listele" - msgid "List disabled routers" msgstr "Devre dışı bırakılmış yönlendiricileri listele" msgid "List domains" msgstr "Alanları listele" -msgid "List enabled networks" -msgstr "Aktif ağları listele" - msgid "List enabled routers" msgstr "Etkinleştirilmiş yönlendiricileri listele" @@ -2676,9 +2625,6 @@ msgstr "Kimlik API için uzantıları listele" msgid "List extensions for the Network API" msgstr "Ağ API için uzantıları listele" -msgid "List external networks" -msgstr "Harici ağları listele" - msgid "List federation protocols" msgstr "Federasyon protokollerinin listele" @@ -2688,24 +2634,6 @@ msgstr "Flavorları listele" msgid "List floating IP(s)" msgstr "Yüzen IP(ler) listesi" -msgid "List floating IP(s) according to given fixed IP address" -msgstr "Verilen sabit IP adresine göre yüzen IP(ler) listesi" - -msgid "List floating IP(s) according to given network (name or ID)" -msgstr "Verilen ağdaki yüzen IP(ler) listesi (isim veya ID)" - -msgid "List floating IP(s) according to given port (name or ID)" -msgstr "Verilen bağlantı noktasına göre yüzen IP(ler) listesi (isim veya ID)" - -msgid "List floating IP(s) according to given project (name or ID)" -msgstr "Verilen projeye göre yüzen IP(ler)i listele (isim veya ID)" - -msgid "List floating IP(s) according to given router (name or ID)" -msgstr "Verilen yönlendiriciye göre yüzen IP(ler)i listele (isim veya ID)" - -msgid "List floating IP(s) according to given status ('ACTIVE', 'DOWN')" -msgstr "Verilen duruma göre yüzen IP(ler) listesi ('AKTİF', 'KAPALI')" - msgid "List groups" msgstr "Grupları listele" @@ -2715,9 +2643,6 @@ msgstr "Yönetici arakatman listesi" msgid "List identity providers" msgstr "Kimlik sağlayıcılarını listele" -msgid "List internal networks" -msgstr "Dahili ağları listele" - msgid "List key fingerprints" msgstr "Anahtar parmak izlerini listele" @@ -2733,13 +2658,6 @@ msgstr "Modül sürümlerini listele" msgid "List network RBAC policies" msgstr "Ağ RBAC politikalarının listesi" -msgid "" -"List network RBAC policies according to given action (\"access_as_external\" " -"or \"access_as_shared\")" -msgstr "" -"Verilen eyleme göre ağ RBAC ilkelerini listele (\"access_as_external\" veya " -"\"access_as_shared\")" - msgid "List network agents" msgstr "Ağ ajanlarını listele" @@ -2761,48 +2679,12 @@ msgstr "Ağ kotasını listele" msgid "List network segments" msgstr "Ağ bölümlerini listele" -msgid "List network segments that belong to this network (name or ID)" -msgstr "Bu ağa ait ağ kesimlerini listeleyin (ad veya kimlik)" - msgid "List networks" msgstr "Ağları listele" -msgid "" -"List networks according to VLAN ID for VLAN networks or Tunnel ID for GENEVE/" -"GRE/VXLAN networks" -msgstr "" -"VLAN ağları için VLAN ID'ye veya GENEVA/GRE/VXLAN ağları için Tünel ID'ye " -"göre ağları listele" - -msgid "List networks according to name of the physical network" -msgstr "Ağları fiziksel ağın adına göre listeleme" - -msgid "List networks according to their name" -msgstr "Ağları isimlerine göre listele" - -msgid "List networks according to their project (name or ID)" -msgstr "Projelerine göre ağları listele (isim veya ID)" - -msgid "" -"List networks according to their status ('ACTIVE', 'BUILD', 'DOWN', 'ERROR')" -msgstr "" -"Ağları durumlarına göre listeleyin ('AKTİF', 'KURMA', 'KAPALI', 'HATA')" - -msgid "List networks hosted by agent (ID only)" -msgstr "Aracı tarafından barındırılan ağları listele (yalnızca ID)" - -msgid "List networks not shared between projects" -msgstr "Projeler arasında paylaşılmayan ağları listele" - -msgid "List networks shared between projects" -msgstr "Projeler arasında paylaşılan ağları listele" - msgid "List objects" msgstr "Nesneleri listele" -msgid "List only address scopes of given name in output" -msgstr "Çıktıda verilen adın sadece adres kapsamlarını listeleyin" - msgid "List only agents running on the specified host" msgstr "Yalnızca belirtilen ana bilgisayarda çalışan ajanları listele" @@ -2825,9 +2707,6 @@ msgstr "" "Yalnızca belirtilen aygıt sahibi olan bağlantı noktalarını listeleyin. Bu, " "bağlantı noktasını kullanan varlıktır (örneğin, ağ: dhcp)." -msgid "List only ports with this MAC address" -msgstr "Sadece bu MAC adresine sahip bağlantı noktalarını listele" - msgid "List only private flavors" msgstr "Sadece gizli flavorları listele" @@ -2849,29 +2728,6 @@ msgstr "Sadece genele açık türleri listele" msgid "List only specified service (name only)" msgstr "Sadece belirtilen servisleri listele (sadece isim)" -msgid "List only subnet pools of given address scope in output (name or ID)" -msgstr "" -"Verilen adres kapsamındaki sadece altağ havuzlarını çıktıda listele (isim " -"veya ID)" - -msgid "List only subnet pools of given name in output" -msgstr "Verilen isimdeki sadece altağ havuzlarını çıktıda listele" - -msgid "List only subnets of given gateway IP in output" -msgstr "Verilen geçit IP'sinin sadece alt ağlarını çıktıda listele" - -msgid "List only subnets of given name in output" -msgstr "Verilen isimdeki sadece altağları çıktıda listele" - -msgid "" -"List only subnets which belong to a given network in output (name or ID)" -msgstr "Verilen bir ağa ait sadece altağları çıktıda listele (isim veya ID)" - -msgid "" -"List only subnets which belong to a given project in output (name or ID)" -msgstr "" -"Verilen bir projeye ait sadece altağları çıktıda listele (isim veya ID)" - msgid "List policies" msgstr "Politikaları listele" @@ -2881,9 +2737,6 @@ msgstr "Yüzer IP adresleri havuzlarını listele" msgid "List ports" msgstr "Bağlantı noktalarını listele" -msgid "List ports according to their project (name or ID)" -msgstr "Projeye bağlı olarak bağlantı noktalarını listele (isim veya ID)" - msgid "List projects" msgstr "Projeleri listele" @@ -2913,27 +2766,12 @@ msgstr "Yönlendiricilerin listesi" msgid "List routers according to their name" msgstr "Yönlendiricileri isimlerine göre listele" -msgid "List routers according to their project (name or ID)" -msgstr "Yönlendiricileri projelerine göre listele (isim veya ID)" - -msgid "List routers hosted by an agent (ID only)" -msgstr "Bir ajan tarafından sunulan yönlendiricileri listele (sadece ID)" - -msgid "List rules applied to incoming network traffic" -msgstr "Gelen ağ trafiğine uygulanan kurallar listesi" - -msgid "List rules applied to outgoing network traffic" -msgstr "Giden ağ trafiğine uygulanan kuralları listele" - msgid "List security group rules" msgstr "Güvenlik grubu kurallarını listele" msgid "List security groups" msgstr "Güvenlik gruplarını listele" -msgid "List security groups according to the project (name or ID)" -msgstr "Projeye göre güvenlik grubunu listele (isim veya ID)" - msgid "List servers" msgstr "Sunucuları listele" @@ -2955,32 +2793,9 @@ msgstr "Belirtilen sunucu üzerindeki servisleri listele (sadece isim)" msgid "List subnet pools" msgstr "Altağ havuzlarını listele" -msgid "List subnet pools according to their project (name or ID)" -msgstr "Alt ağ havuzlarını projelerine göre listele (isim veya ID)" - -msgid "List subnet pools not shared between projects" -msgstr "Projeler arasında paylaşılmayan altağ havularını listele" - -msgid "List subnet pools not used as the default external subnet pool" -msgstr "" -"Varsayılan harici altağ havuzu olarak kullanılmayan altağ havuzlarını listele" - -msgid "List subnet pools shared between projects" -msgstr "Projeler arasında paylaşılan altağ havuzlarını listele" - -msgid "List subnet pools used as the default external subnet pool" -msgstr "" -"Varsayılan harici altağ havuzu olarak kullanılan altağ havuzlarını listele" - msgid "List subnets" msgstr "Alt ağları listele" -msgid "List subnets which have DHCP disabled" -msgstr "DHCP'nin devre dışı olduğu altağları listele" - -msgid "List subnets which have DHCP enabled" -msgstr "DHCP'nin etkin olduğu altağları listele" - msgid "List the default volume type" msgstr "Varsayılan disk bölümü türünü listele" @@ -4643,8 +4458,8 @@ msgstr "Ağı varsayılan dış ağ olarak ayarlayın" msgid "" "Set the notional service where the encryption is performed (\"front-end\" or " -"\"back-end\") (admin only) (The default value for this option is \"front-end" -"\" when setting encryption type of a volume for the first time. Consider " +"\"back-end\") (admin only) (The default value for this option is \"front-" +"end\" when setting encryption type of a volume for the first time. Consider " "using other encryption options such as: \"--encryption-cipher\", \"--" "encryption-key-size\" and \"--encryption-provider\")" msgstr "" @@ -4656,10 +4471,10 @@ msgstr "" msgid "" "Set the notional service where the encryption is performed (\"front-end\" or " -"\"back-end\") (admin only) (The default value for this option is \"front-end" -"\" when setting encryption type of a volume. Consider using other encryption " -"options such as: \"--encryption-cipher\", \"--encryption-key-size\" and \"--" -"encryption-provider\")" +"\"back-end\") (admin only) (The default value for this option is \"front-" +"end\" when setting encryption type of a volume. Consider using other " +"encryption options such as: \"--encryption-cipher\", \"--encryption-key-" +"size\" and \"--encryption-provider\")" msgstr "" "Şifrelemenin gerçekleştirildiği kavramsal servisi ayarla (\"front-end\" veya " "\"back-end\") (sadece yönetici) (Bir disk bölümünün şifreleme türü " diff --git a/openstackclient/network/client.py b/openstackclient/network/client.py index 5165e1ecdc..4f57faf8a9 100644 --- a/openstackclient/network/client.py +++ b/openstackclient/network/client.py @@ -10,7 +10,9 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse import logging +from typing import Any from osc_lib import utils @@ -23,9 +25,16 @@ API_VERSION_OPTION = 'os_network_api_version' API_NAME = 'network' API_VERSIONS = ('2.0', '2') +API_EXTENSIONS = ( + 'bgpvpn', + 'fwaas', + 'taas', + 'dynamic_routing', + 'vpnaas', +) -def make_client(instance): +def make_client(instance: Any) -> Any: """Returns a network proxy""" LOG.debug( 'Network client initialized using OpenStack SDK: %s', @@ -34,7 +43,9 @@ def make_client(instance): return instance.sdk_connection.network -def build_option_parser(parser): +def build_option_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: """Hook to add global options""" parser.add_argument( '--os-network-api-version', diff --git a/openstackclient/network/common.py b/openstackclient/network/common.py index 9bcd16583b..01d5f0bf62 100644 --- a/openstackclient/network/common.py +++ b/openstackclient/network/common.py @@ -11,18 +11,17 @@ # under the License. # -import abc import argparse +from collections.abc import Generator import contextlib import logging -import typing as ty +from typing import Any -import cliff.app import openstack.exceptions from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions +from openstackclient import command from openstackclient.i18n import _ from openstackclient.network import utils @@ -38,13 +37,11 @@ 'security_groups': 'security-groups', } -_NET_TYPE_NEUTRON = 'neutron' -_NET_TYPE_COMPUTE = 'nova-network' -_QUALIFIER_FMT = "%s\n\n*%s*" - @contextlib.contextmanager -def check_missing_extension_if_error(client_manager, attrs): +def check_missing_extension_if_error( + client_manager: Any, attrs: dict[str, Any] +) -> Generator[None, None, None]: # If specified option requires extension, then try to # find out if it exists. If it does not exist, # then an exception with the appropriate message @@ -58,231 +55,6 @@ def check_missing_extension_if_error(client_manager, attrs): raise -class NetDetectionMixin(metaclass=abc.ABCMeta): - """Convenience methods for nova-network vs. neutron decisions. - - A live environment detects which network type it is running and creates its - parser with only the options relevant to that network type. - - But the command classes are used for docs builds as well, and docs must - present the options for both network types, often qualified accordingly. - """ - - app: cliff.app.App - - @property - def _network_type(self): - """Discover whether the running cloud is using neutron or nova-network. - - :return: - * ``NET_TYPE_NEUTRON`` if neutron is detected - * ``NET_TYPE_COMPUTE`` if running in a cloud but neutron is not - detected. - * ``None`` if not running in a cloud, which hopefully means we're - building docs. - """ - # Have we set it up yet for this command? - if not hasattr(self, '_net_type'): - try: - if self.app.client_manager.is_network_endpoint_enabled(): - net_type = _NET_TYPE_NEUTRON - else: - net_type = _NET_TYPE_COMPUTE - except AttributeError: - LOG.warning( - "%s: Could not detect a network type. Assuming we are " - "building docs.", - self.__class__.__name__, - ) - net_type = None - self._net_type = net_type - return self._net_type - - @property - def is_neutron(self): - return self._network_type is _NET_TYPE_NEUTRON - - @property - def is_nova_network(self): - return self._network_type is _NET_TYPE_COMPUTE - - @property - def is_docs_build(self): - return self._network_type is None - - def enhance_help_neutron(self, _help): - if self.is_docs_build: - # Why can't we say 'neutron'? - return _QUALIFIER_FMT % (_help, _("Network version 2 only")) - return _help - - def enhance_help_nova_network(self, _help): - if self.is_docs_build: - # Why can't we say 'nova-network'? - return _QUALIFIER_FMT % (_help, _("Compute version 2 only")) - return _help - - @staticmethod - def split_help(network_help, compute_help): - return ( - "*{network_qualifier}:*\n {network_help}\n\n" - "*{compute_qualifier}:*\n {compute_help}".format( - **dict( - network_qualifier=_("Network version 2"), - network_help=network_help, - compute_qualifier=_("Compute version 2"), - compute_help=compute_help, - ) - ) - ) - - def get_parser(self, prog_name: str) -> argparse.ArgumentParser: - LOG.debug('get_parser(%s)', prog_name) - parser = super().get_parser(prog_name) # type: ignore - parser = self.update_parser_common(parser) - LOG.debug('common parser: %s', parser) - if self.is_neutron or self.is_docs_build: - parser = self.update_parser_network(parser) - if self.is_nova_network or self.is_docs_build: - # Add nova-net options if running nova-network or building docs - parser = self.update_parser_compute(parser) - return parser - - def update_parser_common(self, parser): - """Default is no updates to parser.""" - return parser - - def update_parser_network(self, parser): - """Default is no updates to parser.""" - return parser - - def update_parser_compute(self, parser): - """Default is no updates to parser.""" - return parser - - def take_action(self, parsed_args): - if self.is_neutron: - return self.take_action_network( - self.app.client_manager.network, parsed_args - ) - elif self.is_nova_network: - return self.take_action_compute( - self.app.client_manager.compute, parsed_args - ) - - def take_action_network(self, client, parsed_args): - """Override to do something useful.""" - pass - - def take_action_compute(self, client, parsed_args): - """Override to do something useful.""" - pass - - -class NetworkAndComputeCommand( - NetDetectionMixin, command.Command, metaclass=abc.ABCMeta -): - """Network and Compute Command - - Command class for commands that support implementation via - the network or compute endpoint. Such commands have different - implementations for take_action() and may even have different - arguments. - """ - - pass - - -class NetworkAndComputeDelete(NetworkAndComputeCommand, metaclass=abc.ABCMeta): - """Network and Compute Delete - - Delete class for commands that support implementation via - the network or compute endpoint. Such commands have different - implementations for take_action() and may even have different - arguments. This class supports bulk deletion, and error handling - following the rules in doc/source/command-errors.rst. - """ - - def take_action(self, parsed_args): - ret = 0 - resources = getattr(parsed_args, self.resource, []) - - for r in resources: - self.r = r - try: - if self.app.client_manager.is_network_endpoint_enabled(): - self.take_action_network( - self.app.client_manager.network, parsed_args - ) - else: - self.take_action_compute( - self.app.client_manager.compute, - parsed_args, - ) - except Exception as e: - msg = _( - "Failed to delete %(resource)s with name or ID " - "'%(name_or_id)s': %(e)s" - ) % { - "resource": self.resource, - "name_or_id": r, - "e": e, - } - LOG.error(msg) - ret += 1 - - if ret: - total = len(resources) - msg = _("%(num)s of %(total)s %(resource)ss failed to delete.") % { - "num": ret, - "total": total, - "resource": self.resource, - } - raise exceptions.CommandError(msg) - - -class NetworkAndComputeLister( - NetDetectionMixin, command.Lister, metaclass=abc.ABCMeta -): - """Network and Compute Lister - - Lister class for commands that support implementation via - the network or compute endpoint. Such commands have different - implementations for take_action() and may even have different - arguments. - """ - - pass - - -class NetworkAndComputeShowOne( - NetDetectionMixin, command.ShowOne, metaclass=abc.ABCMeta -): - """Network and Compute ShowOne - - ShowOne class for commands that support implementation via - the network or compute endpoint. Such commands have different - implementations for take_action() and may even have different - arguments. - """ - - def take_action(self, parsed_args): - try: - if self.app.client_manager.is_network_endpoint_enabled(): - return self.take_action_network( - self.app.client_manager.network, parsed_args - ) - else: - return self.take_action_compute( - self.app.client_manager.compute, parsed_args - ) - except openstack.exceptions.HttpException as exc: - msg = _("Error while executing command: %s") % exc.message - if exc.details: - msg += ", " + str(exc.details) - raise exceptions.CommandError(msg) - - class NeutronCommandWithExtraArgs(command.Command): """Create and Update commands with additional extra properties. @@ -299,7 +71,7 @@ class NeutronCommandWithExtraArgs(command.Command): 'str': str, } - def _get_property_converter(self, _property): + def _get_property_converter(self, _property: dict[str, Any]) -> Any: if 'type' in _property: converter = self._allowed_types_dict.get(_property['type']) else: @@ -315,15 +87,17 @@ def _get_property_converter(self, _property): ) return converter - def _parse_extra_properties(self, extra_properties): - result: dict[str, ty.Any] = {} + def _parse_extra_properties( + self, extra_properties: list[dict[str, Any]] | None + ) -> dict[str, Any]: + result: dict[str, Any] = {} if extra_properties: for _property in extra_properties: converter = self._get_property_converter(_property) result[_property['name']] = converter(_property['value']) return result - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--extra-property', @@ -348,8 +122,10 @@ def get_parser(self, prog_name): class NeutronUnsetCommandWithExtraArgs(NeutronCommandWithExtraArgs): - def _parse_extra_properties(self, extra_properties): - result: dict[str, ty.Any] = {} + def _parse_extra_properties( + self, extra_properties: list[dict[str, Any]] | None + ) -> dict[str, Any]: + result: dict[str, Any] = {} if extra_properties: for _property in extra_properties: result[_property['name']] = None diff --git a/openstackclient/network/utils.py b/openstackclient/network/utils.py index 30c0da0645..76a0a1d825 100644 --- a/openstackclient/network/utils.py +++ b/openstackclient/network/utils.py @@ -9,50 +9,22 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# + +from typing import Any, cast from osc_lib import exceptions from openstackclient.i18n import _ -# Transform compute security group rule for display. -def transform_compute_security_group_rule(sg_rule): - info = {} - info.update(sg_rule) - from_port = info.pop('from_port') - to_port = info.pop('to_port') - if isinstance(from_port, int) and isinstance(to_port, int): - port_range = {'port_range': f"{from_port}:{to_port}"} - elif from_port is None and to_port is None: - port_range = {'port_range': ""} - else: - port_range = {'port_range': f"{from_port}:{to_port}"} - info.update(port_range) - if 'cidr' in info['ip_range']: - info['ip_range'] = info['ip_range']['cidr'] - else: - info['ip_range'] = '' - if info['ip_protocol'] is None: - info['ip_protocol'] = '' - elif info['ip_protocol'].lower() == 'icmp': - info['port_range'] = '' - group = info.pop('group') - if 'name' in group: - info['remote_security_group'] = group['name'] - else: - info['remote_security_group'] = '' - return info - - -def str2bool(strbool): +def str2bool(strbool: str | None) -> bool | None: if strbool is None: return None return strbool.lower() == 'true' -def str2list(strlist): - result = [] +def str2list(strlist: str | None) -> list[str]: + result: list[str] = [] if strlist: result = strlist.split(';') return result @@ -78,17 +50,12 @@ def str2dict(strdict: str) -> dict[str, str]: else: kvlist[i - 1] = f"{kvlist[i - 1]};{kv}" for kv in kvlist: - key, sep, value = kv.partition(':') + key, value = kv.split(':', 1) result[key] = value return result -def format_security_group_rule_show(obj): - data = transform_compute_security_group_rule(obj) - return zip(*sorted(data.items())) - - -def format_network_port_range(rule): +def format_network_port_range(rule: dict[str, Any]) -> str: # Display port range or ICMP type and code. For example: # - ICMP type: 'type=3' # - ICMP type and code: 'type=3:code=0' @@ -114,8 +81,8 @@ def format_network_port_range(rule): return port_range -def format_remote_ip_prefix(rule): - remote_ip_prefix = rule['remote_ip_prefix'] +def format_remote_ip_prefix(rule: dict[str, Any]) -> str | None: + remote_ip_prefix = cast(str | None, rule['remote_ip_prefix']) if remote_ip_prefix is None: ethertype = rule['ether_type'] if ethertype == 'IPv4': @@ -125,7 +92,7 @@ def format_remote_ip_prefix(rule): return remote_ip_prefix -def convert_ipvx_case(string): +def convert_ipvx_case(string: str) -> str: if string.lower() == 'ipv4': return 'IPv4' if string.lower() == 'ipv6': @@ -133,7 +100,7 @@ def convert_ipvx_case(string): return string -def is_icmp_protocol(protocol): +def is_icmp_protocol(protocol: str | None) -> bool: # NOTE(rtheis): Neutron has deprecated protocol icmpv6. # However, while the OSC CLI doesn't document the protocol, # the code must still handle it. In addition, handle both @@ -144,12 +111,14 @@ def is_icmp_protocol(protocol): return False -def convert_to_lowercase(string): +def convert_to_lowercase(string: str) -> str: return string.lower() -def get_protocol(parsed_args, default_protocol='any'): - protocol = default_protocol +def get_protocol( + parsed_args: Any, default_protocol: str = 'any' +) -> str | None: + protocol: str | None = default_protocol if parsed_args.protocol is not None: protocol = parsed_args.protocol if hasattr(parsed_args, "proto") and parsed_args.proto is not None: @@ -159,7 +128,7 @@ def get_protocol(parsed_args, default_protocol='any'): return protocol -def get_ethertype(parsed_args, protocol): +def get_ethertype(parsed_args: Any, protocol: str | None) -> str: ethertype = 'IPv4' if parsed_args.ethertype is not None: ethertype = parsed_args.ethertype @@ -168,16 +137,20 @@ def get_ethertype(parsed_args, protocol): return ethertype -def is_ipv6_protocol(protocol): +def is_ipv6_protocol(protocol: str | None) -> bool: # NOTE(rtheis): Neutron has deprecated protocol icmpv6. # However, while the OSC CLI doesn't document the protocol, # the code must still handle it. In addition, handle both # protocol names and numbers. - if ( - protocol is not None - and protocol.startswith('ipv6-') - or protocol in ['icmpv6', '41', '43', '44', '58', '59', '60'] - ): + if (protocol is not None and protocol.startswith('ipv6-')) or protocol in [ + 'icmpv6', + '41', + '43', + '44', + '58', + '59', + '60', + ]: return True else: return False diff --git a/openstackclient/network/v2/address_group.py b/openstackclient/network/v2/address_group.py index d47c34cddf..1b03a3304d 100644 --- a/openstackclient/network/v2/address_group.py +++ b/openstackclient/network/v2/address_group.py @@ -13,13 +13,18 @@ """Address group action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any import netaddr -from osc_lib.command import command +from openstack.network.v2 import address_group as _address_group from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -27,30 +32,34 @@ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _address_group.AddressGroup, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns ) -def _format_addresses(addresses): +def _format_addresses(addresses: list[str]) -> list[str]: return [str(netaddr.IPNetwork(addr)) for addr in addresses] -def _get_attrs(client_manager, parsed_args): - attrs = {} +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} attrs['name'] = parsed_args.name if parsed_args.description: attrs['description'] = parsed_args.description attrs['addresses'] = _format_addresses(parsed_args.address) if 'project' in parsed_args and parsed_args.project is not None: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id return attrs @@ -59,7 +68,7 @@ def _get_attrs(client_manager, parsed_args): class CreateAddressGroup(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a new Address Group") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar="", help=_("New address group name") @@ -87,7 +96,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) @@ -105,7 +116,7 @@ def take_action(self, parsed_args): class DeleteAddressGroup(command.Command): _description = _("Delete address group(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'address_group', @@ -116,7 +127,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -145,26 +156,30 @@ def take_action(self, parsed_args): class ListAddressGroup(command.Lister): _description = _("List address groups") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--name', metavar='', - help=_("List only address groups of given name in output"), + help=_("List only address groups with the specified name"), ) parser.add_argument( '--project', metavar="", help=_( - "List address groups according to their project (name or ID)" + "List only address groups with the specified project " + "(name or ID)" ), ) identity_common.add_project_domain_option_to_parser(parser) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( 'id', @@ -184,13 +199,20 @@ def take_action(self, parsed_args): if parsed_args.name: attrs['name'] = parsed_args.name if 'project' in parsed_args and parsed_args.project is not None: - identity_client = self.app.client_manager.identity - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id + if parsed_args.marker is not None: + attrs['marker'] = parsed_args.marker + if parsed_args.limit is not None: + attrs['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + attrs['max_items'] = parsed_args.max_items + data = client.address_groups(**attrs) return ( @@ -209,7 +231,7 @@ def take_action(self, parsed_args): class SetAddressGroup(common.NeutronCommandWithExtraArgs): _description = _("Set address group properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'address_group', @@ -235,7 +257,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_address_group( parsed_args.address_group, ignore_missing=False @@ -260,7 +282,7 @@ def take_action(self, parsed_args): class ShowAddressGroup(command.ShowOne): _description = _("Display address group details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'address_group', @@ -270,7 +292,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_address_group( parsed_args.address_group, ignore_missing=False @@ -284,7 +308,7 @@ def take_action(self, parsed_args): class UnsetAddressGroup(command.Command): _description = _("Unset address group properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'address_group', @@ -303,7 +327,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_address_group( parsed_args.address_group, ignore_missing=False diff --git a/openstackclient/network/v2/address_scope.py b/openstackclient/network/v2/address_scope.py index a7f396b73f..e549d1c2da 100644 --- a/openstackclient/network/v2/address_scope.py +++ b/openstackclient/network/v2/address_scope.py @@ -13,12 +13,17 @@ """Address scope action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import address_scope as _address_scope from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -26,7 +31,9 @@ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _address_scope.AddressScope, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { 'is_shared': 'shared', } @@ -36,8 +43,10 @@ def _get_columns(item): ) -def _get_attrs(client_manager, parsed_args): - attrs = {} +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} attrs['name'] = parsed_args.name attrs['ip_version'] = parsed_args.ip_version if parsed_args.share: @@ -45,23 +54,21 @@ def _get_attrs(client_manager, parsed_args): if parsed_args.no_share: attrs['shared'] = False if 'project' in parsed_args and parsed_args.project is not None: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id return attrs -# TODO(rtheis): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class CreateAddressScope(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a new Address Scope") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar="", help=_("New address scope name") @@ -96,7 +103,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) attrs.update( @@ -112,7 +121,7 @@ def take_action(self, parsed_args): class DeleteAddressScope(command.Command): _description = _("Delete address scope(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'address_scope', @@ -123,7 +132,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -149,18 +158,16 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) -# TODO(yanxing'an): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class ListAddressScope(command.Lister): _description = _("List address scopes") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--name', metavar='', - help=_("List only address scopes of given name in output"), + help=_("List only address scopes with the specified name"), ) parser.add_argument( '--ip-version', @@ -169,32 +176,37 @@ def get_parser(self, prog_name): metavar='', dest='ip_version', help=_( - "List address scopes of given IP version networks (4 or 6)" + "List only address scopes with the specified IP version " + "networks (4 or 6)" ), ) parser.add_argument( '--project', metavar="", help=_( - "List address scopes according to their project (name or ID)" + "List only address scopes with the specified project " + "(name or ID)" ), ) identity_common.add_project_domain_option_to_parser(parser) - + pagination.add_marker_pagination_option_to_parser(parser) shared_group = parser.add_mutually_exclusive_group() shared_group.add_argument( '--share', action='store_true', - help=_("List address scopes shared between projects"), + help=_("List only address scopes shared between projects"), ) shared_group.add_argument( '--no-share', action='store_true', - help=_("List address scopes not shared between projects"), + help=_("List only address scopes not shared between projects"), ) + return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( 'id', @@ -220,13 +232,19 @@ def take_action(self, parsed_args): if parsed_args.no_share: attrs['is_shared'] = False if 'project' in parsed_args and parsed_args.project is not None: - identity_client = self.app.client_manager.identity - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id + if parsed_args.marker is not None: + attrs['marker'] = parsed_args.marker + if parsed_args.limit is not None: + attrs['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + attrs['max_items'] = parsed_args.max_items data = client.address_scopes(**attrs) return ( @@ -242,12 +260,10 @@ def take_action(self, parsed_args): ) -# TODO(rtheis): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class SetAddressScope(common.NeutronCommandWithExtraArgs): _description = _("Set address scope properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'address_scope', @@ -271,7 +287,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_address_scope( parsed_args.address_scope, ignore_missing=False @@ -292,7 +308,7 @@ def take_action(self, parsed_args): class ShowAddressScope(command.ShowOne): _description = _("Display address scope details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'address_scope', @@ -302,7 +318,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_address_scope( parsed_args.address_scope, ignore_missing=False diff --git a/openstackclient/tests/functional/image/v1/__init__.py b/openstackclient/network/v2/bgpvpn/__init__.py similarity index 100% rename from openstackclient/tests/functional/image/v1/__init__.py rename to openstackclient/network/v2/bgpvpn/__init__.py diff --git a/openstackclient/network/v2/bgpvpn/bgpvpn.py b/openstackclient/network/v2/bgpvpn/bgpvpn.py new file mode 100644 index 0000000000..851a8de39d --- /dev/null +++ b/openstackclient/network/v2/bgpvpn/bgpvpn.py @@ -0,0 +1,477 @@ +# Copyright (c) 2016 Juniper Networks Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +import logging +from typing import Any + +from openstack.network.v2 import bgpvpn as _bgpvpn +from osc_lib.cli import format_columns +from osc_lib.cli import identity as osc_id +from osc_lib.cli.parseractions import KeyValueAction +from osc_lib import exceptions +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('name', 'Name', column_util.LIST_BOTH), + ('type', 'Type', column_util.LIST_BOTH), + ('route_targets', 'Route Targets', column_util.LIST_LONG_ONLY), + ('import_targets', 'Import Targets', column_util.LIST_LONG_ONLY), + ('export_targets', 'Export Targets', column_util.LIST_LONG_ONLY), + ( + 'route_distinguishers', + 'Route Distinguishers', + column_util.LIST_LONG_ONLY, + ), + ('networks', 'Associated Networks', column_util.LIST_LONG_ONLY), + ('routers', 'Associated Routers', column_util.LIST_LONG_ONLY), + ('ports', 'Associated Ports', column_util.LIST_LONG_ONLY), + ('vni', 'VNI', column_util.LIST_LONG_ONLY), + ('local_pref', 'Local Pref', column_util.LIST_LONG_ONLY), +) +_formatters = { + 'route_targets': format_columns.ListColumn, + 'import_targets': format_columns.ListColumn, + 'export_targets': format_columns.ListColumn, + 'route_distinguishers': format_columns.ListColumn, + 'networks': format_columns.ListColumn, + 'routers': format_columns.ListColumn, + 'ports': format_columns.ListColumn, +} + + +def _get_columns( + item: _bgpvpn.BgpVpn, +) -> tuple[tuple[str, ...], tuple[str, ...]]: + column_map: dict[str, str] = {} + hidden_columns = ['location', 'tenant_id'] + return osc_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns + ) + + +def _get_common_parser( + parser: argparse.ArgumentParser, update: str | None = None +) -> None: + """Adds to parser arguments common to create, set and unset commands. + + :params ArgumentParser parser: argparse object contains all command's + arguments + :params string update: Determines if it is a create command (value: None), + it is a set command (value: 'set') or if it is an unset command (value: + 'unset') + """ + ADD_RT = _("Add Route Target to import/export list") + REMOVE_RT = _("Remove Route Target from import/export list") + ADD_IMPORT_RT = _("Add Route Target to import list") + DEL_IMPORT_RT = _("Remove Route Target from import list") + ADD_EXPORT_RT = _("Add Route Target to export list") + DEL_EXPORT_RT = _("Remove Route Target from export list") + ADD_RD = _( + "Add Route Distinguisher to the list of Route Distinguishers " + "from which a Route Distinguishers will be picked from to " + "advertise a VPN route" + ) + REMOVE_RD = _( + "Remove Route Distinguisher from the list of Route " + "Distinguishers from which a Route Distinguishers will be " + "picked from to advertise a VPN route" + ) + REPEAT_RT = _("repeat option for multiple Route Targets") + REPEAT_RD = _("repeat option for multiple Route Distinguishers") + + def is_appended() -> bool: + return update is None or update == 'set' + + if update is None or update == 'set': + parser.add_argument( + '--name', + metavar="", + help=_("Name of the BGP VPN"), + ) + parser.add_argument( + '--route-target', + dest='route_targets', + action='append', + metavar="", + help=f"{ADD_RT if is_appended() else REMOVE_RT} ({REPEAT_RT})", + ) + if update: + parser.add_argument( + '--no-route-target' if update == 'set' else '--all-route-target', + dest='purge_route_target', + action='store_true', + help=_('Empty route target list'), + ) + import_target_action = ADD_IMPORT_RT if is_appended() else DEL_IMPORT_RT + parser.add_argument( + '--import-target', + dest='import_targets', + action='append', + metavar="", + help=f"{import_target_action} ({REPEAT_RT})", + ) + if update: + parser.add_argument( + '--no-import-target' if update == 'set' else '--all-import-target', + dest='purge_import_target', + action='store_true', + help=_('Empty import route target list'), + ) + export_target_action = ADD_EXPORT_RT if is_appended() else DEL_EXPORT_RT + parser.add_argument( + '--export-target', + dest='export_targets', + action='append', + metavar="", + help=f"{export_target_action} ({REPEAT_RT})", + ) + if update: + parser.add_argument( + '--no-export-target' if update == 'set' else '--all-export-target', + dest='purge_export_target', + action='store_true', + help=_('Empty export route target list'), + ) + parser.add_argument( + '--route-distinguisher', + dest='route_distinguishers', + action='append', + metavar="", + help=f"{ADD_RD if is_appended() else REMOVE_RD} ({REPEAT_RD})", + ) + if update: + parser.add_argument( + '--no-route-distinguisher' + if update == 'set' + else '--all-route-distinguisher', + dest='purge_route_distinguisher', + action='store_true', + help=_('Empty route distinguisher list'), + ) + parser.add_argument( + '--vni', + type=int, + help=_( + 'VXLAN Network Identifier to be used for this BGPVPN ' + 'when a VXLAN encapsulation is used' + ), + ) + parser.add_argument( + '--local-pref', + type=int, + dest='local_pref', + help=_( + 'Default BGP LOCAL_PREF to use in route advertisements' + 'towards this BGPVPN.' + ), + ) + + +def _args2body( + client_manager: Any, id: str, action: str, args: argparse.Namespace +) -> dict[str, Any]: + + if not ( + args.purge_route_target + and args.purge_import_target + and args.purge_export_target + and args.purge_route_distinguisher + ) and ( + args.route_targets + or args.import_targets + or args.export_targets + or args.route_distinguishers + ): + bgpvpn = client_manager.network.get_bgpvpn(id) + + attrs: dict[str, Any] = {} + + if 'name' in args and args.name is not None: + attrs['name'] = str(args.name) + + if 'vni' in args and args.vni is not None: + attrs['vni'] = args.vni + + if 'local_pref' in args and args.local_pref is not None: + attrs['local_pref'] = args.local_pref + + if args.purge_route_target: + attrs['route_targets'] = [] + elif args.route_targets: + if action == 'set': + attrs['route_targets'] = list( + set(bgpvpn['route_targets']) | set(args.route_targets) + ) + elif action == 'unset': + attrs['route_targets'] = list( + set(bgpvpn['route_targets']) - set(args.route_targets) + ) + + if args.purge_import_target: + attrs['import_targets'] = [] + elif args.import_targets: + if action == 'set': + attrs['import_targets'] = list( + set(bgpvpn['import_targets']) | set(args.import_targets) + ) + elif action == 'unset': + attrs['import_targets'] = list( + set(bgpvpn['import_targets']) - set(args.import_targets) + ) + + if args.purge_export_target: + attrs['export_targets'] = [] + elif args.export_targets: + if action == 'set': + attrs['export_targets'] = list( + set(bgpvpn['export_targets']) | set(args.export_targets) + ) + elif action == 'unset': + attrs['export_targets'] = list( + set(bgpvpn['export_targets']) - set(args.export_targets) + ) + + if args.purge_route_distinguisher: + attrs['route_distinguishers'] = [] + elif args.route_distinguishers: + if action == 'set': + attrs['route_distinguishers'] = list( + set(bgpvpn['route_distinguishers']) + | set(args.route_distinguishers) + ) + elif action == 'unset': + attrs['route_distinguishers'] = list( + set(bgpvpn['route_distinguishers']) + - set(args.route_distinguishers) + ) + + return attrs + + +class CreateBgpvpn(command.ShowOne): + _description = _("Create BGP VPN resource") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + osc_id.add_project_owner_option_to_parser(parser) + _get_common_parser(parser) + parser.add_argument( + '--type', + default='l3', + choices=['l2', 'l3'], + help=_( + "BGP VPN type selection between IP VPN (l3) and Ethernet " + "VPN (l2) (default: l3)" + ), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs = {} + if parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + if parsed_args.type is not None: + attrs['type'] = parsed_args.type + if parsed_args.route_targets is not None: + attrs['route_targets'] = parsed_args.route_targets + if parsed_args.import_targets is not None: + attrs['import_targets'] = parsed_args.import_targets + if parsed_args.export_targets is not None: + attrs['export_targets'] = parsed_args.export_targets + if parsed_args.route_distinguishers is not None: + attrs['route_distinguishers'] = parsed_args.route_distinguishers + if parsed_args.vni is not None: + attrs['vni'] = parsed_args.vni + if parsed_args.local_pref is not None: + attrs['local_pref'] = parsed_args.local_pref + if 'project' in parsed_args and parsed_args.project is not None: + project_id = osc_id.find_project( + self.app.client_manager.sdk_connection, + parsed_args.project, + parsed_args.project_domain, + ).id + attrs['project_id'] = project_id + obj = client.create_bgpvpn(**attrs) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties( + obj, columns, formatters=_formatters + ) + return display_columns, data + + +class SetBgpvpn(command.Command): + _description = _("Set BGP VPN properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN to update (name or ID)"), + ) + _get_common_parser(parser, update='set') + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + id = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)['id'] + body = _args2body(self.app.client_manager, id, 'set', parsed_args) + client.update_bgpvpn(id, **body) + + +class UnsetBgpvpn(command.Command): + _description = _("Unset BGP VPN properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN to update (name or ID)"), + ) + _get_common_parser(parser, update='unset') + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + id = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)['id'] + body = _args2body(self.app.client_manager, id, 'unset', parsed_args) + client.update_bgpvpn(id, **body) + + +class DeleteBgpvpn(command.Command): + _description = _("Delete BGP VPN resource(s)") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgpvpns', + metavar="", + nargs="+", + help=_("BGP VPN(s) to delete (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + fails = 0 + for id_or_name in parsed_args.bgpvpns: + try: + id = client.find_bgpvpn(id_or_name, ignore_missing=False)['id'] + client.delete_bgpvpn(id) + LOG.warning("BGP VPN %(id)s deleted", {'id': id}) + except Exception as e: + fails += 1 + LOG.error( + "Failed to delete BGP VPN with name or ID " + "'%(id_or_name)s': %(e)s", + {'id_or_name': id_or_name, 'e': e}, + ) + if fails > 0: + msg = _("Failed to delete %(fails)s of %(total)s BGP VPN.") % { + 'fails': fails, + 'total': len(parsed_args.bgpvpns), + } + raise exceptions.CommandError(msg) + + +class ListBgpvpn(command.Lister): + _description = _("List BGP VPN resources") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + osc_id.add_project_owner_option_to_parser(parser) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output"), + ) + parser.add_argument( + '--property', + metavar="", + default=dict(), + help=_( + "Filter property to apply on returned BGP VPNs (repeat to " + "filter on multiple properties)" + ), + action=KeyValueAction, + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + params = {} + if parsed_args.project is not None: + project_id = osc_id.find_project( + self.app.client_manager.sdk_connection, + parsed_args.project, + parsed_args.project_domain, + ).id + params['project_id'] = project_id + if parsed_args.property: + params.update(parsed_args.property) + objs = client.bgpvpns(**params) + headers, columns = column_util.get_column_definitions( + list(_attr_map), long_listing=parsed_args.long + ) + return ( + headers, + ( + osc_utils.get_dict_properties( + s, columns, formatters=_formatters + ) + for s in objs + ), + ) + + +class ShowBgpvpn(command.ShowOne): + _description = _("Show information of a given BGP VPN") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN to display (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + id = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False)['id'] + obj = client.get_bgpvpn(id) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties( + obj, columns, formatters=_formatters + ) + return display_columns, data diff --git a/openstackclient/network/v2/bgpvpn/constants.py b/openstackclient/network/v2/bgpvpn/constants.py new file mode 100644 index 0000000000..5b2931a221 --- /dev/null +++ b/openstackclient/network/v2/bgpvpn/constants.py @@ -0,0 +1,30 @@ +# Copyright (c) 2016 Juniper Networks Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +BGPVPN = 'bgpvpn' +BGPVPNS = f'{BGPVPN}s' + +NETWORK_RESOURCE_NAME = 'network' +NETWORK_ASSOCIATION = f'{NETWORK_RESOURCE_NAME}_association' +NETWORK_ASSOCIATIONS = f'{NETWORK_ASSOCIATION}s' + +ROUTER_RESOURCE_NAME = 'router' +ROUTER_ASSOCIATION = f'{ROUTER_RESOURCE_NAME}_association' +ROUTER_ASSOCIATIONS = f'{ROUTER_ASSOCIATION}s' + +PORT_RESOURCE_NAME = 'port' +PORT_ASSOCIATION = f'{PORT_RESOURCE_NAME}_association' +PORT_ASSOCIATIONS = f'{PORT_ASSOCIATION}s' diff --git a/openstackclient/network/v2/bgpvpn/network_association.py b/openstackclient/network/v2/bgpvpn/network_association.py new file mode 100644 index 0000000000..134fce4253 --- /dev/null +++ b/openstackclient/network/v2/bgpvpn/network_association.py @@ -0,0 +1,226 @@ +# Copyright (c) 2016 Juniper Networks Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +import logging +from typing import Any + +from openstack.network.v2 import ( + bgpvpn_network_association as _bgpvpn_network_association, +) +from osc_lib.cli import identity as osc_id +from osc_lib.cli import parseractions +from osc_lib import exceptions +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('network_id', 'Network ID', column_util.LIST_BOTH), +) +_formatters: dict[str, Any] = {} + + +def _get_columns( + item: _bgpvpn_network_association.BgpVpnNetworkAssociation, +) -> tuple[tuple[str, ...], tuple[str, ...]]: + column_map: dict[str, str] = {} + hidden_columns = ['location', 'name', 'tenant_id'] + return osc_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns + ) + + +class CreateBgpvpnNetAssoc(command.ShowOne): + _description = _("Create a BGP VPN network association") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + osc_id.add_project_owner_option_to_parser(parser) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN to apply the network association (name or ID)"), + ) + parser.add_argument( + 'resource', + metavar="", + help=_("Network to associate the BGP VPN (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + network = client.find_network( + parsed_args.resource, ignore_missing=False + ) + body: dict[str, Any] = {'network_id': network['id']} + if 'project' in parsed_args and parsed_args.project is not None: + project_id = osc_id.find_project( + self.app.client_manager.sdk_connection, + parsed_args.project, + parsed_args.project_domain, + ).id + body['project_id'] = project_id + + obj = client.create_bgpvpn_network_association(bgpvpn['id'], **body) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties( + obj, columns, formatters=_formatters + ) + return display_columns, data + + +class DeleteBgpvpnNetAssoc(command.Command): + _description = _( + "Delete a BGP VPN network association(s) for a given BGP VPN" + ) + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'resource_association_ids', + metavar="", + nargs="+", + help=_("Network association ID(s) to remove"), + ) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN the network association belongs to (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + fails = 0 + for id in parsed_args.resource_association_ids: + try: + client.delete_bgpvpn_network_association(bgpvpn['id'], id) + LOG.warning( + "Network association %(id)s deleted", + {'id': id}, + ) + except Exception as e: + fails += 1 + LOG.error( + "Failed to delete network " + "association with ID '%(id)s': %(e)s", + {'id': id, 'e': e}, + ) + if fails > 0: + msg = _( + "Failed to delete %(fails)s of %(total)s " + "network BGP VPN association(s)." + ) % { + 'fails': fails, + 'total': len(parsed_args.resource_association_ids), + } + raise exceptions.CommandError(msg) + + +class ListBgpvpnNetAssoc(command.Lister): + _description = _("List BGP VPN network associations for a given BGP VPN") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN listed associations belong to (name or ID)"), + ) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output"), + ) + parser.add_argument( + '--property', + metavar="", + help=_( + "Filter property to apply on returned BGP VPNs (repeat to " + "filter on multiple properties)" + ), + action=parseractions.KeyValueAction, + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + params = {} + if parsed_args.property: + params.update(parsed_args.property) + objs = client.bgpvpn_network_associations( + bgpvpn['id'], retrieve_all=True, **params + ) + headers, columns = column_util.get_column_definitions( + list(_attr_map), long_listing=parsed_args.long + ) + return ( + headers, + ( + osc_utils.get_dict_properties( + s, columns, formatters=_formatters + ) + for s in objs + ), + ) + + +class ShowBgpvpnNetAssoc(command.ShowOne): + _description = _("Show information of a given BGP VPN network association") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'resource_association_id', + metavar="", + help=_("Network association ID to look up"), + ) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN the association belongs to (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + obj = client.get_bgpvpn_network_association( + bgpvpn['id'], parsed_args.resource_association_id + ) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties( + obj, columns, formatters=_formatters + ) + return display_columns, data diff --git a/openstackclient/network/v2/bgpvpn/port_association.py b/openstackclient/network/v2/bgpvpn/port_association.py new file mode 100644 index 0000000000..ed294b999b --- /dev/null +++ b/openstackclient/network/v2/bgpvpn/port_association.py @@ -0,0 +1,504 @@ +# Copyright (c) 2017 Juniper networks Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +import logging +from typing import Any + +from openstack.network.v2 import ( + bgpvpn_port_association as _bgpvpn_port_association, +) +from osc_lib.cli import format_columns +from osc_lib.cli import identity as osc_id +from osc_lib.cli import parseractions +from osc_lib import exceptions +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('port_id', 'Port ID', column_util.LIST_BOTH), + ( + 'prefix_routes', + 'Prefix Routes (BGP LOCAL_PREF)', + column_util.LIST_LONG_ONLY, + ), + ( + 'bgpvpn_routes', + 'BGP VPN Routes (BGP LOCAL_PREF)', + column_util.LIST_LONG_ONLY, + ), + ( + 'advertise_fixed_ips', + "Advertise Port's Fixed IPs", + column_util.LIST_LONG_ONLY, + ), +) +_formatters = { + 'prefix_routes': format_columns.ListColumn, + 'bgpvpn_routes': format_columns.ListColumn, +} + + +def _get_columns( + item: _bgpvpn_port_association.BgpVpnPortAssociation, +) -> tuple[tuple[str, ...], tuple[str, ...]]: + column_map: dict[str, str] = {} + hidden_columns = ['location', 'name', 'tenant_id'] + return osc_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns + ) + + +def _transform_resource(data: Any) -> Any: + """Transforms BGP VPN port association routes property. + + Separates the two route types and formats them with ListColumn. + + {'routes': + [ + {'type': 'prefix', 'local_pref': 100, 'prefix': '8.8.8.0/27'}, + {'type': 'bgpvpn', 'local_pref': 50, + 'bgpvpn': '157d72a9-9968-48e7-8087-6c9a9bc7a181'}, + ], + } + + to + + { + 'prefix_routes': ['8.8.8.0/27 (100)'], + 'bgpvpn_routes': ['157d72a9-9968-48e7-8087-6c9a9bc7a181 (50)'], + } + """ + for route in data.get('routes', []): + local_pref = '' + if route.get('local_pref'): + local_pref = ' ({local_pref})' + if route['type'] == 'prefix': + data.setdefault('prefix_routes', []).append( + '{}{}'.format(route['prefix'], local_pref) + ) + elif route['type'] == 'bgpvpn': + data.setdefault('bgpvpn_routes', []).append( + '{}{}'.format(route['bgpvpn_id'], local_pref) + ) + else: + LOG.warning("Unknown route type %s (%s).", route['type'], route) + data.pop('routes', None) + return data + + +def _get_common_parser(parser: argparse.ArgumentParser, action: str) -> None: + """Adds to parser arguments common to create, set and unset commands. + + :params ArgumentParser parser: argparse object contains all command's + arguments + :params string action: 'create', 'set' or 'unset' + """ + ADVERTISE_ROUTE = _( + "Fixed IPs of the port will be advertised to the BGP VPN%s" + ) % (_(' (default)') if action == 'create' else "") + NOT_ADVERTISE_ROUTE = _( + "Fixed IPs of the port will not be advertised to the BGP VPN" + ) + + LOCAL_PREF_VALUE = _( + ". Optionally, can control the value of the BGP " + "LOCAL_PREF of the routes that will be " + "advertised" + ) + + ADD_PREFIX_ROUTE = ( + _("Add prefix route in CIDR notation%s") % LOCAL_PREF_VALUE + ) + REMOVE_PREFIX_ROUTE = _("Remove prefix route in CIDR notation") + REPEAT_PREFIX_ROUTE = _("repeat option for multiple prefix routes") + + ADD_BGVPVPN_ROUTE = ( + _("Add BGP VPN route for route leaking%s") % LOCAL_PREF_VALUE + ) + REMOVE_BGPVPN_ROUTE = _("Remove BGP VPN route") + REPEAT_BGPVPN_ROUTE = _("repeat option for multiple BGP VPN routes") + + group_advertise_fixed_ips = parser.add_mutually_exclusive_group() + group_advertise_fixed_ips.add_argument( + '--advertise-fixed-ips', + action='store_true', + help=NOT_ADVERTISE_ROUTE if action == 'unset' else ADVERTISE_ROUTE, + ) + group_advertise_fixed_ips.add_argument( + '--no-advertise-fixed-ips', + action='store_true', + help=ADVERTISE_ROUTE if action == 'unset' else NOT_ADVERTISE_ROUTE, + ) + + if action in ['create', 'set']: + parser.add_argument( + '--prefix-route', + metavar="prefix=[,local_pref=]", + dest='prefix_routes', + action=parseractions.MultiKeyValueAction, + required_keys=['prefix'], + optional_keys=['local_pref'], + help=f"{ADD_PREFIX_ROUTE} ({REPEAT_PREFIX_ROUTE})", + ) + parser.add_argument( + '--bgpvpn-route', + metavar="bgpvpn=[,local_pref=]", + dest='bgpvpn_routes', + action=parseractions.MultiKeyValueAction, + required_keys=['bgpvpn'], + optional_keys=['local_pref'], + help=f"{ADD_BGVPVPN_ROUTE} ({REPEAT_BGPVPN_ROUTE})", + ) + else: + parser.add_argument( + '--prefix-route', + metavar="", + dest='prefix_routes', + action='append', + help=f"{REMOVE_PREFIX_ROUTE} ({REPEAT_PREFIX_ROUTE})", + ) + parser.add_argument( + '--bgpvpn-route', + metavar="", + dest='bgpvpn_routes', + action='append', + help=f"{REMOVE_BGPVPN_ROUTE} ({REPEAT_BGPVPN_ROUTE})", + ) + if action != 'create': + parser.add_argument( + '--no-prefix-route' if action == 'set' else '--all-prefix-routes', + dest='purge_prefix_route', + action='store_true', + help=_('Empty prefix route list'), + ) + parser.add_argument( + '--no-bgpvpn-route' if action == 'set' else '--all-bgpvpn-routes', + dest='purge_bgpvpn_route', + action='store_true', + help=_('Empty BGP VPN route list'), + ) + + +def _args2body( + client: Any, action: str, bgpvpn_id: str, args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} + + if action != 'create': + assoc = client.find_bgpvpn_port_association( + args.port_association_id, + bgpvpn_id=bgpvpn_id, + ignore_missing=False, + ) + else: + assoc = {'routes': []} + + if args.advertise_fixed_ips: + attrs['advertise_fixed_ips'] = action != 'unset' + elif args.no_advertise_fixed_ips: + attrs['advertise_fixed_ips'] = action == 'unset' + + prefix_routes: dict[str, Any] | None = None + if 'purge_prefix_route' in args and args.purge_prefix_route: + prefix_routes = {} + else: + prefix_routes = { + r['prefix']: r.get('local_pref') + for r in assoc['routes'] + if r['type'] == 'prefix' + } + if args.prefix_routes: + if action in ['create', 'set']: + prefix_routes.update( + { + r['prefix']: r.get('local_pref') + for r in args.prefix_routes + } + ) + elif action == 'unset': + for prefix in args.prefix_routes: + prefix_routes.pop(prefix, None) + + bgpvpn_routes: dict[str, Any] | None = None + if 'purge_bgpvpn_route' in args and args.purge_bgpvpn_route: + bgpvpn_routes = {} + else: + bgpvpn_routes = { + r['bgpvpn_id']: r.get('local_pref') + for r in assoc['routes'] + if r['type'] == 'bgpvpn' + } + if args.bgpvpn_routes: + if action == 'unset': + routes = [{'bgpvpn': bgpvpn} for bgpvpn in args.bgpvpn_routes] + else: + routes = args.bgpvpn_routes + args_bgpvpn_routes = { + client.find_bgpvpn( + r['bgpvpn'], ignore_missing=False + ).id: r.get('local_pref') + for r in routes + } + if action in ['create', 'set']: + bgpvpn_routes.update(args_bgpvpn_routes) + elif action == 'unset': + for bgpvpn_id in args_bgpvpn_routes: + bgpvpn_routes.pop(bgpvpn_id, None) + + if prefix_routes is not None and not prefix_routes: + attrs.setdefault('routes', []) + elif prefix_routes is not None: + for prefix, local_pref in prefix_routes.items(): + route: dict[str, Any] = { + 'type': 'prefix', + 'prefix': prefix, + } + if local_pref: + route['local_pref'] = int(local_pref) + attrs.setdefault('routes', []).append(route) + if bgpvpn_routes is not None and not bgpvpn_routes: + attrs.setdefault('routes', []) + elif bgpvpn_routes is not None: + for bgpvpn_id, local_pref in bgpvpn_routes.items(): + route = { + 'type': 'bgpvpn', + 'bgpvpn_id': bgpvpn_id, + } + if local_pref: + route['local_pref'] = int(local_pref) + attrs.setdefault('routes', []).append(route) + + return attrs + + +class CreateBgpvpnPortAssoc(command.ShowOne): + _description = _("Create a BGP VPN port association") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + osc_id.add_project_owner_option_to_parser(parser) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN to apply the port association (name or ID)"), + ) + parser.add_argument( + 'port', + metavar="", + help=_("Port to associate the BGP VPN (name or ID)"), + ) + _get_common_parser(parser, 'create') + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + port = client.find_port(parsed_args.port, ignore_missing=False) + body: dict[str, Any] = {'port_id': port['id']} + if 'project' in parsed_args and parsed_args.project is not None: + project_id = osc_id.find_project( + self.app.client_manager.sdk_connection, + parsed_args.project, + parsed_args.project_domain, + ).id + body['project_id'] = project_id + + body.update(_args2body(client, 'create', bgpvpn['id'], parsed_args)) + + obj = client.create_bgpvpn_port_association(bgpvpn['id'], **body) + _transform_resource(obj) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties( + obj, columns, formatters=_formatters + ) + return display_columns, data + + +class SetBgpvpnPortAssoc(command.Command): + _description = _("Set BGP VPN port association properties") + _action = 'set' + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'port_association_id', + metavar="", + help=_("Port association ID to update"), + ) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN the port association belongs to (name or ID)"), + ) + _get_common_parser(parser, self._action) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + body = _args2body(client, self._action, bgpvpn['id'], parsed_args) + client.update_bgpvpn_port_association( + bgpvpn['id'], parsed_args.port_association_id, **body + ) + + +class UnsetBgpvpnPortAssoc(SetBgpvpnPortAssoc): + _description = _("Unset BGP VPN port association properties") + _action = 'unset' + + +class DeleteBgpvpnPortAssoc(command.Command): + _description = _( + "Delete a BGP VPN port association(s) for a given BGP VPN" + ) + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'port_association_ids', + metavar="", + nargs="+", + help=_("Port association ID(s) to remove"), + ) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN the port association belongs to (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + fails = 0 + for id in parsed_args.port_association_ids: + try: + client.delete_bgpvpn_port_association(bgpvpn['id'], id) + LOG.warning( + "Port association %(id)s deleted", + {'id': id}, + ) + except Exception as e: + fails += 1 + LOG.error( + "Failed to delete port " + "association with ID '%(id)s': %(e)s", + {'id': id, 'e': e}, + ) + if fails > 0: + msg = _( + "Failed to delete %(fails)s of %(total)s " + "port BGP VPN association(s)." + ) % { + 'fails': fails, + 'total': len(parsed_args.port_association_ids), + } + raise exceptions.CommandError(msg) + + +class ListBgpvpnPortAssoc(command.Lister): + _description = _("List BGP VPN port associations for a given BGP VPN") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN listed associations belong to (name or ID)"), + ) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output"), + ) + parser.add_argument( + '--property', + metavar="", + help=_( + "Filter property to apply on returned BGP VPNs (repeat to " + "filter on multiple properties)" + ), + action=parseractions.KeyValueAction, + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + params = {} + if parsed_args.property: + params.update(parsed_args.property) + objs = client.bgpvpn_port_associations( + bgpvpn['id'], retrieve_all=True, **params + ) + transformed_objs = [_transform_resource(obj) for obj in objs] + headers, columns = column_util.get_column_definitions( + list(_attr_map), long_listing=parsed_args.long + ) + return ( + headers, + ( + osc_utils.get_dict_properties( + s, columns, formatters=_formatters + ) + for s in transformed_objs + ), + ) + + +class ShowBgpvpnPortAssoc(command.ShowOne): + _description = _("Show information of a given BGP VPN port association") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'port_association_id', + metavar="", + help=_("Port association ID to look up"), + ) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN the association belongs to (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + obj = client.get_bgpvpn_port_association( + bgpvpn['id'], parsed_args.port_association_id + ) + _transform_resource(obj) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties( + obj, columns, formatters=_formatters + ) + return display_columns, data diff --git a/openstackclient/network/v2/bgpvpn/router_association.py b/openstackclient/network/v2/bgpvpn/router_association.py new file mode 100644 index 0000000000..2e505b79f0 --- /dev/null +++ b/openstackclient/network/v2/bgpvpn/router_association.py @@ -0,0 +1,301 @@ +# Copyright (c) 2016 Juniper networks Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +import logging +from typing import Any + +from openstack.network.v2 import ( + bgpvpn_router_association as _bgpvpn_router_association, +) +from osc_lib.cli import identity as osc_id +from osc_lib.cli import parseractions +from osc_lib import exceptions +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('router_id', 'Router ID', column_util.LIST_BOTH), + ( + 'advertise_extra_routes', + 'Advertise extra routes', + column_util.LIST_LONG_ONLY, + ), +) +_formatters: dict[str, osc_utils.FormatterT] = {} + + +def _get_columns( + item: _bgpvpn_router_association.BgpVpnRouterAssociation, +) -> tuple[tuple[str, ...], tuple[str, ...]]: + column_map: dict[str, str] = {} + hidden_columns = ['location', 'name', 'tenant_id'] + return osc_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns + ) + + +def _get_common_parser(parser: argparse.ArgumentParser, action: str) -> None: + """Adds to parser arguments common to create, set and unset commands. + + :params ArgumentParser parser: argparse object contains all command's + arguments + :params string action: 'create', 'set' or 'unset' + """ + ADVERTISE_ROUTES = _("Routes will be advertised to the BGP VPN%s") % ( + _(' (default)') if action == 'create' else "" + ) + NOT_ADVERTISE_ROUTES = _( + "Routes from the router will not be advertised to the BGP VPN" + ) + + group_advertise_extra_routes = parser.add_mutually_exclusive_group() + group_advertise_extra_routes.add_argument( + '--advertise_extra_routes', + action='store_true', + help=NOT_ADVERTISE_ROUTES if action == 'unset' else ADVERTISE_ROUTES, + ) + group_advertise_extra_routes.add_argument( + '--no-advertise_extra_routes', + action='store_true', + help=ADVERTISE_ROUTES if action == 'unset' else NOT_ADVERTISE_ROUTES, + ) + + +def _args2body(action: str, args: argparse.Namespace) -> dict[str, Any]: + attrs = {'advertise_extra_routes': False} + if args.advertise_extra_routes: + attrs['advertise_extra_routes'] = action != 'unset' + elif args.no_advertise_extra_routes: + attrs['advertise_extra_routes'] = action == 'unset' + return attrs + + +class CreateBgpvpnRouterAssoc(command.ShowOne): + _description = _("Create a BGP VPN router association") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + osc_id.add_project_owner_option_to_parser(parser) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN to apply the router association (name or ID)"), + ) + parser.add_argument( + 'resource', + metavar="", + help=_("Router to associate the BGP VPN (name or ID)"), + ) + _get_common_parser(parser, 'create') + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + router = client.find_router(parsed_args.resource, ignore_missing=False) + body = {'router_id': router['id']} + if 'project' in parsed_args and parsed_args.project is not None: + project_id = osc_id.find_project( + self.app.client_manager.sdk_connection, + parsed_args.project, + parsed_args.project_domain, + ).id + body['project_id'] = project_id + + body.update(_args2body('create', parsed_args)) + + obj = client.create_bgpvpn_router_association(bgpvpn['id'], **body) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties( + obj, columns, formatters=_formatters + ) + return display_columns, data + + +class SetBgpvpnRouterAssoc(command.Command): + _description = _("Set BGP VPN router association properties") + _action = 'set' + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'resource_association_id', + metavar="", + help=_("Router association ID to update"), + ) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN the router association belongs to (name or ID)"), + ) + _get_common_parser(parser, self._action) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + body = _args2body(self._action, parsed_args) + client.update_bgpvpn_router_association( + bgpvpn['id'], parsed_args.resource_association_id, **body + ) + + +class UnsetBgpvpnRouterAssoc(SetBgpvpnRouterAssoc): + _description = _("Unset BGP VPN router association properties") + _action = 'unset' + + +class DeleteBgpvpnRouterAssoc(command.Command): + _description = _( + "Delete a BGP VPN router association(s) for a given BGP VPN" + ) + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'resource_association_ids', + metavar="", + nargs="+", + help=_("Router association ID(s) to remove"), + ) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN the router association belongs to (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + fails = 0 + for id in parsed_args.resource_association_ids: + try: + client.delete_bgpvpn_router_association(bgpvpn['id'], id) + LOG.warning( + "Router association %(id)s deleted", + {'id': id}, + ) + except Exception as e: + fails += 1 + LOG.error( + "Failed to delete router " + "association with ID '%(id)s': %(e)s", + {'id': id, 'e': e}, + ) + if fails > 0: + msg = _( + "Failed to delete %(fails)s of %(total)s " + "router BGP VPN association(s)." + ) % { + 'fails': fails, + 'total': len(parsed_args.resource_association_ids), + } + raise exceptions.CommandError(msg) + + +class ListBgpvpnRouterAssoc(command.Lister): + _description = _("List BGP VPN router associations for a given BGP VPN") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN listed associations belong to (name or ID)"), + ) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output"), + ) + parser.add_argument( + '--property', + metavar="", + help=_( + "Filter property to apply on returned BGP VPNs (repeat to " + "filter on multiple properties)" + ), + action=parseractions.KeyValueAction, + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + params = {} + if parsed_args.property: + params.update(parsed_args.property) + objs = client.bgpvpn_router_associations( + bgpvpn['id'], retrieve_all=True, **params + ) + headers, columns = column_util.get_column_definitions( + list(_attr_map), long_listing=parsed_args.long + ) + return ( + headers, + ( + osc_utils.get_dict_properties( + s, columns, formatters=_formatters + ) + for s in objs + ), + ) + + +class ShowBgpvpnRouterAssoc(command.ShowOne): + _description = _("Show information of a given BGP VPN router association") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'resource_association_id', + metavar="", + help=_("Router association ID to look up"), + ) + parser.add_argument( + 'bgpvpn', + metavar="", + help=_("BGP VPN the association belongs to (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + bgpvpn = client.find_bgpvpn(parsed_args.bgpvpn, ignore_missing=False) + obj = client.get_bgpvpn_router_association( + bgpvpn['id'], parsed_args.resource_association_id + ) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties( + obj, columns, formatters=_formatters + ) + return display_columns, data diff --git a/openstackclient/network/v2/default_security_group_rule.py b/openstackclient/network/v2/default_security_group_rule.py index fddec68747..8649baab7c 100644 --- a/openstackclient/network/v2/default_security_group_rule.py +++ b/openstackclient/network/v2/default_security_group_rule.py @@ -13,13 +13,19 @@ """Default Security Group Rule action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any +from openstack.network.v2 import ( + default_security_group_rule as _default_security_group_rule, +) from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.network import common from openstackclient.network import utils as network_utils @@ -27,7 +33,9 @@ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _default_security_group_rule.DefaultSecurityGroupRule, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'name', 'revision_number'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns @@ -44,7 +52,7 @@ class CreateDefaultSecurityGroupRule( groups. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -149,8 +157,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.sdk_connection.network + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network # Build the create attributes. attrs = {} attrs['protocol'] = network_utils.get_protocol(parsed_args) @@ -236,7 +246,7 @@ class DeleteDefaultSecurityGroupRule(command.Command): default security groups. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'rule', @@ -246,9 +256,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: result = 0 - client = self.app.client_manager.sdk_connection.network + client = self.app.client_manager.network for r in parsed_args.rule: try: obj = client.find_default_security_group_rule( @@ -281,18 +291,21 @@ class ListDefaultSecurityGroupRule(command.Lister): security groups. """ - def _format_network_security_group_rule(self, rule): + @staticmethod + def _format_network_security_group_rule( + rule: _default_security_group_rule.DefaultSecurityGroupRule, + ) -> dict[str, object]: """Transform the SDK DefaultSecurityGroupRule object to a dict The SDK object gets in the way of reformatting columns... Create port_range column from port_range_min and port_range_max """ - rule = rule.to_dict() - rule['port_range'] = network_utils.format_network_port_range(rule) - rule['remote_ip_prefix'] = network_utils.format_remote_ip_prefix(rule) - return rule + data = rule.to_dict() + data['port_range'] = network_utils.format_network_port_range(data) + data['remote_ip_prefix'] = network_utils.format_remote_ip_prefix(data) + return data - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -300,7 +313,8 @@ def get_parser(self, prog_name): metavar='', type=network_utils.convert_to_lowercase, help=_( - "List rules by the IP protocol (ah, dhcp, egp, esp, gre, " + "List only default rules with the specified IP protocol " + "(ah, dhcp, egp, esp, gre, " "icmp, igmp, ipv6-encap, ipv6-frag, ipv6-icmp, " "ipv6-nonxt, ipv6-opts, ipv6-route, ospf, pgm, rsvp, " "sctp, tcp, udp, udplite, vrrp and integer " @@ -319,7 +333,7 @@ def get_parser(self, prog_name): '--ingress', action='store_true', help=_( - "List default rules which will be applied to incoming " + "List only default rules which will be applied to incoming " "network traffic" ), ) @@ -327,14 +341,16 @@ def get_parser(self, prog_name): '--egress', action='store_true', help=_( - "List default rules which will be applied to outgoing " + "List only default rules which will be applied to outgoing " "network traffic" ), ) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.sdk_connection.network + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network column_headers = ( 'ID', 'IP Protocol', @@ -393,7 +409,7 @@ class ShowDefaultSecurityGroupRule(command.ShowOne): created. This rule may not be present on existing default security groups. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'rule', @@ -402,8 +418,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.sdk_connection.network + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network obj = client.find_default_security_group_rule( parsed_args.rule, ignore_missing=False ) diff --git a/openstackclient/network/v2/dynamic_routing/__init__.py b/openstackclient/network/v2/dynamic_routing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/network/v2/dynamic_routing/bgp_dragent.py b/openstackclient/network/v2/dynamic_routing/bgp_dragent.py new file mode 100644 index 0000000000..42226451d9 --- /dev/null +++ b/openstackclient/network/v2/dynamic_routing/bgp_dragent.py @@ -0,0 +1,133 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 collections.abc import Iterable, Sequence +from typing import Any + +from osc_lib import utils + +from openstackclient import command +from openstackclient.i18n import _ + + +class AddBgpSpeakerToDRAgent(command.Command): + """Add a BGP speaker to a dynamic routing agent""" + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'dragent_id', + metavar='', + help=_("ID of the dynamic routing agent"), + ) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("ID or name of the BGP speaker"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker( + parsed_args.bgp_speaker, ignore_missing=False + ).id + client.add_bgp_speaker_to_dragent(parsed_args.dragent_id, speaker_id) + + +class RemoveBgpSpeakerFromDRAgent(command.Command): + """Removes a BGP speaker from a dynamic routing agent""" + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'dragent_id', + metavar='', + help=_("ID of the dynamic routing agent"), + ) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("ID or name of the BGP speaker"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker( + parsed_args.bgp_speaker, ignore_missing=False + ).id + client.remove_bgp_speaker_from_dragent( + parsed_args.dragent_id, speaker_id + ) + + +class ListDRAgent(command.Lister): + """List dynamic routing agents""" + + resource = 'agent' + list_columns = ['id', 'host', 'admin_state_up', 'alive'] + unknown_parts_flag = False + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--bgp-speaker', + metavar='', + help=_( + "List dynamic routing agents hosting a " + "BGP speaker (name or ID)" + ), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + data: Iterable[Any] + if parsed_args.bgp_speaker is not None: + speaker_id = client.find_bgp_speaker( + parsed_args.bgp_speaker, ignore_missing=False + ).id + data = client.get_bgp_dragents_hosting_speaker(speaker_id) + else: + data = client.agents(agent_type='BGP dynamic routing agent') + columns = ( + 'id', + 'agent_type', + 'host', + 'availability_zone', + 'is_alive', + 'is_admin_state_up', + 'binary', + ) + column_headers = ( + 'ID', + 'Agent Type', + 'Host', + 'Availability Zone', + 'Alive', + 'State', + 'Binary', + ) + return ( + column_headers, + ( + utils.get_item_properties( + s, + columns, + ) + for s in data + ), + ) diff --git a/openstackclient/network/v2/dynamic_routing/bgp_peer.py b/openstackclient/network/v2/dynamic_routing/bgp_peer.py new file mode 100644 index 0000000000..e20416e262 --- /dev/null +++ b/openstackclient/network/v2/dynamic_routing/bgp_peer.py @@ -0,0 +1,219 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 collections.abc import Iterable, Sequence +from typing import Any + +from osc_lib.cli import identity as identity_utils +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network import utils as network_utils + + +MIN_AS_NUM = 1 +MAX_AS_NUM = 4294967295 + + +def _get_attrs(parsed_args: argparse.Namespace) -> dict[str, Any]: + attrs = {} + + # Validate password + if 'auth_type' in parsed_args: + if parsed_args.auth_type != 'none': + if 'password' not in parsed_args or parsed_args.password is None: + raise exceptions.CommandError( + _('Must provide password if auth-type is specified.') + ) + if ( + parsed_args.auth_type == 'none' + and parsed_args.password is not None + ): + raise exceptions.CommandError( + _('Must provide auth-type if password is specified.') + ) + attrs['auth_type'] = parsed_args.auth_type + + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if 'remote_as' in parsed_args: + attrs['remote_as'] = parsed_args.remote_as + if 'peer_ip' in parsed_args: + attrs['peer_ip'] = parsed_args.peer_ip + if 'password' in parsed_args: + attrs['password'] = parsed_args.password + return attrs + + +class CreateBgpPeer(command.ShowOne): + _description = _("Create a BGP peer") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'name', metavar='', help=_("Name of the BGP peer to create") + ) + parser.add_argument( + '--peer-ip', + metavar='', + required=True, + help=_("Peer IP address"), + ) + parser.add_argument( + '--remote-as', + required=True, + metavar='', + help=_( + "Peer AS number. (Integer in [%(min_val)s, %(max_val)s] " + "is allowed)" + ) + % { + 'min_val': MIN_AS_NUM, + 'max_val': MAX_AS_NUM, + }, + ) + parser.add_argument( + '--auth-type', + metavar='', + choices=['none', 'md5'], + type=network_utils.convert_to_lowercase, + default='none', + help=_( + "Authentication algorithm. Supported algorithms: " + "none (default), md5" + ), + ) + parser.add_argument( + '--password', + metavar='', + help=_("Authentication password"), + ) + identity_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + + attrs = _get_attrs(parsed_args) + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + + obj = client.create_bgp_peer(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, {}, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteBgpPeer(command.Command): + _description = _("Delete a BGP peer") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgp_peer', + metavar="", + help=_("BGP peer to delete (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + id = client.find_bgp_peer( + parsed_args.bgp_peer, ignore_missing=False + ).id + client.delete_bgp_peer(id) + + +class ListBgpPeer(command.Lister): + _description = _("List BGP peers") + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + data = self.app.client_manager.network.bgp_peers(retrieve_all=True) + headers = ('ID', 'Name', 'Peer IP', 'Remote AS') + columns = ('id', 'name', 'peer_ip', 'remote_as') + return ( + headers, + ( + utils.get_dict_properties( + s, + columns, + ) + for s in data + ), + ) + + +class SetBgpPeer(command.Command): + _description = _("Update a BGP peer") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument('--name', help=_("Updated name of the BGP peer")) + parser.add_argument( + '--password', + metavar='', + help=_("Updated authentication password"), + ) + parser.add_argument( + 'bgp_peer', + metavar="", + help=_("BGP peer to update (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + id = client.find_bgp_peer( + parsed_args.bgp_peer, ignore_missing=False + ).id + attrs = _get_attrs(parsed_args) + client.update_bgp_peer(id, **attrs) + + +class ShowBgpPeer(command.ShowOne): + _description = _("Show information for a BGP peer") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgp_peer', + metavar="", + help=_("BGP peer to display (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + obj = client.find_bgp_peer(parsed_args.bgp_peer, ignore_missing=False) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, {}, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns) + return display_columns, data diff --git a/openstackclient/network/v2/dynamic_routing/bgp_speaker.py b/openstackclient/network/v2/dynamic_routing/bgp_speaker.py new file mode 100644 index 0000000000..26760e160e --- /dev/null +++ b/openstackclient/network/v2/dynamic_routing/bgp_speaker.py @@ -0,0 +1,373 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 collections.abc import Iterable, Sequence +from typing import Any + +from osc_lib.cli import identity as identity_utils +from osc_lib import utils + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network.v2.dynamic_routing import bgp_peer + + +def _get_attrs(parsed_args: argparse.Namespace) -> dict[str, Any]: + attrs: dict[str, Any] = {} + if parsed_args.name is not None: + attrs['name'] = str(parsed_args.name) + if 'local_as' in parsed_args: + attrs['local_as'] = parsed_args.local_as + if 'ip_version' in parsed_args: + attrs['ip_version'] = parsed_args.ip_version + if parsed_args.advertise_tenant_networks: + attrs['advertise_tenant_networks'] = True + if parsed_args.no_advertise_tenant_networks: + attrs['advertise_tenant_networks'] = False + if parsed_args.advertise_floating_ip_host_routes: + attrs['advertise_floating_ip_host_routes'] = True + if parsed_args.no_advertise_floating_ip_host_routes: + attrs['advertise_floating_ip_host_routes'] = False + return attrs + + +def add_common_arguments(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + '--advertise-floating-ip-host-routes', + action='store_true', + help=_( + "Enable the advertisement of floating IP host routes " + "by the BGP speaker. (default)" + ), + ) + parser.add_argument( + '--no-advertise-floating-ip-host-routes', + action='store_true', + help=_( + "Disable the advertisement of floating IP host routes " + "by the BGP speaker." + ), + ) + parser.add_argument( + '--advertise-tenant-networks', + action='store_true', + help=_( + "Enable the advertisement of tenant network routes " + "by the BGP speaker. (default)" + ), + ) + parser.add_argument( + '--no-advertise-tenant-networks', + action='store_true', + help=_( + "Disable the advertisement of tenant network routes " + "by the BGP speaker." + ), + ) + + +class AddNetworkToSpeaker(command.Command): + _description = _("Add a network to a BGP speaker") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("BGP speaker (name or ID)"), + ) + parser.add_argument( + 'network', + metavar='', + help=_("Network to add (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker( + parsed_args.bgp_speaker, ignore_missing=False + ).id + net_id = client.find_network( + parsed_args.network, ignore_missing=False + ).id + client.add_gateway_network_to_speaker(speaker_id, net_id) + + +class AddPeerToSpeaker(command.Command): + _description = _("Add a peer to a BGP speaker") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("BGP speaker (name or ID)"), + ) + parser.add_argument( + 'bgp_peer', + metavar='', + help=_("BGP Peer to add (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker( + parsed_args.bgp_speaker, ignore_missing=False + )['id'] + peer_id = client.find_bgp_peer( + parsed_args.bgp_peer, ignore_missing=False + )['id'] + client.add_bgp_peer_to_speaker(speaker_id, peer_id) + + +class CreateBgpSpeaker(command.ShowOne): + _description = _("Create a BGP speaker") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_("Name of the BGP speaker to create"), + ) + parser.add_argument( + '--local-as', + metavar='', + required=True, + help=_( + "Local AS number. (Integer in [%(min_val)s, %(max_val)s] " + "is allowed.)" + ) + % { + 'min_val': bgp_peer.MIN_AS_NUM, + 'max_val': bgp_peer.MAX_AS_NUM, + }, + ) + parser.add_argument( + '--ip-version', + type=int, + choices=[4, 6], + default=4, + help=_("IP version for the BGP speaker (default is 4)"), + ) + add_common_arguments(parser) + identity_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + + attrs = _get_attrs(parsed_args) + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + + obj = client.create_bgp_speaker(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, {}, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteBgpSpeaker(command.Command): + _description = _("Delete a BGP speaker") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar="", + help=_("BGP speaker to delete (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + id = client.find_bgp_speaker( + parsed_args.bgp_speaker, ignore_missing=False + ).id + client.delete_bgp_speaker(id) + + +class ListBgpSpeaker(command.Lister): + _description = _("List BGP speakers") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--agent', + metavar='', + help=_("List BGP speakers hosted by an agent (ID only)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + data: Iterable[Any] + if parsed_args.agent is not None: + data = client.get_bgp_speakers_hosted_by_dragent(parsed_args.agent) + else: + data = client.bgp_speakers(retrieve_all=True) + + headers = ('ID', 'Name', 'Local AS', 'IP Version') + columns = ('id', 'name', 'local_as', 'ip_version') + return (headers, (utils.get_dict_properties(s, columns) for s in data)) + + +class ListRoutesAdvertisedBySpeaker(command.Lister): + _description = _("List routes advertised") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("BGP speaker (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker( + parsed_args.bgp_speaker, ignore_missing=False + )['id'] + data = client.get_advertised_routes_of_speaker(speaker_id) + headers = ('Destination', 'Nexthop') + columns = ('destination', 'next_hop') + return ( + headers, + ( + utils.get_dict_properties(s, columns) + for s in data['advertised_routes'] + ), + ) + + +class RemoveNetworkFromSpeaker(command.Command): + _description = _("Remove a network from a BGP speaker") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("BGP speaker (name or ID)"), + ) + parser.add_argument( + 'network', + metavar='', + help=_("Network to remove (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker( + parsed_args.bgp_speaker, ignore_missing=False + )['id'] + net_id = client.find_network( + parsed_args.network, ignore_missing=False + )['id'] + client.remove_gateway_network_from_speaker(speaker_id, net_id) + + +class RemovePeerFromSpeaker(command.Command): + _description = _("Remove a peer from a BGP speaker") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar='', + help=_("BGP speaker (name or ID)"), + ) + parser.add_argument( + 'bgp_peer', + metavar='', + help=_("BGP Peer to remove (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + speaker_id = client.find_bgp_speaker( + parsed_args.bgp_speaker, ignore_missing=False + )['id'] + peer_id = client.find_bgp_peer( + parsed_args.bgp_peer, ignore_missing=False + )['id'] + client.remove_bgp_peer_from_speaker(speaker_id, peer_id) + + +class SetBgpSpeaker(command.Command): + _description = _("Set BGP speaker properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar="", + help=_("BGP speaker to update (name or ID)"), + ) + parser.add_argument('--name', help=_("New name for the BGP speaker")) + add_common_arguments(parser) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + id = client.find_bgp_speaker( + parsed_args.bgp_speaker, ignore_missing=False + ).id + attrs = _get_attrs(parsed_args) + client.update_bgp_speaker(id, **attrs) + + +class ShowBgpSpeaker(command.ShowOne): + _description = _("Show a BGP speaker") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'bgp_speaker', + metavar="", + help=_("BGP speaker to display (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + obj = client.find_bgp_speaker( + parsed_args.bgp_speaker, ignore_missing=False + ) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, {}, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns) + return display_columns, data diff --git a/openstackclient/network/v2/floating_ip.py b/openstackclient/network/v2/floating_ip.py index ea1281177b..05e921b475 100644 --- a/openstackclient/network/v2/floating_ip.py +++ b/openstackclient/network/v2/floating_ip.py @@ -12,34 +12,48 @@ """IP Floating action implementations""" +import argparse +from collections.abc import Iterable, Sequence +import logging +from typing import Any + from openstack import exceptions as sdk_exceptions +from openstack.network.v2 import floating_ip as _floating_ip from osc_lib.cli import format_columns +from osc_lib import exceptions from osc_lib import utils from osc_lib.utils import tags as _tag -from openstackclient.api import compute_v2 +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common +LOG = logging.getLogger(__name__) + _formatters = { 'port_details': format_columns.DictColumn, } -def _get_network_columns(item): +def _get_network_columns( + item: _floating_ip.FloatingIP, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns ) -def _get_columns(item): +def _get_columns(item: Any) -> tuple[str, ...]: columns = list(item.keys()) return tuple(sorted(columns)) -def _get_attrs(client_manager, parsed_args): +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: attrs = {} network_client = client_manager.network @@ -75,12 +89,12 @@ def _get_attrs(client_manager, parsed_args): attrs['description'] = parsed_args.description if parsed_args.project: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id if parsed_args.dns_domain: @@ -92,104 +106,82 @@ def _get_attrs(client_manager, parsed_args): return attrs -class CreateFloatingIP( - common.NetworkAndComputeShowOne, common.NeutronCommandWithExtraArgs -): +class CreateFloatingIP(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create floating IP") - def update_parser_common(self, parser): - # In Compute v2 network, floating IPs could be allocated from floating - # IP pools, which are actually external networks. So deprecate the - # parameter "pool", and use "network" instead. + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'network', metavar='', help=_("Network to allocate floating IP from (name or ID)"), ) - return parser - - def update_parser_network(self, parser): parser.add_argument( '--subnet', metavar='', - help=self.enhance_help_neutron( - _( - "Subnet on which you want to create the floating IP " - "(name or ID)" - ) + help=_( + "Subnet on which you want to create the floating IP " + "(name or ID)" ), ) parser.add_argument( '--port', metavar='', - help=self.enhance_help_neutron( - _("Port to be associated with the floating IP (name or ID)") - ), + help=_("Port to be associated with the floating IP (name or ID)"), ) parser.add_argument( '--floating-ip-address', metavar='', dest='floating_ip_address', - help=self.enhance_help_neutron(_("Floating IP address")), + help=_("Floating IP address"), ) parser.add_argument( '--fixed-ip-address', metavar='', dest='fixed_ip_address', - help=self.enhance_help_neutron( - _("Fixed IP address mapped to the floating IP") - ), + help=_("Fixed IP address mapped to the floating IP"), ) parser.add_argument( '--qos-policy', metavar='', - help=self.enhance_help_neutron( - _("Attach QoS policy to the floating IP (name or ID)") - ), + help=_("Attach QoS policy to the floating IP (name or ID)"), ) parser.add_argument( '--description', metavar='', - help=self.enhance_help_neutron(_('Set floating IP description')), + help=_('Set floating IP description'), ) parser.add_argument( '--project', metavar='', - help=self.enhance_help_neutron(_("Owner's project (name or ID)")), + help=_("Owner's project (name or ID)"), ) parser.add_argument( '--dns-domain', metavar='', dest='dns_domain', - help=self.enhance_help_neutron( - _("Set DNS domain for this floating IP") - ), + help=_("Set DNS domain for this floating IP"), ) parser.add_argument( '--dns-name', metavar='', dest='dns_name', - help=self.enhance_help_neutron( - _("Set DNS name for this floating IP") - ), + help=_("Set DNS name for this floating IP"), ) - identity_common.add_project_domain_option_to_parser( - parser, enhance_help=self.enhance_help_neutron - ) - _tag.add_tag_option_to_parser_for_create( - parser, _('floating IP'), enhance_help=self.enhance_help_neutron - ) + identity_common.add_project_domain_option_to_parser(parser) + _tag.add_tag_option_to_parser_for_create(parser, _('floating IP')) return parser - def take_action_network(self, client, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) attrs.update( self._parse_extra_properties(parsed_args.extra_properties) ) - with common.check_missing_extension_if_error( - self.app.client_manager.network, attrs - ): + with common.check_missing_extension_if_error(client, attrs): obj = client.create_ip(**attrs) # tags cannot be set when created, so tags need to be set later. @@ -199,21 +191,12 @@ def take_action_network(self, client, parsed_args): data = utils.get_item_properties(obj, columns) return (display_columns, data) - def take_action_compute(self, client, parsed_args): - obj = compute_v2.create_floating_ip(client, parsed_args.network) - columns = _get_columns(obj) - data = utils.get_dict_properties(obj, columns) - return (columns, data) - -class DeleteFloatingIP(common.NetworkAndComputeDelete): +class DeleteFloatingIP(command.Command): _description = _("Delete floating IP(s)") - # Used by base class to find resources in parsed_args. - resource = 'floating_ip' - r = None - - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'floating_ip', metavar="", @@ -222,101 +205,120 @@ def update_parser_common(self, parser): ) return parser - def take_action_network(self, client, parsed_args): - obj = client.find_ip( - self.r, - ignore_missing=False, - ) - client.delete_ip(obj) - - def take_action_compute(self, client, parsed_args): - compute_v2.delete_floating_ip(client, self.r) + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 + + for fip in parsed_args.floating_ip: + try: + obj = client.find_ip(fip, ignore_missing=False) + client.delete_ip(obj) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete floating IP with " + "name or ID '%(fip)s': %(e)s" + ), + {'fip': fip, 'e': e}, + ) + if result > 0: + total = len(parsed_args.floating_ip) + msg = _( + "%(result)s of %(total)s floating IPs failed to delete." + ) % { + 'result': result, + 'total': total, + } + raise exceptions.CommandError(msg) -class ListFloatingIP(common.NetworkAndComputeLister): - # TODO(songminglong): Use SDK resource mapped attribute names once - # the OSC minimum requirements include SDK 1.0 +class ListFloatingIP(command.Lister): _description = _("List floating IP(s)") - def update_parser_network(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( '--network', metavar='', - help=self.enhance_help_neutron( - _( - "List floating IP(s) according to " - "given network (name or ID)" - ) + dest='networks', + action='append', + help=_( + "List only floating IP(s) with the specified network " + "(name or ID) " + "(repeat option to fiter on multiple networks)" ), ) parser.add_argument( '--port', metavar='', - help=self.enhance_help_neutron( - _("List floating IP(s) according to given port (name or ID)") + dest='ports', + action='append', + help=_( + "List only floating IP(s) with the specified port " + "(name or ID) " + "(repeat option to fiter on multiple ports)" ), ) parser.add_argument( '--fixed-ip-address', metavar='', - help=self.enhance_help_neutron( - _("List floating IP(s) according to given fixed IP address") + help=_( + "List only floating IP(s) with the specified fixed IP address" ), ) parser.add_argument( '--floating-ip-address', metavar='', - help=self.enhance_help_neutron( - _("List floating IP(s) according to given floating IP address") - ), - ) - parser.add_argument( - '--long', - action='store_true', - default=False, - help=self.enhance_help_neutron( - _("List additional fields in output") + help=_( + "List only floating IP(s) with the specified floating IP " + "address" ), ) parser.add_argument( '--status', metavar='', choices=['ACTIVE', 'DOWN'], - help=self.enhance_help_neutron( - _( - "List floating IP(s) according to given status ('ACTIVE', " - "'DOWN')" - ) + help=_( + "List only floating IP(s) with the specified status " + "('ACTIVE', 'DOWN')" ), ) parser.add_argument( '--project', metavar='', - help=self.enhance_help_neutron( - _( - "List floating IP(s) according to given project (name or " - "ID)" - ) + help=_( + "List only floating IP(s) with the specified project " + "(name or ID)" ), ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--router', metavar='', - help=self.enhance_help_neutron( - _("List floating IP(s) according to given router (name or ID)") + dest='routers', + action='append', + help=_( + "List only floating IP(s) with the specified router " + "(name or ID) " + "(repeat option to fiter on multiple routers)" ), ) - _tag.add_tag_filtering_option_to_parser( - parser, _('floating IP'), enhance_help=self.enhance_help_neutron + _tag.add_tag_filtering_option_to_parser(parser, _('floating IP')) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output"), ) - + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action_network(self, client, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: network_client = self.app.client_manager.network - identity_client = self.app.client_manager.identity columns: tuple[str, ...] = ( 'id', @@ -352,41 +354,63 @@ def take_action_network(self, client, parsed_args): 'DNS Domain', ) - query = {} + query: dict[str, object] = {} + + if parsed_args.networks is not None: + network_ids = [] + for network in parsed_args.networks: + network_id = network_client.find_network( + network, ignore_missing=False + ).id + network_ids.append(network_id) + query['floating_network_id'] = network_ids + + if parsed_args.ports is not None: + port_ids = [] + for port in parsed_args.ports: + port_id = network_client.find_port( + port, ignore_missing=False + ).id + port_ids.append(port_id) + query['port_id'] = port_ids - if parsed_args.network is not None: - network = network_client.find_network( - parsed_args.network, ignore_missing=False - ) - query['floating_network_id'] = network.id - if parsed_args.port is not None: - port = network_client.find_port( - parsed_args.port, ignore_missing=False - ) - query['port_id'] = port.id if parsed_args.fixed_ip_address is not None: query['fixed_ip_address'] = parsed_args.fixed_ip_address + if parsed_args.floating_ip_address is not None: query['floating_ip_address'] = parsed_args.floating_ip_address + if parsed_args.status: query['status'] = parsed_args.status + if parsed_args.project is not None: - project = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, ) - query['project_id'] = project.id - if parsed_args.router is not None: - router = network_client.find_router( - parsed_args.router, ignore_missing=False - ) - query['router_id'] = router.id + query['project_id'] = project_id + + if parsed_args.routers is not None: + router_ids = [] + for router in parsed_args.routers: + router_id = network_client.find_router( + router, ignore_missing=False + ).id + router_ids.append(router_id) + query['router_id'] = router_ids + if parsed_args.marker is not None: + query['marker'] = parsed_args.marker + if parsed_args.limit is not None: + query['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + query['max_items'] = parsed_args.max_items _tag.get_tag_filtering_args(parsed_args, query) try: - data = list(client.ips(**query)) + data = list(network_client.ips(**query)) except sdk_exceptions.NotFoundException: data = [] @@ -402,40 +426,11 @@ def take_action_network(self, client, parsed_args): ), ) - def take_action_compute(self, client, parsed_args): - columns: tuple[str, ...] = ( - 'ID', - 'IP', - 'Fixed IP', - 'Instance ID', - 'Pool', - ) - headers: tuple[str, ...] = ( - 'ID', - 'Floating IP Address', - 'Fixed IP Address', - 'Server', - 'Pool', - ) - - objs = compute_v2.list_floating_ips(client) - return ( - headers, - ( - utils.get_dict_properties( - s, - columns, - formatters={}, - ) - for s in objs - ), - ) - class SetFloatingIP(common.NeutronCommandWithExtraArgs): _description = _("Set floating IP Properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'floating_ip', @@ -476,13 +471,15 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network - attrs = {} obj = client.find_ip( parsed_args.floating_ip, ignore_missing=False, ) + + attrs: dict[str, Any] = {} + if parsed_args.port: port = client.find_port(parsed_args.port, ignore_missing=False) attrs['port_id'] = port.id @@ -512,10 +509,11 @@ def take_action(self, parsed_args): _tag.update_tags_for_set(client, obj, parsed_args) -class ShowFloatingIP(common.NetworkAndComputeShowOne): +class ShowFloatingIP(command.ShowOne): _description = _("Display floating IP details") - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'floating_ip', metavar="", @@ -523,7 +521,10 @@ def update_parser_common(self, parser): ) return parser - def take_action_network(self, client, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network obj = client.find_ip( parsed_args.floating_ip, ignore_missing=False, @@ -532,17 +533,11 @@ def take_action_network(self, client, parsed_args): data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) - def take_action_compute(self, client, parsed_args): - obj = compute_v2.get_floating_ip(client, parsed_args.floating_ip) - columns = _get_columns(obj) - data = utils.get_dict_properties(obj, columns) - return (columns, data) - class UnsetFloatingIP(common.NeutronCommandWithExtraArgs): _description = _("Unset floating IP Properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'floating_ip', @@ -565,7 +560,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_ip( parsed_args.floating_ip, diff --git a/openstackclient/network/v2/floating_ip_pool.py b/openstackclient/network/v2/floating_ip_pool.py deleted file mode 100644 index 2030eb8ad6..0000000000 --- a/openstackclient/network/v2/floating_ip_pool.py +++ /dev/null @@ -1,38 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Floating IP Pool action implementations""" - -from osc_lib import exceptions - -from openstackclient.api import compute_v2 -from openstackclient.i18n import _ -from openstackclient.network import common - - -class ListFloatingIPPool(common.NetworkAndComputeLister): - _description = _("List pools of floating IP addresses") - - def take_action_network(self, client, parsed_args): - msg = _( - "Floating ip pool operations are only available for " - "Compute v2 network." - ) - raise exceptions.CommandError(msg) - - def take_action_compute(self, client, parsed_args): - columns = ('Name',) - data = [ - (x['name'],) for x in compute_v2.list_floating_ip_pools(client) - ] - - return (columns, data) diff --git a/openstackclient/network/v2/floating_ip_port_forwarding.py b/openstackclient/network/v2/floating_ip_port_forwarding.py index 3810deddf8..185ed31e52 100644 --- a/openstackclient/network/v2/floating_ip_port_forwarding.py +++ b/openstackclient/network/v2/floating_ip_port_forwarding.py @@ -13,20 +13,24 @@ """Floating IP Port Forwarding action implementations""" +import argparse import logging -import typing as ty +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import port_forwarding as _port_forwarding from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.network import common LOG = logging.getLogger(__name__) -def validate_ports_diff(ports): +def validate_ports_diff(ports: list[int]) -> int: if len(ports) == 0: return 0 @@ -40,7 +44,9 @@ def validate_ports_diff(ports): return ports_diff -def validate_ports_match(internal_ports, external_ports): +def validate_ports_match( + internal_ports: list[int], external_ports: list[int] +) -> None: internal_ports_diff = validate_ports_diff(internal_ports) external_ports_diff = validate_ports_diff(external_ports) @@ -52,7 +58,9 @@ def validate_ports_match(internal_ports, external_ports): raise exceptions.CommandError(msg) -def validate_and_assign_port_ranges(parsed_args, attrs): +def validate_and_assign_port_ranges( + parsed_args: argparse.Namespace, attrs: dict[str, Any] +) -> None: internal_port_range = parsed_args.internal_protocol_port external_port_range = parsed_args.external_protocol_port external_ports = internal_ports = [] @@ -79,13 +87,15 @@ def validate_and_assign_port_ranges(parsed_args, attrs): attrs['external_port'] = int(external_port_range) -def validate_port(port): +def validate_port(port: int) -> None: if port <= 0 or port > 65535: msg = _("The port number range is <1-65535>") raise exceptions.CommandError(msg) -def _get_columns(item): +def _get_columns( + item: _port_forwarding.PortForwarding, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns @@ -97,7 +107,7 @@ class CreateFloatingIPPortForwarding( ): _description = _("Create floating IP port forwarding") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--internal-ip-address', @@ -164,8 +174,10 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): - attrs: dict[str, ty.Any] = {} + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + attrs: dict[str, Any] = {} client = self.app.client_manager.network floating_ip = client.find_ip( parsed_args.floating_ip, @@ -198,7 +210,7 @@ def take_action(self, parsed_args): class DeleteFloatingIPPortForwarding(command.Command): _description = _("Delete floating IP port forwarding") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'floating_ip', @@ -216,7 +228,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network floating_ip = client.find_ip( parsed_args.floating_ip, @@ -251,7 +263,7 @@ def take_action(self, parsed_args): class ListFloatingIPPortForwarding(command.Lister): _description = _("List floating IP port forwarding") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'floating_ip', @@ -265,8 +277,8 @@ def get_parser(self, prog_name): '--port', metavar='', help=_( - "Filter the list result by the ID or name of " - "the internal network port" + "List only floating IP port forwardings with the " + "specified internal network port (name or ID)" ), ) parser.add_argument( @@ -274,19 +286,24 @@ def get_parser(self, prog_name): metavar='', dest='external_protocol_port', help=_( - "Filter the list result by the " - "protocol port number of the floating IP" + "List only floating IP port forwardings with the " + "specified external protocol port number" ), ) parser.add_argument( '--protocol', metavar='', - help=_("Filter the list result by the port protocol"), + help=_( + "List only floating IP port forwardings with the " + "specified protocol number" + ), ) - + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( @@ -312,7 +329,7 @@ def take_action(self, parsed_args): 'Description', ) - query = {} + query: dict[str, Any] = {} if parsed_args.port: port = client.find_port(parsed_args.port, ignore_missing=False) @@ -327,6 +344,12 @@ def take_action(self, parsed_args): ) if parsed_args.protocol is not None: query['protocol'] = parsed_args.protocol + if parsed_args.marker is not None: + query['marker'] = parsed_args.marker + if parsed_args.limit is not None: + query['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + query['max_items'] = parsed_args.max_items obj = client.find_ip( parsed_args.floating_ip, @@ -351,7 +374,7 @@ def take_action(self, parsed_args): class SetFloatingIPPortForwarding(common.NeutronCommandWithExtraArgs): _description = _("Set floating IP Port Forwarding Properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'floating_ip', @@ -416,7 +439,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network floating_ip = client.find_ip( parsed_args.floating_ip, @@ -451,7 +474,7 @@ def take_action(self, parsed_args): class ShowFloatingIPPortForwarding(command.ShowOne): _description = _("Display floating IP Port Forwarding details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'floating_ip', @@ -468,7 +491,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network floating_ip = client.find_ip( parsed_args.floating_ip, diff --git a/openstackclient/network/v2/fwaas/__init__.py b/openstackclient/network/v2/fwaas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/network/v2/fwaas/group.py b/openstackclient/network/v2/fwaas/group.py new file mode 100644 index 0000000000..c948da4bfb --- /dev/null +++ b/openstackclient/network/v2/fwaas/group.py @@ -0,0 +1,502 @@ +# Copyright 2016-2017 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +import logging +from typing import Any + +from cliff import columns as cliff_columns +from osc_lib.cli import identity as identity_utils +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +LOG = logging.getLogger(__name__) + + +class AdminStateColumn(cliff_columns.FormattableColumn[bool]): + def human_readable(self) -> str: + return 'UP' if self._value else 'DOWN' + + +_formatters = { + 'admin_state_up': AdminStateColumn, +} + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('ingress_firewall_policy_id', 'Ingress Policy ID', column_util.LIST_BOTH), + ('egress_firewall_policy_id', 'Egress Policy ID', column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('status', 'Status', column_util.LIST_LONG_ONLY), + ('ports', 'Ports', column_util.LIST_LONG_ONLY), + ('admin_state_up', 'State', column_util.LIST_LONG_ONLY), + ('shared', 'Shared', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), +) + +_attr_map_dict = {x[0]: x[1] for x in _attr_map} + + +def _get_common_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: + parser.add_argument( + '--description', + metavar='', + help=_('Description of the firewall group'), + ) + ingress_group = parser.add_mutually_exclusive_group() + ingress_group.add_argument( + '--ingress-firewall-policy', + metavar='', + dest='ingress_firewall_policy', + help=_('Ingress firewall policy (name or ID)'), + ) + ingress_group.add_argument( + '--no-ingress-firewall-policy', + dest='no_ingress_firewall_policy', + action='store_true', + help=_('Detach ingress firewall policy from the firewall group'), + ) + egress_group = parser.add_mutually_exclusive_group() + egress_group.add_argument( + '--egress-firewall-policy', + metavar='', + dest='egress_firewall_policy', + help=_('Egress firewall policy (name or ID)'), + ) + egress_group.add_argument( + '--no-egress-firewall-policy', + dest='no_egress_firewall_policy', + action='store_true', + help=_('Detach egress firewall policy from the firewall group'), + ) + shared_group = parser.add_mutually_exclusive_group() + shared_group.add_argument( + '--share', + action='store_true', + default=None, + dest='shared', + help=_( + 'Share the firewall group to be used in all projects ' + '(by default, it is restricted to be used by the ' + 'current project).' + ), + ) + shared_group.add_argument( + '--no-share', + action='store_false', + dest='shared', + help=_('Restrict use of the firewall group to the current project'), + ) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', + action='store_true', + default=None, + dest='admin_state_up', + help=_('Enable firewall group'), + ) + admin_group.add_argument( + '--disable', + action='store_false', + dest='admin_state_up', + help=_('Disable firewall group'), + ) + return parser + + +def _get_common_attrs( + client_manager: Any, + parsed_args: argparse.Namespace, + is_create: bool = True, +) -> dict[str, Any]: + attrs: dict[str, Any] = {} + client = client_manager.network + + if parsed_args.ingress_firewall_policy: + attrs['ingress_firewall_policy_id'] = client.find_firewall_policy( + parsed_args.ingress_firewall_policy, ignore_missing=False + ).id + elif parsed_args.no_ingress_firewall_policy: + attrs['ingress_firewall_policy_id'] = None + + if parsed_args.egress_firewall_policy: + attrs['egress_firewall_policy_id'] = client.find_firewall_policy( + parsed_args.egress_firewall_policy, ignore_missing=False + ).id + elif parsed_args.no_egress_firewall_policy: + attrs['egress_firewall_policy_id'] = None + + if parsed_args.shared is not None: + attrs['shared'] = parsed_args.shared + if parsed_args.admin_state_up is not None: + attrs['admin_state_up'] = parsed_args.admin_state_up + if parsed_args.name: + attrs['name'] = parsed_args.name + if parsed_args.description: + attrs['description'] = parsed_args.description + if parsed_args.port and parsed_args.no_port: + attrs['ports'] = sorted( + [ + client.find_port(p, ignore_missing=False).id + for p in set(parsed_args.port) + ] + ) + elif parsed_args.port: + ports = [] + for p in set(parsed_args.port): + ports.append(client.find_port(p, ignore_missing=False).id) + if not is_create: + ports += client.find_firewall_group( + parsed_args.firewall_group, ignore_missing=False + ).ports + attrs['ports'] = sorted(set(ports)) + elif parsed_args.no_port: + attrs['ports'] = [] + return attrs + + +class CreateFirewallGroup(command.ShowOne): + _description = _("Create a new firewall group") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + # TODO(slaweq): Remove the deprecated --name option and make the + # positional name argument required (remove nargs='?') once the + # deprecation period is over. + parser.add_argument( + 'positional_name', + nargs='?', + metavar='', + default=None, + help=_('Name for the firewall group'), + ) + parser.add_argument( + '--name', + help=_( + '(Deprecated, please pass name as a positional argument) ' + 'Name for the firewall group' + ), + ) + identity_utils.add_project_owner_option_to_parser(parser) + port_group = parser.add_mutually_exclusive_group() + port_group.add_argument( + '--port', + metavar='', + action='append', + help=_( + 'Port(s) (name or ID) to apply firewall group. This ' + 'option can be repeated' + ), + ) + port_group.add_argument( + '--no-port', + dest='no_port', + action='store_true', + help=_('Detach all port from the firewall group'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + # TODO(slaweq): Remove this --name deprecation handling once the + # deprecation period is over. + if parsed_args.positional_name and parsed_args.name: + msg = _( + "Cannot specify name as both a positional argument " + "and with the --name option." + ) + raise exceptions.CommandError(msg) + if parsed_args.name: + LOG.warning( + 'The --name option is deprecated for the "firewall group ' + 'create" command, please pass the name as a positional ' + 'argument instead.' + ) + elif parsed_args.positional_name: + parsed_args.name = parsed_args.positional_name + + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + obj = client.create_firewall_group(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns, formatters=_formatters) + return (display_columns, data) + + +class DeleteFirewallGroup(command.Command): + _description = _("Delete firewall group(s)") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'firewall_group', + metavar='', + nargs='+', + help=_('Firewall group(s) to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 + for fwg in parsed_args.firewall_group: + try: + fwg = client.find_firewall_group(fwg, ignore_missing=False) + client.delete_firewall_group(fwg.id) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete firewall group with " + "name or ID '%(firewall_group)s': %(e)s" + ), + {'firewall_group': fwg, 'e': e}, + ) + + if result > 0: + total = len(parsed_args.firewall_group) + msg = _( + "%(result)s of %(total)s firewall group(s) failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListFirewallGroup(command.Lister): + _description = _("List firewall groups") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + obj = client.firewall_groups() + headers, columns = column_util.get_column_definitions( + list(_attr_map), long_listing=parsed_args.long + ) + return ( + headers, + ( + utils.get_dict_properties(s, columns, formatters=_formatters) + for s in obj + ), + ) + + +class SetFirewallGroup(command.Command): + _description = _("Set firewall group properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + 'firewall_group', + metavar='', + help=_('Firewall group to update (name or ID)'), + ) + parser.add_argument('--name', help=_('Name for the firewall group')) + parser.add_argument( + '--port', + metavar='', + action='append', + help=_( + 'Port(s) (name or ID) to apply firewall group. This ' + 'option can be repeated' + ), + ) + parser.add_argument( + '--no-port', + dest='no_port', + action='store_true', + help=_('Detach all port from the firewall group'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + fwg_id = client.find_firewall_group( + parsed_args.firewall_group, ignore_missing=False + ).id + attrs = _get_common_attrs( + self.app.client_manager, parsed_args, is_create=False + ) + try: + client.update_firewall_group(fwg_id, **attrs) + except Exception as e: + msg = _("Failed to set firewall group '%(group)s': %(e)s") % { + 'group': parsed_args.firewall_group, + 'e': e, + } + raise exceptions.CommandError(msg) + + +class ShowFirewallGroup(command.ShowOne): + _description = _("Display firewall group details") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'firewall_group', + metavar='', + help=_('Firewall group to show (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + fwg_id = client.find_firewall_group( + parsed_args.firewall_group, ignore_missing=False + ).id + obj = client.get_firewall_group(fwg_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns, formatters=_formatters) + return (display_columns, data) + + +class UnsetFirewallGroup(command.Command): + _description = _("Unset firewall group properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'firewall_group', + metavar='', + help=_('Firewall group to unset (name or ID)'), + ) + port_group = parser.add_mutually_exclusive_group() + port_group.add_argument( + '--port', + metavar='', + action='append', + help=_( + 'Port(s) (name or ID) to apply firewall group. This ' + 'option can be repeated' + ), + ) + port_group.add_argument( + '--all-port', + action='store_true', + help=_('Remove all ports for this firewall group'), + ) + parser.add_argument( + '--ingress-firewall-policy', + action='store_true', + help=_('Ingress firewall policy (name or ID) to delete'), + ) + parser.add_argument( + '--egress-firewall-policy', + action='store_true', + dest='egress_firewall_policy', + help=_('Egress firewall policy (name or ID) to delete'), + ) + shared_group = parser.add_mutually_exclusive_group() + shared_group.add_argument( + '--share', + action='store_true', + help=_( + '(Deprecated) Use "firewall group set --no-share" instead. ' + 'Restrict use of the firewall group to the current project' + ), + ) + parser.add_argument( + '--enable', + action='store_true', + help=_( + '(Deprecated) Use "firewall group set --disable" instead. ' + 'Disable firewall group' + ), + ) + return parser + + def _get_attrs( + self, client: Any, parsed_args: argparse.Namespace + ) -> dict[str, Any]: + attrs: dict[str, Any] = {} + if parsed_args.ingress_firewall_policy: + attrs['ingress_firewall_policy_id'] = None + if parsed_args.egress_firewall_policy: + attrs['egress_firewall_policy_id'] = None + if parsed_args.share: + LOG.warning( + 'The --share option is deprecated, please use ' + '"firewall group set --no-share" instead.' + ) + attrs['shared'] = False + if parsed_args.enable: + LOG.warning( + 'The --enable option is deprecated, please use ' + '"firewall group set --disable" instead.' + ) + attrs['admin_state_up'] = False + if parsed_args.port: + old = client.find_firewall_group( + parsed_args.firewall_group, ignore_missing=False + ).ports + new = [ + client.find_port(r, ignore_missing=False).id + for r in parsed_args.port + ] + attrs['ports'] = sorted(list(set(old) - set(new))) + if parsed_args.all_port: + attrs['ports'] = [] + return attrs + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + fwg_id = client.find_firewall_group( + parsed_args.firewall_group, ignore_missing=False + ).id + attrs = self._get_attrs(client, parsed_args) + try: + client.update_firewall_group(fwg_id, **attrs) + except Exception as e: + msg = _("Failed to unset firewall group '%(group)s': %(e)s") % { + 'group': parsed_args.firewall_group, + 'e': e, + } + raise exceptions.CommandError(msg) diff --git a/openstackclient/network/v2/fwaas/policy.py b/openstackclient/network/v2/fwaas/policy.py new file mode 100644 index 0000000000..6022bbcce5 --- /dev/null +++ b/openstackclient/network/v2/fwaas/policy.py @@ -0,0 +1,520 @@ +# Copyright 2016-2017 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +import logging +from typing import Any, cast + +from osc_lib.cli import identity as identity_utils +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +LOG = logging.getLogger(__name__) + + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('firewall_rules', 'Firewall Rules', column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('audited', 'Audited', column_util.LIST_LONG_ONLY), + ('shared', 'Shared', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), +) + +_attr_map_dict = {x[0]: x[1] for x in _attr_map} + + +def _get_common_attrs( + client_manager: Any, + parsed_args: argparse.Namespace, + is_create: bool = True, +) -> dict[str, Any]: + attrs: dict[str, Any] = {} + client = client_manager.network + + if parsed_args.firewall_rule and parsed_args.no_firewall_rule: + _firewall_rules = [] + for f in parsed_args.firewall_rule: + _firewall_rules.append( + client.find_firewall_rule(f, ignore_missing=False).id + ) + attrs['firewall_rules'] = _firewall_rules + elif parsed_args.firewall_rule: + rules = [] + if not is_create: + fwp = client.find_firewall_policy( + parsed_args.firewall_policy, ignore_missing=False + ) + rules += fwp.firewall_rules + for f in parsed_args.firewall_rule: + rules.append(client.find_firewall_rule(f, ignore_missing=False).id) + attrs['firewall_rules'] = rules + elif parsed_args.no_firewall_rule: + attrs['firewall_rules'] = [] + + if parsed_args.audited is not None: + attrs['audited'] = parsed_args.audited + if parsed_args.name: + attrs['name'] = parsed_args.name + if parsed_args.description: + attrs['description'] = parsed_args.description + if parsed_args.shared is not None: + attrs['shared'] = parsed_args.shared + return attrs + + +def _get_common_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: + parser.add_argument( + '--description', help=_('Description of the firewall policy') + ) + audited_group = parser.add_mutually_exclusive_group() + audited_group.add_argument( + '--audited', + default=None, + action='store_true', + dest='audited', + help=_('Enable auditing for the policy'), + ) + audited_group.add_argument( + '--no-audited', + action='store_false', + dest='audited', + help=_('Disable auditing for the policy'), + ) + shared_group = parser.add_mutually_exclusive_group() + shared_group.add_argument( + '--share', + action='store_true', + default=None, + dest='shared', + help=_( + 'Share the firewall policy to be used in all projects ' + '(by default, it is restricted to be used by the ' + 'current project).' + ), + ) + shared_group.add_argument( + '--no-share', + action='store_false', + dest='shared', + help=_('Restrict use of the firewall policy to the current project'), + ) + return parser + + +class CreateFirewallPolicy(command.ShowOne): + _description = _("Create a new firewall policy") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + identity_utils.add_project_owner_option_to_parser(parser) + parser.add_argument( + 'name', metavar='', help=_('Name for the firewall policy') + ) + fwr_group = parser.add_mutually_exclusive_group() + fwr_group.add_argument( + '--firewall-rule', + action='append', + metavar='', + help=_('Firewall rule(s) to apply (name or ID)'), + ) + fwr_group.add_argument( + '--no-firewall-rule', + action='store_true', + help=_('Unset all firewall rules from firewall policy'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + obj = client.create_firewall_policy(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns, formatters={}) + return (display_columns, data) + + +class DeleteFirewallPolicy(command.Command): + _description = _("Delete firewall policy(s)") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'firewall_policy', + metavar='', + nargs='+', + help=_('Firewall policy(s) to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 + for fwp in parsed_args.firewall_policy: + try: + fwp_id = client.find_firewall_policy( + fwp, ignore_missing=False + ).id + client.delete_firewall_policy(fwp_id) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete Firewall policy with " + "name or ID '%(firewall_policy)s': %(e)s" + ), + {'firewall_policy': fwp, 'e': e}, + ) + + if result > 0: + total = len(parsed_args.firewall_policy) + msg = _( + "%(result)s of %(total)s firewall policy(s) failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class FirewallPolicyInsertRule(command.Command): + _description = _("Insert a rule into a given firewall policy") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'firewall_policy', + metavar='', + help=_('Firewall policy to insert rule (name or ID)'), + ) + parser.add_argument( + '--insert-before', + metavar='', + help=_( + 'Insert the new rule before this existing rule (name or ID)' + ), + ) + parser.add_argument( + '--insert-after', + metavar='', + help=_( + 'Insert the new rule after this existing rule (name or ID)' + ), + ) + parser.add_argument( + 'firewall_rule', + metavar='', + help=_('Firewall rule to be inserted (name or ID)'), + ) + return parser + + def args2body(self, parsed_args: argparse.Namespace) -> dict[str, str]: + client = self.app.client_manager.network + _rule_id = _get_required_firewall_rule(client, parsed_args) + _insert_before = '' + if 'insert_before' in parsed_args: + if parsed_args.insert_before: + _insert_before = client.find_firewall_rule( + parsed_args.insert_before, ignore_missing=False + ).id + _insert_after = '' + if 'insert_after' in parsed_args: + if parsed_args.insert_after: + _insert_after = client.find_firewall_rule( + parsed_args.insert_after, ignore_missing=False + ).id + return { + 'firewall_rule_id': _rule_id, + 'insert_before': _insert_before, + 'insert_after': _insert_after, + } + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + policy_id = client.find_firewall_policy( + parsed_args.firewall_policy, ignore_missing=False + ).id + body = self.args2body(parsed_args) + client.insert_rule_into_policy(policy_id, **body) + rule_id = body['firewall_rule_id'] + policy = parsed_args.firewall_policy + print( + ( + _( + 'Inserted firewall rule %(rule)s in firewall policy ' + '%(policy)s' + ) + % {'rule': rule_id, 'policy': policy} + ), + file=self.app.stdout, + ) + + +class FirewallPolicyRemoveRule(command.Command): + _description = _("Remove a rule from a given firewall policy") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'firewall_policy', + metavar='', + help=_('Firewall policy to remove rule (name or ID)'), + ) + parser.add_argument( + 'firewall_rule', + metavar='', + help=_('Firewall rule to remove from policy (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + policy_id = client.find_firewall_policy( + parsed_args.firewall_policy, ignore_missing=False + ).id + fwr_id = _get_required_firewall_rule(client, parsed_args) + body = {'firewall_rule_id': fwr_id} + client.remove_rule_from_policy(policy_id, **body) + rule_id = body['firewall_rule_id'] + policy = parsed_args.firewall_policy + print( + ( + _( + 'Removed firewall rule %(rule)s from firewall policy ' + '%(policy)s' + ) + % {'rule': rule_id, 'policy': policy} + ), + file=self.app.stdout, + ) + + +class ListFirewallPolicy(command.Lister): + _description = _("List firewall policies") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + obj = client.firewall_policies() + headers, columns = column_util.get_column_definitions( + list(_attr_map), long_listing=parsed_args.long + ) + return ( + headers, + ( + utils.get_dict_properties(s, columns, formatters={}) + for s in obj + ), + ) + + +class SetFirewallPolicy(command.Command): + _description = _("Set firewall policy properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + 'firewall_policy', + metavar='', + help=_('Firewall policy to update (name or ID)'), + ) + parser.add_argument( + '--name', metavar='', help=_('Name for the firewall policy') + ) + parser.add_argument( + '--firewall-rule', + action='append', + metavar='', + help=_('Firewall rule(s) to apply (name or ID)'), + ) + parser.add_argument( + '--no-firewall-rule', + action='store_true', + help=_('Remove all firewall rules from firewall policy'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + fwp_id = client.find_firewall_policy( + parsed_args.firewall_policy, ignore_missing=False + ).id + attrs = _get_common_attrs( + self.app.client_manager, parsed_args, is_create=False + ) + try: + client.update_firewall_policy(fwp_id, **attrs) + except Exception as e: + msg = _("Failed to set firewall policy '%(policy)s': %(e)s") % { + 'policy': parsed_args.firewall_policy, + 'e': e, + } + raise exceptions.CommandError(msg) + + +class ShowFirewallPolicy(command.ShowOne): + _description = _("Display firewall policy details") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'firewall_policy', + metavar='', + help=_('Firewall policy to show (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + fwp_id = client.find_firewall_policy( + parsed_args.firewall_policy, ignore_missing=False + ).id + obj = client.get_firewall_policy(fwp_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns, formatters={}) + return (display_columns, data) + + +def _get_required_firewall_rule( + client: Any, parsed_args: argparse.Namespace +) -> str: + if not parsed_args.firewall_rule: + msg = _("Firewall rule (name or ID) is required.") + raise exceptions.CommandError(msg) + return cast( + str, + client.find_firewall_rule( + parsed_args.firewall_rule, ignore_missing=False + ).id, + ) + + +class UnsetFirewallPolicy(command.Command): + _description = _("Unset firewall policy properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'firewall_policy', + metavar='', + help=_('Firewall policy to unset (name or ID)'), + ) + firewall_rule_group = parser.add_mutually_exclusive_group() + firewall_rule_group.add_argument( + '--firewall-rule', + action='append', + metavar='', + help=_( + 'Remove firewall rule(s) from the firewall policy (name or ID)' + ), + ) + firewall_rule_group.add_argument( + '--all-firewall-rule', + action='store_true', + help=_('Remove all firewall rules from the firewall policy'), + ) + parser.add_argument( + '--audited', + action='store_true', + help=_('Disable auditing for the policy'), + ) + parser.add_argument( + '--share', + action='store_true', + help=_( + '(Deprecated) Use "firewall policy set --no-share" instead. ' + 'Restrict use of the firewall policy to the current project' + ), + ) + return parser + + def _get_attrs( + self, client_manager: Any, parsed_args: argparse.Namespace + ) -> dict[str, Any]: + attrs: dict[str, Any] = {} + client = client_manager.network + + if parsed_args.firewall_rule: + current = client.find_firewall_policy( + parsed_args.firewall_policy, ignore_missing=False + ).firewall_rules + removed = [] + for f in set(parsed_args.firewall_rule): + removed.append( + client.find_firewall_rule(f, ignore_missing=False).id + ) + attrs['firewall_rules'] = [r for r in current if r not in removed] + if parsed_args.all_firewall_rule: + attrs['firewall_rules'] = [] + if parsed_args.audited: + attrs['audited'] = False + if parsed_args.share: + LOG.warning( + 'The --share option is deprecated, please use ' + '"firewall policy set --no-share" instead.' + ) + attrs['shared'] = False + return attrs + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + fwp_id = client.find_firewall_policy( + parsed_args.firewall_policy, ignore_missing=False + ).id + attrs = self._get_attrs(self.app.client_manager, parsed_args) + try: + client.update_firewall_policy(fwp_id, **attrs) + except Exception as e: + msg = _("Failed to unset firewall policy '%(policy)s': %(e)s") % { + 'policy': parsed_args.firewall_policy, + 'e': e, + } + raise exceptions.CommandError(msg) diff --git a/openstackclient/network/v2/fwaas/rule.py b/openstackclient/network/v2/fwaas/rule.py new file mode 100644 index 0000000000..f38b6c837f --- /dev/null +++ b/openstackclient/network/v2/fwaas/rule.py @@ -0,0 +1,612 @@ +# Copyright 2016-2017 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +import logging +from typing import Any + +from cliff import columns as cliff_columns +from osc_lib.cli import identity as identity_utils +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +LOG = logging.getLogger(__name__) + + +_attr_map = ( + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('enabled', 'Enabled', column_util.LIST_BOTH), + ('summary', 'Summary', column_util.LIST_SHORT_ONLY), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('firewall_policy_id', 'Firewall Policy', column_util.LIST_BOTH), + ('ip_version', 'IP Version', column_util.LIST_LONG_ONLY), + ('action', 'Action', column_util.LIST_LONG_ONLY), + ('protocol', 'Protocol', column_util.LIST_LONG_ONLY), + ('source_ip_address', 'Source IP Address', column_util.LIST_LONG_ONLY), + ('source_port', 'Source Port', column_util.LIST_LONG_ONLY), + ( + 'destination_ip_address', + 'Destination IP Address', + column_util.LIST_LONG_ONLY, + ), + ('destination_port', 'Destination Port', column_util.LIST_LONG_ONLY), + ('shared', 'Shared', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ( + 'source_firewall_group_id', + 'Source Firewall Group ID', + column_util.LIST_LONG_ONLY, + ), + ( + 'destination_firewall_group_id', + 'Destination Firewall Group ID', + column_util.LIST_LONG_ONLY, + ), +) + +_attr_map_dict = {x[0]: x[1] for x in _attr_map} + + +def _convert_to_lowercase(string: str) -> str: + return string.lower() + + +def _get_common_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: + parser.add_argument( + '--description', + metavar='', + help=_('Description of the firewall rule'), + ) + parser.add_argument( + '--protocol', + type=_convert_to_lowercase, + help=_( + 'IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, ' + 'ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, ipv6-opts, ' + 'ipv6-route, ospf, pgm, rsvp, sctp, tcp, udp, udplite, ' + 'vrrp and integer representations [0-255] or any; ' + 'default: any (all protocols))' + ), + ) + parser.add_argument( + '--action', + choices=['allow', 'deny', 'reject'], + type=_convert_to_lowercase, + help=_('Action for the firewall rule'), + ) + parser.add_argument( + '--ip-version', + metavar='', + choices=['4', '6'], + help=_('Set IP version 4 or 6 (default is 4)'), + ) + src_ip_group = parser.add_mutually_exclusive_group() + src_ip_group.add_argument( + '--source-ip-address', + metavar='', + help=_('Source IP address or subnet'), + ) + src_ip_group.add_argument( + '--no-source-ip-address', + action='store_true', + help=_('Detach source IP address'), + ) + dst_ip_group = parser.add_mutually_exclusive_group() + dst_ip_group.add_argument( + '--destination-ip-address', + metavar='', + help=_('Destination IP address or subnet'), + ) + dst_ip_group.add_argument( + '--no-destination-ip-address', + action='store_true', + help=_('Detach destination IP address'), + ) + src_port_group = parser.add_mutually_exclusive_group() + src_port_group.add_argument( + '--source-port', + metavar='', + help=_( + 'Source port number or range ' + '(integer in [1, 65535] or range like 123:456)' + ), + ) + src_port_group.add_argument( + '--no-source-port', + action='store_true', + help=_('Detach source port number or range'), + ) + dst_port_group = parser.add_mutually_exclusive_group() + dst_port_group.add_argument( + '--destination-port', + metavar='', + help=_( + 'Destination port number or range' + '(integer in [1, 65535] or range like 123:456)' + ), + ) + dst_port_group.add_argument( + '--no-destination-port', + action='store_true', + help=_('Detach destination port number or range'), + ) + shared_group = parser.add_mutually_exclusive_group() + shared_group.add_argument( + '--share', + action='store_true', + default=None, + dest='shared', + help=_( + 'Share the firewall rule to be used in all projects ' + '(by default, it is restricted to be used by the ' + 'current project).' + ), + ) + shared_group.add_argument( + '--no-share', + action='store_false', + dest='shared', + help=_('Restrict use of the firewall rule to the current project'), + ) + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--enable-rule', + action='store_true', + default=None, + dest='enabled', + help=_('Enable this rule (default is enabled)'), + ) + enable_group.add_argument( + '--disable-rule', + action='store_false', + dest='enabled', + help=_('Disable this rule'), + ) + src_fwg_group = parser.add_mutually_exclusive_group() + src_fwg_group.add_argument( + '--source-firewall-group', + metavar='', + help=_('Source firewall group (name or ID)'), + ) + src_fwg_group.add_argument( + '--no-source-firewall-group', + action='store_true', + help=_('No associated source firewall group'), + ) + dst_fwg_group = parser.add_mutually_exclusive_group() + dst_fwg_group.add_argument( + '--destination-firewall-group', + metavar='', + help=_('Destination firewall group (name or ID)'), + ) + dst_fwg_group.add_argument( + '--no-destination-firewall-group', + action='store_true', + help=_('No associated destination firewall group'), + ) + return parser + + +def _get_common_attrs( + client_manager: Any, + parsed_args: argparse.Namespace, + is_create: bool = True, +) -> dict[str, Any]: + attrs: dict[str, Any] = {} + client = client_manager.network + if parsed_args.name: + attrs['name'] = parsed_args.name + if parsed_args.description: + attrs['description'] = parsed_args.description + if parsed_args.protocol: + protocol = parsed_args.protocol + attrs['protocol'] = None if protocol == 'any' else protocol + if parsed_args.action: + attrs['action'] = parsed_args.action + if parsed_args.ip_version: + attrs['ip_version'] = str(parsed_args.ip_version) + + if parsed_args.source_port: + attrs['source_port'] = parsed_args.source_port + if parsed_args.no_source_port: + attrs['source_port'] = None + + if parsed_args.source_ip_address: + attrs['source_ip_address'] = parsed_args.source_ip_address + if parsed_args.no_source_ip_address: + attrs['source_ip_address'] = None + + if parsed_args.destination_port: + attrs['destination_port'] = parsed_args.destination_port + if parsed_args.no_destination_port: + attrs['destination_port'] = None + + if parsed_args.destination_ip_address: + attrs['destination_ip_address'] = parsed_args.destination_ip_address + if parsed_args.no_destination_ip_address: + attrs['destination_ip_address'] = None + + if parsed_args.enabled is not None: + attrs['enabled'] = parsed_args.enabled + + if parsed_args.shared is not None: + attrs['shared'] = parsed_args.shared + + if parsed_args.source_firewall_group: + attrs['source_firewall_group_id'] = client.find_firewall_group( + parsed_args.source_firewall_group, ignore_missing=False + ).id + if parsed_args.no_source_firewall_group: + attrs['source_firewall_group_id'] = None + + if parsed_args.destination_firewall_group: + attrs['destination_firewall_group_id'] = client.find_firewall_group( + parsed_args.destination_firewall_group, ignore_missing=False + ).id + if parsed_args.no_destination_firewall_group: + attrs['destination_firewall_group_id'] = None + + return attrs + + +class ProtocolColumn(cliff_columns.FormattableColumn[str | None]): + def human_readable(self) -> str: + return self._value if self._value else 'any' + + +_formatters = {'protocol': ProtocolColumn} + + +class CreateFirewallRule(command.ShowOne): + _description = _("Create a new firewall rule") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + # TODO(slaweq): Remove the deprecated --name option and make the + # positional name argument required (remove nargs='?') once the + # deprecation period is over. + parser.add_argument( + 'positional_name', + nargs='?', + metavar='', + default=None, + help=_('Name of the firewall rule'), + ) + parser.add_argument( + '--name', + metavar='', + help=_( + '(Deprecated, please pass name as a positional argument) ' + 'Name of the firewall rule' + ), + ) + identity_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + # TODO(slaweq): Remove this --name deprecation handling once the + # deprecation period is over. + if parsed_args.positional_name and parsed_args.name: + msg = _( + "Cannot specify name as both a positional argument " + "and with the --name option." + ) + raise exceptions.CommandError(msg) + if parsed_args.name: + LOG.warning( + 'The --name option is deprecated for the "firewall group ' + 'rule create" command, please pass the name as a positional ' + 'argument instead.' + ) + elif parsed_args.positional_name: + parsed_args.name = parsed_args.positional_name + attrs = _get_common_attrs(self.app.client_manager, parsed_args) + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + obj = client.create_firewall_rule(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns, formatters=_formatters) + return display_columns, data + + +class DeleteFirewallRule(command.Command): + _description = _("Delete firewall rule(s)") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'firewall_rule', + metavar='', + nargs='+', + help=_('Firewall rule(s) to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 + for fwr in parsed_args.firewall_rule: + try: + fwr = client.find_firewall_rule(fwr, ignore_missing=False) + client.delete_firewall_rule(fwr.id) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete Firewall rule with " + "name or ID '%(firewall_rule)s': %(e)s" + ), + {'firewall_rule': fwr, 'e': e}, + ) + + if result > 0: + total = len(parsed_args.firewall_rule) + msg = _( + "%(result)s of %(total)s firewall rule(s) failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListFirewallRule(command.Lister): + _description = _("List firewall rules that belong to a given tenant") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output"), + ) + return parser + + def extend_list( + self, data: Iterable[Any], parsed_args: argparse.Namespace + ) -> list[Any]: + ext_data = [] + for d in data: + protocol = d['protocol'].upper() if d['protocol'] else 'ANY' + src_ip = 'none specified' + dst_ip = 'none specified' + src_port = '(none specified)' + dst_port = '(none specified)' + if d.get('source_ip_address'): + src_ip = str(d['source_ip_address']).lower() + if d.get('source_port'): + src_port = '(' + str(d['source_port']).lower() + ')' + if d.get('destination_ip_address'): + dst_ip = str(d['destination_ip_address']).lower() + if d.get('destination_port'): + dst_port = '(' + str(d['destination_port']).lower() + ')' + action = d['action'] if d.get('action') else 'no-action' + src = 'source(port): ' + src_ip + src_port + dst = 'dest(port): ' + dst_ip + dst_port + d['summary'] = ',\n '.join([protocol, src, dst, action]) + ext_data.append(d) + return ext_data + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + obj = client.firewall_rules() + obj_extend = self.extend_list(obj, parsed_args) + headers, columns = column_util.get_column_definitions( + list(_attr_map), long_listing=parsed_args.long + ) + return ( + headers, + ( + utils.get_dict_properties(s, columns, formatters=_formatters) + for s in obj_extend + ), + ) + + +class SetFirewallRule(command.Command): + _description = _("Set firewall rule properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + 'firewall_rule', + metavar='', + help=_('Firewall rule to set (name or ID)'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('Name of the firewall rule'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + attrs = _get_common_attrs( + self.app.client_manager, parsed_args, is_create=False + ) + fwr_id = client.find_firewall_rule( + parsed_args.firewall_rule, ignore_missing=False + ).id + try: + client.update_firewall_rule(fwr_id, **attrs) + except Exception as e: + msg = _("Failed to set firewall rule '%(rule)s': %(e)s") % { + 'rule': parsed_args.firewall_rule, + 'e': e, + } + raise exceptions.CommandError(msg) + + +class ShowFirewallRule(command.ShowOne): + _description = _("Display firewall rule details") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'firewall_rule', + metavar='', + help=_('Firewall rule to display (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + fwr_id = client.find_firewall_rule( + parsed_args.firewall_rule, ignore_missing=False + ).id + obj = client.get_firewall_rule(fwr_id) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns, formatters=_formatters) + return (display_columns, data) + + +class UnsetFirewallRule(command.Command): + _description = _("Unset firewall rule properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'firewall_rule', + metavar='', + help=_('Firewall rule to unset (name or ID)'), + ) + parser.add_argument( + '--source-ip-address', + action='store_true', + help=_('Source IP address or subnet'), + ) + parser.add_argument( + '--destination-ip-address', + action='store_true', + help=_('Destination IP address or subnet'), + ) + parser.add_argument( + '--source-port', + action='store_true', + help=_( + 'Source port number or range' + '(integer in [1, 65535] or range like 123:456)' + ), + ) + parser.add_argument( + '--destination-port', + action='store_true', + help=_( + 'Destination port number or range' + '(integer in [1, 65535] or range like 123:456)' + ), + ) + parser.add_argument( + '--share', + action='store_true', + help=_( + '(Deprecated) Use "firewall rule set --no-share" instead. ' + 'Restrict use of the firewall rule to the current project' + ), + ) + parser.add_argument( + '--enable-rule', + action='store_true', + help=_( + '(Deprecated) Use "firewall rule set --disable-rule" instead. ' + 'Disable this rule' + ), + ) + + parser.add_argument( + '--source-firewall-group', + action='store_true', + help=_('Source firewall group (name or ID)'), + ) + + parser.add_argument( + '--destination-firewall-group', + action='store_true', + help=_('Destination firewall group (name or ID)'), + ) + return parser + + def _get_attrs( + self, client_manager: Any, parsed_args: argparse.Namespace + ) -> dict[str, Any]: + attrs: dict[str, Any] = {} + if parsed_args.source_ip_address: + attrs['source_ip_address'] = None + if parsed_args.source_port: + attrs['source_port'] = None + if parsed_args.destination_ip_address: + attrs['destination_ip_address'] = None + if parsed_args.destination_port: + attrs['destination_port'] = None + if parsed_args.share: + LOG.warning( + 'The --share option is deprecated, please use ' + '"firewall rule set --no-share" instead.' + ) + attrs['shared'] = False + if parsed_args.enable_rule: + LOG.warning( + 'The --enable-rule option is deprecated, please use ' + '"firewall rule set --disable-rule" instead.' + ) + attrs['enabled'] = False + if parsed_args.source_firewall_group: + attrs['source_firewall_group_id'] = None + if parsed_args.destination_firewall_group: + attrs['destination_firewall_group_id'] = None + return attrs + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + attrs = self._get_attrs(self.app.client_manager, parsed_args) + fwr_id = client.find_firewall_rule( + parsed_args.firewall_rule, ignore_missing=False + ).id + try: + client.update_firewall_rule(fwr_id, **attrs) + except Exception as e: + msg = _("Failed to unset firewall rule '%(rule)s': %(e)s") % { + 'rule': parsed_args.firewall_rule, + 'e': e, + } + raise exceptions.CommandError(msg) diff --git a/openstackclient/network/v2/ip_availability.py b/openstackclient/network/v2/ip_availability.py index 51ea01199f..5288c16d2b 100644 --- a/openstackclient/network/v2/ip_availability.py +++ b/openstackclient/network/v2/ip_availability.py @@ -13,10 +13,16 @@ """IP Availability Info implementations""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack.network.v2 import network_ip_availability as _ip_availability from osc_lib.cli import format_columns -from osc_lib.command import command from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -25,19 +31,19 @@ } -def _get_columns(item): +def _get_columns( + item: _ip_availability.NetworkIPAvailability, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['id', 'name', 'location', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns ) -# TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once -# the OSC minimum requirements include SDK 1.0. class ListIPAvailability(command.Lister): _description = _("List IP availability for network") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--ip-version', @@ -47,19 +53,25 @@ def get_parser(self, prog_name): metavar='', dest='ip_version', help=_( - "List IP availability of given IP version " - "networks (default is 4)" + "List only IP availability with the specified IP version " + "networks (4 or 6, default is 4)" ), ) parser.add_argument( '--project', metavar='', - help=_("List IP availability of given project (name or ID)"), + help=_( + "List only IP availability with the specified project " + "(name or ID)" + ), ) identity_common.add_project_domain_option_to_parser(parser) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( @@ -78,15 +90,21 @@ def take_action(self, parsed_args): filters = {} if parsed_args.ip_version: filters['ip_version'] = parsed_args.ip_version - if parsed_args.project: - identity_client = self.app.client_manager.identity - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) filters['project_id'] = project_id + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items + data = client.network_ip_availabilities(**filters) return ( column_headers, @@ -103,7 +121,7 @@ def take_action(self, parsed_args): class ShowIPAvailability(command.ShowOne): _description = _("Show network IP availability details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network', @@ -112,7 +130,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network network_id = client.find_network( parsed_args.network, ignore_missing=False diff --git a/openstackclient/network/v2/l3_conntrack_helper.py b/openstackclient/network/v2/l3_conntrack_helper.py index 011db39e07..0f3115dced 100644 --- a/openstackclient/network/v2/l3_conntrack_helper.py +++ b/openstackclient/network/v2/l3_conntrack_helper.py @@ -13,27 +13,34 @@ """L3 Conntrack Helper action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import l3_conntrack_helper as _l3_conntrack_helper from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _l3_conntrack_helper.ConntrackHelper, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns ) -def _get_attrs(client, parsed_args): +def _get_attrs(client: Any, parsed_args: argparse.Namespace) -> dict[str, Any]: router = client.find_router(parsed_args.router, ignore_missing=False) - attrs = {'router_id': router.id} + attrs: dict[str, Any] = {'router_id': router.id} if parsed_args.helper: attrs['helper'] = parsed_args.helper if parsed_args.protocol: @@ -47,7 +54,7 @@ def _get_attrs(client, parsed_args): class CreateConntrackHelper(command.ShowOne): _description = _("Create a new L3 conntrack helper") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -78,7 +85,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(client, parsed_args) @@ -92,7 +101,7 @@ def take_action(self, parsed_args): class DeleteConntrackHelper(command.Command): _description = _("Delete L3 conntrack helper") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -108,7 +117,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -140,7 +149,7 @@ def take_action(self, parsed_args): class ListConntrackHelper(command.Lister): _description = _("List L3 conntrack helpers") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -150,24 +159,34 @@ def get_parser(self, prog_name): parser.add_argument( '--helper', metavar='', - help=_('The netfilter conntrack helper module'), + help=_( + 'List only helpers using the specified netfilter conntrack ' + 'helper module' + ), ) parser.add_argument( '--protocol', metavar='', help=_( - 'The network protocol for the netfilter conntrack target rule' + 'List only helpers with the specified network protocol for ' + 'the netfilter conntrack target rule' ), ) parser.add_argument( '--port', metavar='', - help=_('The network port for the netfilter conntrack target rule'), + help=_( + 'List only helpers with the specified network port for ' + 'the netfilter conntrack target rule (name or ID)' + ), ) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( 'id', @@ -183,7 +202,15 @@ def take_action(self, parsed_args): 'Protocol', 'Port', ) + attrs = _get_attrs(client, parsed_args) + if parsed_args.marker is not None: + attrs['marker'] = parsed_args.marker + if parsed_args.limit is not None: + attrs['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + attrs['max_items'] = parsed_args.max_items + data = client.conntrack_helpers(attrs.pop('router_id'), **attrs) return ( @@ -202,7 +229,7 @@ def take_action(self, parsed_args): class SetConntrackHelper(command.Command): _description = _("Set L3 conntrack helper properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -234,7 +261,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network attrs = _get_attrs(client, parsed_args) if attrs: @@ -248,7 +275,7 @@ def take_action(self, parsed_args): class ShowConntrackHelper(command.ShowOne): _description = _("Display L3 conntrack helper details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -263,7 +290,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network router = client.find_router(parsed_args.router, ignore_missing=False) obj = client.get_conntrack_helper( diff --git a/openstackclient/network/v2/local_ip.py b/openstackclient/network/v2/local_ip.py index 7fd58818c7..80b66be033 100644 --- a/openstackclient/network/v2/local_ip.py +++ b/openstackclient/network/v2/local_ip.py @@ -15,27 +15,35 @@ """Node Local IP action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import local_ip as _local_ip from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _local_ip.LocalIP, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns ) -def _get_attrs(client_manager, parsed_args): - attrs = {} +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} network_client = client_manager.network if parsed_args.name: @@ -43,12 +51,12 @@ def _get_attrs(client_manager, parsed_args): if parsed_args.description: attrs['description'] = parsed_args.description if 'project' in parsed_args and parsed_args.project is not None: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id if parsed_args.network: network = network_client.find_network( @@ -70,7 +78,7 @@ def _get_attrs(client_manager, parsed_args): class CreateLocalIP(command.ShowOne): _description = _("Create Local IP") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--name', metavar="", help=_("New Local IP name") @@ -105,7 +113,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) @@ -119,7 +129,7 @@ def take_action(self, parsed_args): class DeleteLocalIP(command.Command): _description = _("Delete Local IP(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'local_ip', @@ -130,7 +140,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -160,7 +170,7 @@ def take_action(self, parsed_args): class SetLocalIP(command.Command): _description = _("Set Local IP properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'local_ip', @@ -177,7 +187,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_local_ip(parsed_args.local_ip, ignore_missing=False) attrs = {} @@ -192,45 +202,53 @@ def take_action(self, parsed_args): class ListLocalIP(command.Lister): _description = _("List Local IPs") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--name', metavar='', - help=_("List only Local IPs of given name in output"), + help=_("List only local IP(s) with the specified name"), ) parser.add_argument( '--project', metavar="", - help=_("List Local IPs according to their project (name or ID)"), + help=_( + "List only local IP(s) with the specified project (name or ID)" + ), ) parser.add_argument( '--network', metavar='', - help=_("List Local IP(s) according to given network (name or ID)"), + help=_( + "List only local IP(s) with the specified network (name or ID)" + ), ) parser.add_argument( '--local-port', metavar='', - help=_("List Local IP(s) according to given port (name or ID)"), + help=_( + "List only local IP(s) with the specified port (name or ID)" + ), ) parser.add_argument( '--local-ip-address', metavar='', - help=_("List Local IP(s) according to given Local IP Address"), + help=_("List only local IP(s) with the specified IP address"), ) parser.add_argument( '--ip-mode', metavar='', - help=_("List Local IP(s) according to given IP mode"), + help=_("List only local IP(s) with the specified IP mode"), ) identity_common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( 'id', @@ -256,12 +274,12 @@ def take_action(self, parsed_args): if parsed_args.name: attrs['name'] = parsed_args.name if 'project' in parsed_args and parsed_args.project is not None: - identity_client = self.app.client_manager.identity - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id if parsed_args.network is not None: network = client.find_network( @@ -295,7 +313,7 @@ def take_action(self, parsed_args): class ShowLocalIP(command.ShowOne): _description = _("Display Local IP details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'local_ip', @@ -305,7 +323,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_local_ip(parsed_args.local_ip, ignore_missing=False) display_columns, columns = _get_columns(obj) diff --git a/openstackclient/network/v2/local_ip_association.py b/openstackclient/network/v2/local_ip_association.py index c6b59184b7..e41db1d6d5 100644 --- a/openstackclient/network/v2/local_ip_association.py +++ b/openstackclient/network/v2/local_ip_association.py @@ -15,19 +15,25 @@ """Node Local IP action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import local_ip_association as _local_ip_association from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _local_ip_association.LocalIPAssociation, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'name', 'id', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns @@ -37,7 +43,7 @@ def _get_columns(item): class CreateLocalIPAssociation(command.ShowOne): _description = _("Create Local IP Association") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'local_ip', @@ -61,7 +67,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = {} @@ -83,7 +91,7 @@ def take_action(self, parsed_args): class DeleteLocalIPAssociation(command.Command): _description = _("Delete Local IP association(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'local_ip', @@ -100,7 +108,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network local_ip = client.find_local_ip( parsed_args.local_ip, @@ -138,7 +146,7 @@ def take_action(self, parsed_args): class ListLocalIPAssociation(command.Lister): _description = _("List Local IP Associations") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -150,24 +158,29 @@ def get_parser(self, prog_name): '--fixed-port', metavar='', help=_( - "Filter the list result by the ID or name of the fixed port" + "List only local IP assocations with the specified fixed IP " + "port (name or ID)" ), ) parser.add_argument( '--fixed-ip', metavar='', - help=_("Filter the list result by fixed ip"), + help=_( + "List only local IP associations with the specified fixed IP" + ), ) parser.add_argument( '--host', metavar='', - help=_("Filter the list result by given host"), + help=_("List only local IP associations with the specified host"), ) identity_common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( 'local_ip_id', diff --git a/openstackclient/network/v2/ndp_proxy.py b/openstackclient/network/v2/ndp_proxy.py index fe97cd078f..a3622d3377 100644 --- a/openstackclient/network/v2/ndp_proxy.py +++ b/openstackclient/network/v2/ndp_proxy.py @@ -15,12 +15,16 @@ """Router NDP proxy action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import ndp_proxy as _ndp_proxy from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -28,7 +32,9 @@ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _ndp_proxy.NDPProxy, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns @@ -38,7 +44,7 @@ def _get_columns(item): class CreateNDPProxy(command.ShowOne): _description = _("Create NDP proxy") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', metavar='', help=_("The name or ID of a router") @@ -75,7 +81,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: attrs = {'name': parsed_args.name} client = self.app.client_manager.network router = client.find_router( @@ -102,7 +110,7 @@ def take_action(self, parsed_args): class DeleteNDPProxy(command.Command): _description = _("Delete NDP proxy") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'ndp_proxy', @@ -112,7 +120,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -137,45 +145,53 @@ def take_action(self, parsed_args): class ListNDPProxy(command.Lister): _description = _("List NDP proxies") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--router', metavar='', help=_( - "List only NDP proxies belonging to this router (name or ID)" + "List only NDP proxies associated with the specifed router " + "(name or ID)" ), ) parser.add_argument( '--port', metavar='', help=_( - "List only NDP proxies associated to this port (name or ID)" + "List only NDP proxies associated with the specified port " + "(name or ID)" ), ) parser.add_argument( '--ip-address', metavar='', - help=_("List only NDP proxies associated to this IPv6 address"), + help=_( + "List only NDP proxies associated with the specified " + "IPv6 address" + ), ) parser.add_argument( '--project', metavar='', - help=_("List only NDP proxies of given project (name or ID)"), + help=_( + "List only NDP proxies with the specified project (name or ID)" + ), ) parser.add_argument( '--name', metavar='', - help=_("List only NDP proxies of given name"), + help=_("List only NDP proxies with the specified name"), ) identity_common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network - identity_client = self.app.client_manager.identity columns = ( 'id', @@ -205,11 +221,12 @@ def take_action(self, parsed_args): if parsed_args.ip_address is not None: query['ip_address'] = parsed_args.ip_address if parsed_args.project: - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) query['project_id'] = project_id if parsed_args.name: query['name'] = parsed_args.name @@ -232,7 +249,7 @@ def take_action(self, parsed_args): class SetNDPProxy(command.Command): _description = _("Set NDP proxy properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'ndp_proxy', @@ -252,7 +269,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network attrs = {} if parsed_args.description is not None: @@ -269,7 +286,7 @@ def take_action(self, parsed_args): class ShowNDPProxy(command.ShowOne): _description = _("Display NDP proxy details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'ndp_proxy', @@ -278,7 +295,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_ndp_proxy( parsed_args.ndp_proxy, ignore_missing=False diff --git a/openstackclient/network/v2/network.py b/openstackclient/network/v2/network.py index e04efed616..733b493f1b 100644 --- a/openstackclient/network/v2/network.py +++ b/openstackclient/network/v2/network.py @@ -12,25 +12,34 @@ """Network action implementations""" +import argparse +from collections.abc import Iterable, Sequence +import logging +from typing import Any + from cliff import columns as cliff_columns +from openstack.network.v2 import network as _network from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils from osc_lib.utils import tags as _tag -from openstackclient.api import compute_v2 +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common +LOG = logging.getLogger(__name__) + -class AdminStateColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class AdminStateColumn(cliff_columns.FormattableColumn[bool]): + def human_readable(self) -> str: return 'UP' if self._value else 'DOWN' -class RouterExternalColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class RouterExternalColumn(cliff_columns.FormattableColumn[bool]): + def human_readable(self) -> str: return 'External' if self._value else 'Internal' @@ -47,7 +56,9 @@ def human_readable(self): } -def _get_columns_network(item): +def _get_columns_network( + item: _network.Network, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { 'subnet_ids': 'subnets', 'is_admin_state_up': 'admin_state_up', @@ -67,11 +78,9 @@ def _get_columns_network(item): ) -def _get_columns_compute(item): - return utils.get_osc_show_columns_for_sdk_resource(item, {}) - - -def _get_attrs_network(client_manager, parsed_args): +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: attrs = {} if parsed_args.name is not None: attrs['name'] = parsed_args.name @@ -90,12 +99,12 @@ def _get_attrs_network(client_manager, parsed_args): # "network set" command doesn't support setting project. if 'project' in parsed_args and parsed_args.project is not None: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id # "network set" command doesn't support setting availability zone hints. @@ -137,26 +146,20 @@ def _get_attrs_network(client_manager, parsed_args): attrs['qos_policy_id'] = _qos_policy.id if 'no_qos_policy' in parsed_args and parsed_args.no_qos_policy: attrs['qos_policy_id'] = None + + # Set pvlan + if parsed_args.pvlan: + attrs['pvlan'] = True + if parsed_args.no_pvlan: + attrs['pvlan'] = False + # Update DNS network options if parsed_args.dns_domain is not None: attrs['dns_domain'] = parsed_args.dns_domain return attrs -def _get_attrs_compute(client_manager, parsed_args): - attrs = {} - if parsed_args.name is not None: - attrs['name'] = parsed_args.name - if parsed_args.share: - attrs['share_subnet'] = True - if parsed_args.no_share: - attrs['share_subnet'] = False - if parsed_args.subnet is not None: - attrs['subnet'] = parsed_args.subnet - return attrs - - -def _add_additional_network_options(parser): +def _add_additional_network_options(parser: argparse.ArgumentParser) -> None: # Add additional network options parser.add_argument( @@ -197,14 +200,11 @@ def _add_additional_network_options(parser): ) -# TODO(sindhu): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. -class CreateNetwork( - common.NetworkAndComputeShowOne, common.NeutronCommandWithExtraArgs -): +class CreateNetwork(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create new network") - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar='', help=_("New network name") ) @@ -220,35 +220,32 @@ def update_parser_common(self, parser): action='store_true', help=_("Do not share the network between projects"), ) - return parser - - def update_parser_network(self, parser): admin_group = parser.add_mutually_exclusive_group() admin_group.add_argument( '--enable', action='store_true', default=True, - help=self.enhance_help_neutron(_("Enable network (default)")), + help=_("Enable network (default)"), ) admin_group.add_argument( '--disable', action='store_true', - help=self.enhance_help_neutron(_("Disable network")), + help=_("Disable network"), ) parser.add_argument( '--project', metavar='', - help=self.enhance_help_neutron(_("Owner's project (name or ID)")), + help=_("Owner's project (name or ID)"), ) parser.add_argument( '--description', metavar='', - help=self.enhance_help_neutron(_("Set network description")), + help=_("Set network description"), ) parser.add_argument( '--mtu', metavar='', - help=self.enhance_help_neutron(_("Set network mtu")), + help=_("Set network mtu"), ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( @@ -256,134 +253,118 @@ def update_parser_network(self, parser): action='append', dest='availability_zone_hints', metavar='', - help=self.enhance_help_neutron( - _( - "Availability Zone in which to create this network " - "(Network Availability Zone extension required, " - "repeat option to set multiple availability zones)" - ) + help=_( + "Availability Zone in which to create this network " + "(Network Availability Zone extension required, " + "repeat option to set multiple availability zones)" ), ) port_security_group = parser.add_mutually_exclusive_group() port_security_group.add_argument( '--enable-port-security', action='store_true', - help=self.enhance_help_neutron( - _( - "Enable port security by default for ports created on " - "this network (default)" - ) + help=_( + "Enable port security by default for ports created on " + "this network (default)" ), ) port_security_group.add_argument( '--disable-port-security', action='store_true', - help=self.enhance_help_neutron( - _( - "Disable port security by default for ports created on " - "this network" - ) + help=_( + "Disable port security by default for ports created on " + "this network" ), ) external_router_grp = parser.add_mutually_exclusive_group() external_router_grp.add_argument( '--external', action='store_true', - help=self.enhance_help_neutron( - _( - "The network has an external routing facility that is not " - "managed by Neutron and can be used. For example: " - "openstack router set --external-gateway NETWORK " - "(external-net extension required)" - ) + help=_( + "The network has an external routing facility that is not " + "managed by Neutron and can be used. For example: " + "openstack router set --external-gateway NETWORK " + "(external-net extension required)" ), ) external_router_grp.add_argument( '--internal', action='store_true', - help=self.enhance_help_neutron( - _("Opposite of '--external' (default)") - ), + help=_("Opposite of '--external' (default)"), ) default_router_grp = parser.add_mutually_exclusive_group() default_router_grp.add_argument( '--default', action='store_true', - help=self.enhance_help_neutron( - _( - "Specify if this network should be used as the default " - "external network" - ) + help=_( + "Specify if this network should be used as the default " + "external network" ), ) default_router_grp.add_argument( '--no-default', action='store_true', - help=self.enhance_help_neutron( - _( - "Do not use the network as the default external network " - "(default)" - ) + help=_( + "Do not use the network as the default external network " + "(default)" ), ) parser.add_argument( '--qos-policy', metavar='', - help=self.enhance_help_neutron( - _("QoS policy to attach to this network (name or ID)") - ), + help=_("QoS policy to attach to this network (name or ID)"), ) vlan_transparent_grp = parser.add_mutually_exclusive_group() vlan_transparent_grp.add_argument( '--transparent-vlan', action='store_true', - help=self.enhance_help_neutron( - _("Make the network VLAN transparent") - ), + help=_("Make the network VLAN transparent"), ) vlan_transparent_grp.add_argument( '--no-transparent-vlan', action='store_true', - help=self.enhance_help_neutron( - _("Do not make the network VLAN transparent") - ), + help=_("Do not make the network VLAN transparent"), ) vlan_qinq_grp = parser.add_mutually_exclusive_group() vlan_qinq_grp.add_argument( '--qinq-vlan', action='store_true', - help=self.enhance_help_neutron( - _("Enable VLAN QinQ (S-Tag ethtype 0x8a88) for the network") - ), + help=_("Enable VLAN QinQ (S-Tag ethtype 0x8a88) for the network"), ) vlan_qinq_grp.add_argument( '--no-qinq-vlan', action='store_true', - help=self.enhance_help_neutron( - _("Disable VLAN QinQ (S-Tag ethtype 0x8a88) for the network") - ), + help=_("Disable VLAN QinQ (S-Tag ethtype 0x8a88) for the network"), ) - _add_additional_network_options(parser) - _tag.add_tag_option_to_parser_for_create( - parser, _('network'), enhance_help=self.enhance_help_neutron + pvlan_grp = parser.add_mutually_exclusive_group() + pvlan_grp.add_argument( + '--pvlan', + action='store_true', + help=_( + "Enable Private VLAN for the network " + "(PVLAN extension required)" + ), ) - return parser - - def update_parser_compute(self, parser): - parser.add_argument( - '--subnet', - metavar='', - required=True, - help=self.enhance_help_nova_network( - _("IPv4 subnet for fixed IPs (in CIDR notation)") + pvlan_grp.add_argument( + '--no-pvlan', + action='store_true', + help=_( + "Disable Private VLAN for the network " + "(PVLAN extension required)" ), ) + + _add_additional_network_options(parser) + _tag.add_tag_option_to_parser_for_create(parser, _('network')) return parser - def take_action_network(self, client, parsed_args): - attrs = _get_attrs_network(self.app.client_manager, parsed_args) + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs = _get_attrs(self.app.client_manager, parsed_args) if parsed_args.transparent_vlan: attrs['vlan_transparent'] = True if parsed_args.no_transparent_vlan: @@ -394,6 +375,11 @@ def take_action_network(self, client, parsed_args): if parsed_args.no_qinq_vlan: attrs['vlan_qinq'] = False + if parsed_args.pvlan: + attrs['pvlan'] = True + if parsed_args.no_pvlan: + attrs['pvlan'] = False + if attrs.get('vlan_transparent') and attrs.get('vlan_qinq'): msg = _( "--transparent-vlan and --qinq-vlan can not be both enabled " @@ -401,6 +387,24 @@ def take_action_network(self, client, parsed_args): ) raise exceptions.CommandError(msg) + if ( + attrs.get('port_security_enabled') is False + and attrs.get('pvlan') is True + ): + msg = _( + "--disable-port-security and --pvlan can not be used together." + ) + raise exceptions.CommandError(msg) + if ( + parsed_args.segmentation_id + and not parsed_args.provider_network_type + ): + msg = _( + "--provider-segment requires --provider-network-type " + "to be specified." + ) + raise exceptions.CommandError(msg) + attrs.update( self._parse_extra_properties(parsed_args.extra_properties) ) @@ -415,162 +419,166 @@ def take_action_network(self, client, parsed_args): data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) - def take_action_compute(self, client, parsed_args): - attrs = _get_attrs_compute(self.app.client_manager, parsed_args) - obj = compute_v2.create_network(client, **attrs) - display_columns, columns = _get_columns_compute(obj) - data = utils.get_dict_properties(obj, columns) - return (display_columns, data) - -class DeleteNetwork(common.NetworkAndComputeDelete): +class DeleteNetwork(command.Command): _description = _("Delete network(s)") - # Used by base class to find resources in parsed_args. - resource = 'network' - r = None - - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'network', metavar="", nargs="+", help=_("Network(s) to delete (name or ID)"), ) - return parser - def take_action_network(self, client, parsed_args): - obj = client.find_network(self.r, ignore_missing=False) - client.delete_network(obj) + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 + + for net in parsed_args.network: + try: + obj = client.find_network(net, ignore_missing=False) + client.delete_network(obj) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete network with " + "name or ID '%(network)s': %(e)s" + ), + {'network': net, 'e': e}, + ) - def take_action_compute(self, client, parsed_args): - network = compute_v2.find_network(client, self.r) - compute_v2.delete_network(client, network['id']) + if result > 0: + total = len(parsed_args.network) + msg = _("%(result)s of %(total)s networks failed to delete.") % { + 'result': result, + 'total': total, + } + raise exceptions.CommandError(msg) -# TODO(sindhu): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. -class ListNetwork(common.NetworkAndComputeLister): +class ListNetwork(command.Lister): _description = _("List networks") - def update_parser_network(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) router_ext_group = parser.add_mutually_exclusive_group() router_ext_group.add_argument( '--external', action='store_true', - help=self.enhance_help_neutron(_("List external networks")), + help=_("List only external networks"), ) router_ext_group.add_argument( '--internal', action='store_true', - help=self.enhance_help_neutron(_("List internal networks")), + help=_("List only internal networks"), ) parser.add_argument( '--long', action='store_true', - help=self.enhance_help_neutron( - _("List additional fields in output") - ), + help=_("List additional fields in output"), ) parser.add_argument( '--name', metavar='', - help=self.enhance_help_neutron( - _("List networks according to their name") - ), + help=_("List only networks with the specified name"), ) admin_state_group = parser.add_mutually_exclusive_group() admin_state_group.add_argument( '--enable', action='store_true', - help=self.enhance_help_neutron(_("List enabled networks")), + help=_("List only enabled networks"), ) admin_state_group.add_argument( '--disable', action='store_true', - help=self.enhance_help_neutron(_("List disabled networks")), + help=_("List only disabled networks"), ) parser.add_argument( '--project', metavar='', - help=_("List networks according to their project (name or ID)"), + help=_( + "List only networks with the specified project (name or ID)" + ), + ) + pvlan_group = parser.add_mutually_exclusive_group() + pvlan_group.add_argument( + '--pvlan', + action='store_true', + help=_("List only networks with PVLAN enabled"), ) - identity_common.add_project_domain_option_to_parser( - parser, enhance_help=self.enhance_help_neutron + pvlan_group.add_argument( + '--no-pvlan', + action='store_true', + help=_("List only networks with PVLAN disabled"), ) + identity_common.add_project_domain_option_to_parser(parser) shared_group = parser.add_mutually_exclusive_group() shared_group.add_argument( '--share', action='store_true', - help=self.enhance_help_neutron( - _("List networks shared between projects") - ), + help=_("List only networks shared between projects"), ) shared_group.add_argument( '--no-share', action='store_true', - help=self.enhance_help_neutron( - _("List networks not shared between projects") - ), + help=_("List only networks not shared between projects"), ) parser.add_argument( '--status', metavar='', choices=['ACTIVE', 'BUILD', 'DOWN', 'ERROR'], - help=self.enhance_help_neutron( - _( - "List networks according to their status " - "('ACTIVE', 'BUILD', 'DOWN', 'ERROR')" - ) + help=_( + "List only networks with the specified status " + "('ACTIVE', 'BUILD', 'DOWN', 'ERROR')" ), ) parser.add_argument( '--provider-network-type', metavar='', choices=['flat', 'geneve', 'gre', 'local', 'vlan', 'vxlan'], - help=self.enhance_help_neutron( - _( - "List networks according to their physical mechanisms. " - "The supported options are: flat, geneve, gre, local, " - "vlan and vxlan." - ) + help=_( + "List only networks with the specified physical " + "mechanisms. " + "The supported options are: flat, geneve, gre, local, " + "vlan and vxlan." ), ) parser.add_argument( '--provider-physical-network', metavar='', dest='physical_network', - help=self.enhance_help_neutron( - _("List networks according to name of the physical network") + help=_( + "List only networks with the specified physical network name" ), ) parser.add_argument( '--provider-segment', metavar='', dest='segmentation_id', - help=self.enhance_help_neutron( - _( - "List networks according to VLAN ID for VLAN networks or " - "Tunnel ID for GENEVE/GRE/VXLAN networks" - ) + help=_( + "List only networks with the specified provider segment " + "ID (VLAN ID for VLAN networks or " + "Tunnel ID for GENEVE/GRE/VXLAN networks)" ), ) parser.add_argument( '--agent', metavar='', dest='agent_id', - help=self.enhance_help_neutron( - _('List networks hosted by agent (ID only)') - ), - ) - _tag.add_tag_filtering_option_to_parser( - parser, _('networks'), enhance_help=self.enhance_help_neutron + help=_('List only networks hosted the specified agent (ID only)'), ) + _tag.add_tag_filtering_option_to_parser(parser, _('networks')) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action_network(self, client, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network if parsed_args.long: columns: tuple[str, ...] = ( 'id', @@ -598,7 +606,15 @@ def take_action_network(self, client, parsed_args): 'Availability Zones', 'Tags', ) - elif parsed_args.agent_id: + else: + columns = ('id', 'name', 'subnet_ids') + column_headers = ( + 'ID', + 'Name', + 'Subnets', + ) + + if parsed_args.agent_id: columns = ('id', 'name', 'subnet_ids') column_headers = ( 'ID', @@ -607,7 +623,6 @@ def take_action_network(self, client, parsed_args): ) client = self.app.client_manager.network dhcp_agent = client.get_agent(parsed_args.agent_id) - data = client.dhcp_agent_hosting_networks(dhcp_agent) return ( column_headers, @@ -617,18 +632,11 @@ def take_action_network(self, client, parsed_args): columns, formatters=_formatters, ) - for s in data + for s in client.dhcp_agent_hosting_networks(dhcp_agent) ), ) - else: - columns = ('id', 'name', 'subnet_ids') - column_headers = ( - 'ID', - 'Name', - 'Subnets', - ) - args = {} + args: dict[str, object] = {} if parsed_args.external: args['router:external'] = True @@ -648,12 +656,13 @@ def take_action_network(self, client, parsed_args): args['is_admin_state_up'] = False if parsed_args.project: - project = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, ) - args['project_id'] = project.id + args['project_id'] = project_id if parsed_args.share: args['shared'] = True @@ -674,11 +683,19 @@ def take_action_network(self, client, parsed_args): if parsed_args.segmentation_id: args['provider:segmentation_id'] = parsed_args.segmentation_id args['provider_segmentation_id'] = parsed_args.segmentation_id + if parsed_args.pvlan: + args['pvlan'] = True + elif parsed_args.no_pvlan: + args['pvlan'] = False + if parsed_args.marker is not None: + args['marker'] = parsed_args.marker + if parsed_args.limit is not None: + args['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + args['max_items'] = parsed_args.max_items _tag.get_tag_filtering_args(parsed_args, args) - data = client.networks(**args) - return ( column_headers, ( @@ -687,43 +704,15 @@ def take_action_network(self, client, parsed_args): columns, formatters=_formatters, ) - for s in data + for s in client.networks(**args) ), ) - def take_action_compute(self, client, parsed_args): - columns = ( - 'id', - 'label', - 'cidr', - ) - column_headers = ( - 'ID', - 'Name', - 'Subnet', - ) - - data = compute_v2.list_networks(client) - - return ( - column_headers, - ( - utils.get_dict_properties( - s, - columns, - formatters=_formatters, - ) - for s in data - ), - ) - -# TODO(sindhu): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class SetNetwork(common.NeutronCommandWithExtraArgs): _description = _("Set network properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network', @@ -818,18 +807,43 @@ def get_parser(self, prog_name): action='store_true', help=_("Remove the QoS policy attached to this network"), ) + pvlan_grp = parser.add_mutually_exclusive_group() + pvlan_grp.add_argument( + '--pvlan', + action='store_true', + help=_( + "Enable Private VLAN for the network. PVLAN extension " + "required." + ), + ) + pvlan_grp.add_argument( + '--no-pvlan', + action='store_true', + help=_( + "Disable Private VLAN for the network (Default). " + "PVLAN extension required." + ), + ) _tag.add_tag_option_to_parser_for_set(parser, _('network')) _add_additional_network_options(parser) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_network(parsed_args.network, ignore_missing=False) - attrs = _get_attrs_network(self.app.client_manager, parsed_args) + attrs = _get_attrs(self.app.client_manager, parsed_args) attrs.update( self._parse_extra_properties(parsed_args.extra_properties) ) + if ( + attrs.get('port_security_enabled') is False + and attrs.get('pvlan') is True + ): + msg = _( + "--disable-port-security and --pvlan can not be used together." + ) + raise exceptions.CommandError(msg) if attrs: with common.check_missing_extension_if_error( self.app.client_manager.network, attrs @@ -840,10 +854,11 @@ def take_action(self, parsed_args): _tag.update_tags_for_set(client, obj, parsed_args) -class ShowNetwork(common.NetworkAndComputeShowOne): +class ShowNetwork(command.ShowOne): _description = _("Show network details") - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'network', metavar="", @@ -851,23 +866,20 @@ def update_parser_common(self, parser): ) return parser - def take_action_network(self, client, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network obj = client.find_network(parsed_args.network, ignore_missing=False) display_columns, columns = _get_columns_network(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) return (display_columns, data) - def take_action_compute(self, client, parsed_args): - obj = compute_v2.find_network(client, parsed_args.network) - display_columns, columns = _get_columns_compute(obj) - data = utils.get_dict_properties(obj, columns) - return (display_columns, data) - class UnsetNetwork(common.NeutronUnsetCommandWithExtraArgs): _description = _("Unset network properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network', @@ -877,7 +889,7 @@ def get_parser(self, prog_name): _tag.add_tag_option_to_parser_for_unset(parser, _('network')) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_network(parsed_args.network, ignore_missing=False) diff --git a/openstackclient/network/v2/network_agent.py b/openstackclient/network/v2/network_agent.py index 35b0fab664..577c1494a9 100644 --- a/openstackclient/network/v2/network_agent.py +++ b/openstackclient/network/v2/network_agent.py @@ -13,26 +13,31 @@ """Network agent action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any from cliff import columns as cliff_columns +from openstack.network.v2 import agent as _agent from osc_lib.cli import format_columns -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -class AliveColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class AliveColumn(cliff_columns.FormattableColumn[bool]): + def human_readable(self) -> str: return ":-)" if self._value else "XXX" -class AdminStateColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class AdminStateColumn(cliff_columns.FormattableColumn[bool]): + def human_readable(self) -> str: return 'UP' if self._value else 'DOWN' @@ -45,21 +50,37 @@ def human_readable(self): } -def _get_network_columns(item): - column_map = { - 'is_admin_state_up': 'admin_state_up', - 'is_alive': 'alive', +def _get_network_columns( + item: _agent.Agent, +) -> tuple[tuple[str, ...], tuple[str, ...]]: + column_data_mapping = { + 'admin_state_up': 'is_admin_state_up', + 'agent_type': 'agent_type', + 'alive': 'is_alive', + 'availability_zone': 'availability_zone', + 'binary': 'binary', + 'configuration': 'configuration', + 'created_at': 'created_at', + 'description': 'description', + 'ha_chassis_priority': 'ha_chassis_priority', + 'ha_state': 'ha_state', + 'host': 'host', + 'id': 'id', + 'last_heartbeat_at': 'last_heartbeat_at', + 'resources_synced': 'resources_synced', + 'started_at': 'started_at', + 'topic': 'topic', } - hidden_columns = ['location', 'name', 'tenant_id'] - return utils.get_osc_show_columns_for_sdk_resource( - item, column_map, hidden_columns + return ( + tuple(column_data_mapping.keys()), + tuple(column_data_mapping.values()), ) class AddNetworkToAgent(command.Command): _description = _("Add network to an agent") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--dhcp', @@ -79,7 +100,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network agent = client.get_agent(parsed_args.agent_id) network = client.find_network( @@ -96,11 +117,20 @@ def take_action(self, parsed_args): class AddRouterToAgent(command.Command): _description = _("Add router to an agent") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--l3', action='store_true', help=_('Add router to an L3 agent') ) + parser.add_argument( + '--ha-chassis-priority', + metavar='', + type=int, + help=_( + "HA Chassis priority, ranging from [0, 32767]. " + "Only used with --l3 and for ML2/OVN L3 agents" + ), + ) parser.add_argument( 'agent_id', metavar='', @@ -114,18 +144,22 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network agent = client.get_agent(parsed_args.agent_id) router = client.find_router(parsed_args.router, ignore_missing=False) if parsed_args.l3: - client.add_router_to_agent(agent, router) + client.add_router_to_agent( + agent, + router, + ha_chassis_priority=parsed_args.ha_chassis_priority, + ) class DeleteNetworkAgent(command.Command): _description = _("Delete network agent(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network_agent', @@ -135,7 +169,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -160,8 +194,6 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) -# TODO(huanxuan): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class ListNetworkAgent(command.Lister): _description = _("List network agents") _supported_agents = { @@ -183,7 +215,7 @@ class ListNetworkAgent(command.Lister): 'ovn-agent': 'OVN Neutron agent', } - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) supported_agents = ','.join(self._supported_agents.keys()) parser.add_argument( @@ -205,12 +237,12 @@ def get_parser(self, prog_name): agent_type_group.add_argument( '--network', metavar='', - help=_('List agents hosting a network (name or ID)'), + help=_('List agents hosting the specified network (name or ID)'), ) agent_type_group.add_argument( '--router', metavar='', - help=_('List agents hosting this router (name or ID)'), + help=_('List agents hosting the specified router (name or ID)'), ) parser.add_argument( '--long', @@ -218,10 +250,13 @@ def get_parser(self, prog_name): default=False, help=_("List additional fields in output"), ) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns: tuple[str, ...] = ( 'id', @@ -243,12 +278,19 @@ def take_action(self, parsed_args): ) filters = {} - + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items + + data: list[_agent.Agent] if parsed_args.network is not None: network = client.find_network( parsed_args.network, ignore_missing=False ) - data = client.network_hosting_dhcp_agents(network) + data = list(client.network_hosting_dhcp_agents(network)) elif parsed_args.router is not None: if parsed_args.long: columns += ('ha_state',) @@ -256,7 +298,7 @@ def take_action(self, parsed_args): router = client.find_router( parsed_args.router, ignore_missing=False ) - data = client.routers_hosting_l3_agents(router) + data = list(client.routers_hosting_l3_agents(router)) else: if parsed_args.agent_type is not None: filters['agent_type'] = self._supported_agents[ @@ -265,7 +307,8 @@ def take_action(self, parsed_args): if parsed_args.host is not None: filters['host'] = parsed_args.host - data = client.agents(**filters) + data = list(client.agents(**filters)) + return ( column_headers, ( @@ -282,7 +325,7 @@ def take_action(self, parsed_args): class RemoveNetworkFromAgent(command.Command): _description = _("Remove network from an agent.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--dhcp', @@ -301,7 +344,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network agent = client.get_agent(parsed_args.agent_id) network = client.find_network( @@ -318,7 +361,7 @@ def take_action(self, parsed_args): class RemoveRouterFromAgent(command.Command): _description = _("Remove router from an agent") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--l3', @@ -338,7 +381,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network agent = client.get_agent(parsed_args.agent_id) router = client.find_router(parsed_args.router, ignore_missing=False) @@ -346,12 +389,49 @@ def take_action(self, parsed_args): client.remove_router_from_agent(agent, router) -# TODO(huanxuan): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. +class SetNetworkAgentRouter(command.Command): + _description = _("Set properties of a router associated to an agent") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--ha-chassis-priority', + metavar='', + type=int, + required=True, + help=_( + "HA Chassis priority, ranging from [0, 32767]. " + "Only used with --l3 and for ML2/OVN L3 agents" + ), + ) + parser.add_argument( + 'agent_id', + metavar='', + help=_("L3 agent to modify (ID only)"), + ) + parser.add_argument( + 'router', + metavar='', + help=_("Router to update (name or ID)"), + ) + + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + agent = client.get_agent(parsed_args.agent_id) + router = client.find_router(parsed_args.router, ignore_missing=False) + client.update_router_in_agent( + agent, + router, + ha_chassis_priority=parsed_args.ha_chassis_priority, + ) + + class SetNetworkAgent(command.Command): _description = _("Set network agent properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network_agent', @@ -372,7 +452,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.get_agent(parsed_args.network_agent) attrs = {} @@ -390,7 +470,7 @@ def take_action(self, parsed_args): class ShowNetworkAgent(command.ShowOne): _description = _("Display network agent details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network_agent', @@ -399,7 +479,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.get_agent(parsed_args.network_agent) display_columns, columns = _get_network_columns(obj) diff --git a/openstackclient/network/v2/network_auto_allocated_topology.py b/openstackclient/network/v2/network_auto_allocated_topology.py index 3f5795e6a5..a7781d3b0d 100644 --- a/openstackclient/network/v2/network_auto_allocated_topology.py +++ b/openstackclient/network/v2/network_auto_allocated_topology.py @@ -13,44 +13,54 @@ """Auto-allocated Topology Implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import ( + auto_allocated_topology as _auto_allocated_topology, +) from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _auto_allocated_topology.AutoAllocatedTopology, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['name', 'location', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns ) -def _format_check_resource_columns(): +def _format_check_resource_columns() -> tuple[str, ...]: return ('dry_run',) -def _format_check_resource(item): +def _format_check_resource(item: Any) -> Any: item_id = getattr(item, 'id', False) if item_id == 'dry-run=pass': item.check_resource = 'pass' return item -def _get_attrs(client_manager, parsed_args): - attrs = {} +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} if parsed_args.project: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id if parsed_args.check_resources: attrs['check_resources'] = True @@ -58,12 +68,10 @@ def _get_attrs(client_manager, parsed_args): return attrs -# TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class CreateAutoAllocatedTopology(command.ShowOne): _description = _("Create the auto allocated topology for project") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--project', @@ -94,7 +102,9 @@ def get_parser(self, prog_name): return parser - def check_resource_topology(self, client, parsed_args): + def check_resource_topology( + self, client: Any, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Any]: obj = client.validate_auto_allocated_topology(parsed_args.project) columns = _format_check_resource_columns() @@ -104,13 +114,17 @@ def check_resource_topology(self, client, parsed_args): return (columns, data) - def get_topology(self, client, parsed_args): + def get_topology( + self, client: Any, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Any]: obj = client.get_auto_allocated_topology(parsed_args.project) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters={}) return (display_columns, data) - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network if parsed_args.check_resources: columns, data = self.check_resource_topology(client, parsed_args) @@ -119,12 +133,10 @@ def take_action(self, parsed_args): return (columns, data) -# TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class DeleteAutoAllocatedTopology(command.Command): _description = _("Delete auto allocated topology for project") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--project', @@ -138,6 +150,6 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network client.delete_auto_allocated_topology(parsed_args.project) diff --git a/openstackclient/network/v2/network_flavor.py b/openstackclient/network/v2/network_flavor.py index c4d262dea1..db0c8920db 100644 --- a/openstackclient/network/v2/network_flavor.py +++ b/openstackclient/network/v2/network_flavor.py @@ -13,12 +13,17 @@ """Flavor action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import flavor as _flavor from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -26,7 +31,9 @@ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _flavor.Flavor, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { 'is_enabled': 'enabled', } @@ -37,8 +44,10 @@ def _get_columns(item): ) -def _get_attrs(client_manager, parsed_args): - attrs = {} +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} attrs['name'] = parsed_args.name attrs['service_type'] = parsed_args.service_type if parsed_args.description is not None: @@ -48,12 +57,12 @@ def _get_attrs(client_manager, parsed_args): if parsed_args.disable: attrs['enabled'] = False if 'project' in parsed_args and parsed_args.project is not None: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id return attrs @@ -62,7 +71,7 @@ def _get_attrs(client_manager, parsed_args): class AddNetworkFlavorToProfile(command.Command): _description = _("Add a service profile to a network flavor") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'flavor', metavar="", help=_("Network flavor (name or ID)") @@ -74,7 +83,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj_flavor = client.find_flavor( parsed_args.flavor, ignore_missing=False @@ -87,12 +96,10 @@ def take_action(self, parsed_args): ) -# TODO(dasanind): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class CreateNetworkFlavor(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create new network flavor") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar="", help=_("Name for the flavor") @@ -129,7 +136,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) attrs.update( @@ -145,7 +154,7 @@ def take_action(self, parsed_args): class DeleteNetworkFlavor(command.Command): _description = _("Delete network flavors") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -156,7 +165,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -185,7 +194,14 @@ def take_action(self, parsed_args): class ListNetworkFlavor(command.Lister): _description = _("List network flavors") - def take_action(self, parsed_args): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + pagination.add_marker_pagination_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ('id', 'name', 'is_enabled', 'service_type', 'description') @@ -197,7 +213,15 @@ def take_action(self, parsed_args): 'Description', ) - data = client.flavors() + filters = {} + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items + + data = client.flavors(**filters) return ( column_headers, ( @@ -213,7 +237,7 @@ def take_action(self, parsed_args): class RemoveNetworkFlavorFromProfile(command.Command): _description = _("Remove service profile from network flavor") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'flavor', metavar="", help=_("Network flavor (name or ID)") @@ -225,7 +249,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj_flavor = client.find_flavor( parsed_args.flavor, ignore_missing=False @@ -238,12 +262,10 @@ def take_action(self, parsed_args): ) -# TODO(dasanind): Use only the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class SetNetworkFlavor(common.NeutronCommandWithExtraArgs): _description = _("Set network flavor properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'flavor', @@ -266,7 +288,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_flavor(parsed_args.flavor, ignore_missing=False) attrs = {} @@ -287,7 +309,7 @@ def take_action(self, parsed_args): class ShowNetworkFlavor(command.ShowOne): _description = _("Display network flavor details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'flavor', @@ -296,7 +318,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_flavor(parsed_args.flavor, ignore_missing=False) display_columns, columns = _get_columns(obj) diff --git a/openstackclient/network/v2/network_flavor_profile.py b/openstackclient/network/v2/network_flavor_profile.py index eeb4ce8997..aecc176da2 100644 --- a/openstackclient/network/v2/network_flavor_profile.py +++ b/openstackclient/network/v2/network_flavor_profile.py @@ -11,19 +11,26 @@ # under the License. # +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import service_profile as _service_profile from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.network import common LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _service_profile.ServiceProfile, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { 'is_enabled': 'enabled', } @@ -34,8 +41,10 @@ def _get_columns(item): ) -def _get_attrs(client_manager, parsed_args): - attrs = {} +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} if parsed_args.description is not None: attrs['description'] = parsed_args.description if parsed_args.driver is not None: @@ -50,14 +59,12 @@ def _get_attrs(client_manager, parsed_args): return attrs -# TODO(ndahiwade): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class CreateNetworkFlavorProfile( command.ShowOne, common.NeutronCommandWithExtraArgs ): _description = _("Create new network flavor profile") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--description', @@ -92,7 +99,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) attrs.update( @@ -113,7 +122,7 @@ def take_action(self, parsed_args): class DeleteNetworkFlavorProfile(command.Command): _description = _("Delete network flavor profile") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -124,7 +133,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -154,7 +163,14 @@ def take_action(self, parsed_args): class ListNetworkFlavorProfile(command.Lister): _description = _("List network flavor profile(s)") - def take_action(self, parsed_args): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + pagination.add_marker_pagination_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( @@ -172,7 +188,16 @@ def take_action(self, parsed_args): 'Description', ) - data = client.service_profiles() + filters = {} + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items + + data = client.service_profiles(**filters) + return ( column_headers, ( @@ -185,12 +210,10 @@ def take_action(self, parsed_args): ) -# TODO(ndahiwade): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class SetNetworkFlavorProfile(common.NeutronCommandWithExtraArgs): _description = _("Set network flavor profile properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'flavor_profile', @@ -230,7 +253,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_service_profile( parsed_args.flavor_profile, ignore_missing=False @@ -246,7 +269,7 @@ def take_action(self, parsed_args): class ShowNetworkFlavorProfile(command.ShowOne): _description = _("Display network flavor profile details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'flavor_profile', @@ -255,7 +278,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_service_profile( parsed_args.flavor_profile, ignore_missing=False diff --git a/openstackclient/network/v2/network_meter.py b/openstackclient/network/v2/network_meter.py index 625b7422b6..63e3b72480 100644 --- a/openstackclient/network/v2/network_meter.py +++ b/openstackclient/network/v2/network_meter.py @@ -13,12 +13,17 @@ """Metering Label Implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import metering_label as _metering_label from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -26,7 +31,9 @@ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _metering_label.MeteringLabel, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { 'is_shared': 'shared', } @@ -36,18 +43,20 @@ def _get_columns(item): ) -def _get_attrs(client_manager, parsed_args): - attrs = {} +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} if parsed_args.description is not None: attrs['description'] = parsed_args.description if parsed_args.project is not None and 'project' in parsed_args: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id if parsed_args.share: attrs['shared'] = True @@ -59,12 +68,10 @@ def _get_attrs(client_manager, parsed_args): return attrs -# TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class CreateMeter(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create network meter") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -99,7 +106,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) attrs.update( @@ -112,12 +121,10 @@ def take_action(self, parsed_args): return (display_columns, data) -# TODO(ankur-gupta-f): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class DeleteMeter(command.Command): _description = _("Delete network meter") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -128,7 +135,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -154,7 +161,14 @@ def take_action(self, parsed_args): class ListMeter(command.Lister): _description = _("List network meters") - def take_action(self, parsed_args): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + pagination.add_marker_pagination_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( @@ -170,7 +184,15 @@ def take_action(self, parsed_args): 'Shared', ) - data = client.metering_labels() + filters = {} + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items + + data = client.metering_labels(**filters) return ( column_headers, ( @@ -186,14 +208,16 @@ def take_action(self, parsed_args): class ShowMeter(command.ShowOne): _description = _("Show network meter") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'meter', metavar='', help=_('Meter to display (name or ID)') ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_metering_label( parsed_args.meter, ignore_missing=False diff --git a/openstackclient/network/v2/network_meter_rule.py b/openstackclient/network/v2/network_meter_rule.py index c7551543ea..ba836a3e63 100644 --- a/openstackclient/network/v2/network_meter_rule.py +++ b/openstackclient/network/v2/network_meter_rule.py @@ -13,13 +13,17 @@ """Meter Rule Implementations""" +import argparse import logging -import typing as ty +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import metering_label_rule as _metering_label_rule from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -27,15 +31,19 @@ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _metering_label_rule.MeteringLabelRule, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns ) -def _get_attrs(client_manager, parsed_args): - attrs: dict[str, ty.Any] = {} +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, object]: + attrs: dict[str, object] = {} if parsed_args.exclude: attrs['excluded'] = True @@ -54,12 +62,12 @@ def _get_attrs(client_manager, parsed_args): if parsed_args.meter is not None: attrs['metering_label_id'] = parsed_args.meter if parsed_args.project is not None: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id return attrs @@ -68,7 +76,7 @@ def _get_attrs(client_manager, parsed_args): class CreateMeterRule(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a new meter rule") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -125,7 +133,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network _meter = client.find_metering_label( parsed_args.meter, ignore_missing=False @@ -145,7 +155,7 @@ def take_action(self, parsed_args): class DeleteMeterRule(command.Command): _description = _("Delete meter rule(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -157,7 +167,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -183,7 +193,14 @@ def take_action(self, parsed_args): class ListMeterRule(command.Lister): _description = _("List meter rules") - def take_action(self, parsed_args): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + pagination.add_marker_pagination_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( @@ -202,7 +219,16 @@ def take_action(self, parsed_args): 'Source IP Prefix', 'Destination IP Prefix', ) - data = client.metering_label_rules() + + filters = {} + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items + + data = client.metering_label_rules(**filters) return ( column_headers, ( @@ -218,7 +244,7 @@ def take_action(self, parsed_args): class ShowMeterRule(command.ShowOne): _description = _("Display meter rules details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'meter_rule_id', @@ -227,7 +253,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_metering_label_rule( parsed_args.meter_rule_id, ignore_missing=False diff --git a/openstackclient/network/v2/network_qos_policy.py b/openstackclient/network/v2/network_qos_policy.py index 1a96f939e2..75c6f0be91 100644 --- a/openstackclient/network/v2/network_qos_policy.py +++ b/openstackclient/network/v2/network_qos_policy.py @@ -13,13 +13,18 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from cliff import columns as cliff_columns -from osc_lib.command import command +from openstack.network.v2 import qos_policy as _qos_policy from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -27,8 +32,8 @@ LOG = logging.getLogger(__name__) -class RulesColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class RulesColumn(cliff_columns.FormattableColumn[Any]): + def human_readable(self) -> str: return '\n'.join(str(v) for v in self._value) @@ -37,7 +42,9 @@ def human_readable(self): } -def _get_columns(item): +def _get_columns( + item: _qos_policy.QoSPolicy, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { 'is_shared': 'shared', } @@ -47,7 +54,9 @@ def _get_columns(item): ) -def _get_attrs(client_manager, parsed_args): +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: attrs = {} if 'name' in parsed_args and parsed_args.name is not None: attrs['name'] = parsed_args.name @@ -66,25 +75,23 @@ def _get_attrs(client_manager, parsed_args): # NOTE(ralonsoh): 'project' parameter is defined only in create and list # commands context only. if 'project' in parsed_args and parsed_args.project is not None: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id return attrs -# TODO(abhiraut): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class CreateNetworkQosPolicy( command.ShowOne, common.NeutronCommandWithExtraArgs ): _description = _("Create a QoS policy") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar='', help=_("Name of QoS policy to create") @@ -128,7 +135,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) attrs.update( @@ -143,7 +152,7 @@ def take_action(self, parsed_args): class DeleteNetworkQosPolicy(command.Command): _description = _("Delete Qos Policy(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'policy', @@ -153,7 +162,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -179,18 +188,17 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) -# TODO(abhiraut): Use only the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class ListNetworkQosPolicy(command.Lister): _description = _("List QoS policies") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--project', metavar='', help=_( - "List QoS policies according to their project (name or ID)" + "List only QoS policies with the specified project " + "(name or ID)" ), ) identity_common.add_project_domain_option_to_parser(parser) @@ -198,16 +206,19 @@ def get_parser(self, prog_name): shared_group.add_argument( '--share', action='store_true', - help=_("List QoS policies shared between projects"), + help=_("List only QoS policies shared between projects"), ) shared_group.add_argument( '--no-share', action='store_true', - help=_("List QoS policies not shared between projects"), + help=_("List only QoS policies not shared between projects"), ) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( 'id', @@ -223,7 +234,16 @@ def take_action(self, parsed_args): 'Default', 'Project', ) + attrs = _get_attrs(self.app.client_manager, parsed_args) + + if parsed_args.marker is not None: + attrs['marker'] = parsed_args.marker + if parsed_args.limit is not None: + attrs['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + attrs['max_items'] = parsed_args.max_items + data = client.qos_policies(**attrs) return ( column_headers, @@ -238,12 +258,10 @@ def take_action(self, parsed_args): ) -# TODO(abhiraut): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class SetNetworkQosPolicy(common.NeutronCommandWithExtraArgs): _description = _("Set QoS policy properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'policy', @@ -282,7 +300,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_qos_policy(parsed_args.policy, ignore_missing=False) attrs = _get_attrs(self.app.client_manager, parsed_args) @@ -295,7 +313,7 @@ def take_action(self, parsed_args): class ShowNetworkQosPolicy(command.ShowOne): _description = _("Display QoS policy details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'policy', @@ -304,7 +322,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_qos_policy(parsed_args.policy, ignore_missing=False) display_columns, columns = _get_columns(obj) diff --git a/openstackclient/network/v2/network_qos_rule.py b/openstackclient/network/v2/network_qos_rule.py index 86f342f6ef..dee153285a 100644 --- a/openstackclient/network/v2/network_qos_rule.py +++ b/openstackclient/network/v2/network_qos_rule.py @@ -13,12 +13,22 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse +from collections.abc import Iterable, Sequence import itertools - -from osc_lib.command import command +from typing import Any, cast + +from openstack.network.v2 import ( + qos_bandwidth_limit_rule as _qos_bandwidth_limit_rule, + qos_dscp_marking_rule as _qos_dscp_marking_rule, + qos_minimum_bandwidth_rule as _qos_minimum_bandwidth_rule, + qos_minimum_packet_rate_rule as _qos_minimum_packet_rate_rule, + qos_packet_rate_limit_rule as _qos_packet_rate_limit_rule, +) from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.network import common @@ -72,14 +82,24 @@ ACTION_SHOW = 'get' -def _get_columns(item): +def _get_columns( + item: ( + _qos_bandwidth_limit_rule.QoSBandwidthLimitRule + | _qos_dscp_marking_rule.QoSDSCPMarkingRule + | _qos_minimum_bandwidth_rule.QoSMinimumBandwidthRule + | _qos_minimum_packet_rate_rule.QoSMinimumPacketRateRule + | _qos_packet_rate_limit_rule.QoSPacketRateLimitRule + ), +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'name', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns ) -def _check_type_parameters(attrs, type, is_create): +def _check_type_parameters( + attrs: dict[str, Any], type: str, is_create: bool +) -> None: req_params = MANDATORY_PARAMETERS[type] opt_params = OPTIONAL_PARAMETERS[type] type_params = req_params | opt_params @@ -102,7 +122,11 @@ def _check_type_parameters(attrs, type, is_create): raise exceptions.CommandError(msg) -def _get_attrs(network_client, parsed_args, is_create=False): +def _get_attrs( + network_client: Any, + parsed_args: argparse.Namespace, + is_create: bool = False, +) -> dict[str, Any]: attrs = {} qos = network_client.find_qos_policy( parsed_args.qos_policy, ignore_missing=False @@ -148,19 +172,19 @@ def _get_attrs(network_client, parsed_args, is_create=False): return attrs -def _rule_action_call(client, action, rule_type): +def _rule_action_call(client: Any, action: str, rule_type: str) -> Any: rule_type = rule_type.replace('-', '_') func_name = f'{action}_qos_{rule_type}_rule' return getattr(client, func_name) -def _find_rule_type(qos, rule_id): +def _find_rule_type(qos: Any, rule_id: str) -> str | None: for rule in (r for r in qos.rules if r['id'] == rule_id): - return rule['type'].replace('_', '-') + return cast(str, rule['type']).replace('_', '-') return None -def _add_rule_arguments(parser): +def _add_rule_arguments(parser: argparse.ArgumentParser) -> None: parser.add_argument( '--max-kbps', dest='max_kbps', @@ -230,7 +254,7 @@ class CreateNetworkQosRule( ): _description = _("Create new Network QoS rule") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'qos_policy', @@ -255,7 +279,9 @@ def get_parser(self, prog_name): _add_rule_arguments(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: network_client = self.app.client_manager.network try: attrs = _get_attrs(network_client, parsed_args, is_create=True) @@ -276,7 +302,7 @@ def take_action(self, parsed_args): class DeleteNetworkQosRule(command.Command): _description = _("Delete Network QoS rule") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'qos_policy', @@ -290,7 +316,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: network_client = self.app.client_manager.network rule_id = parsed_args.id try: @@ -313,7 +339,7 @@ def take_action(self, parsed_args): class ListNetworkQosRule(command.Lister): _description = _("List Network QoS rules") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'qos_policy', @@ -322,7 +348,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( 'id', @@ -359,7 +387,7 @@ def take_action(self, parsed_args): class SetNetworkQosRule(common.NeutronCommandWithExtraArgs): _description = _("Set Network QoS rule properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'qos_policy', @@ -374,7 +402,7 @@ def get_parser(self, prog_name): _add_rule_arguments(parser) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: network_client = self.app.client_manager.network try: qos = network_client.find_qos_policy( @@ -405,7 +433,7 @@ def take_action(self, parsed_args): class ShowNetworkQosRule(command.ShowOne): _description = _("Display Network QoS rule details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'qos_policy', @@ -419,7 +447,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: network_client = self.app.client_manager.network rule_id = parsed_args.id try: diff --git a/openstackclient/network/v2/network_qos_rule_type.py b/openstackclient/network/v2/network_qos_rule_type.py index fd4da84af0..a6e6c296cd 100644 --- a/openstackclient/network/v2/network_qos_rule_type.py +++ b/openstackclient/network/v2/network_qos_rule_type.py @@ -13,13 +13,21 @@ # License for the specific language governing permissions and limitations # under the License. -from osc_lib.command import command +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack.network.v2 import qos_rule_type as _qos_rule_type from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ -def _get_columns(item): +def _get_columns( + item: _qos_rule_type.QoSRuleType, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { "type": "rule_type_name", "drivers": "drivers", @@ -33,7 +41,7 @@ def _get_columns(item): class ListNetworkQosRuleType(command.Lister): _description = _("List QoS rule types") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) supported = parser.add_mutually_exclusive_group() supported.add_argument( @@ -52,19 +60,28 @@ def get_parser(self, prog_name): "List all QoS rule types implemented in Neutron QoS driver" ), ) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ('type',) column_headers = ('Type',) - args = {} + filters = {} + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items if parsed_args.all_supported: - args['all_supported'] = True + filters['all_supported'] = True elif parsed_args.all_rules: - args['all_rules'] = True - data = client.qos_rule_types(**args) + filters['all_rules'] = True + data = client.qos_rule_types(**filters) return ( column_headers, @@ -82,7 +99,7 @@ def take_action(self, parsed_args): class ShowNetworkQosRuleType(command.ShowOne): _description = _("Show details about supported QoS rule type") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'rule_type', @@ -91,7 +108,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.get_qos_rule_type(parsed_args.rule_type) display_columns, columns = _get_columns(obj) diff --git a/openstackclient/network/v2/network_rbac.py b/openstackclient/network/v2/network_rbac.py index 0df74375d8..a13ed794bd 100644 --- a/openstackclient/network/v2/network_rbac.py +++ b/openstackclient/network/v2/network_rbac.py @@ -13,12 +13,17 @@ """RBAC action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import rbac_policy as _rbac_policy from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -26,7 +31,9 @@ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _rbac_policy.RBACPolicy, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { 'target_tenant': 'target_project_id', } @@ -36,7 +43,9 @@ def _get_columns(item): ) -def _get_attrs(client_manager, parsed_args): +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: attrs = {} attrs['object_type'] = parsed_args.type attrs['action'] = parsed_args.action @@ -69,33 +78,32 @@ def _get_attrs(client_manager, parsed_args): attrs['object_id'] = object_id - identity_client = client_manager.identity + identity_client = client_manager.sdk_connection.identity if parsed_args.target_project is not None: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.target_project, parsed_args.target_project_domain, - ).id + ) elif parsed_args.target_all_projects: project_id = '*' attrs['target_tenant'] = project_id + if parsed_args.project is not None: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id return attrs -# TODO(abhiraut): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class CreateNetworkRBAC(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create network RBAC policy") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'rbac_object', @@ -166,7 +174,9 @@ def get_parser(self, prog_name): identity_common.add_project_domain_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) attrs.update( @@ -181,7 +191,7 @@ def take_action(self, parsed_args): class DeleteNetworkRBAC(command.Command): _description = _("Delete network RBAC policy(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'rbac_policy', @@ -191,7 +201,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -220,7 +230,7 @@ def take_action(self, parsed_args): class ListNetworkRBAC(command.Lister): _description = _("List network RBAC policies") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--type', @@ -234,8 +244,8 @@ def get_parser(self, prog_name): 'network', ], help=_( - 'List network RBAC policies according to ' - 'given object type ("address_group", "address_scope", ' + 'List only network RBAC policies with the specified ' + 'object type ("address_group", "address_scope", ' '"security_group", "subnetpool", "qos_policy" or ' '"network")' ), @@ -245,14 +255,17 @@ def get_parser(self, prog_name): metavar='', choices=['access_as_external', 'access_as_shared'], help=_( - 'List network RBAC policies according to given ' + 'List only network RBAC policies with the specified ' 'action ("access_as_external" or "access_as_shared")' ), ) parser.add_argument( '--target-project', metavar='', - help=_('List network RBAC policies for a specific target project'), + help=_( + 'List only network RBAC policies with the specified ' + 'target project (name or ID)' + ), ) parser.add_argument( '--long', @@ -260,9 +273,12 @@ def get_parser(self, prog_name): default=False, help=_("List additional fields in output"), ) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns: tuple[str, ...] = ( @@ -285,15 +301,23 @@ def take_action(self, parsed_args): if parsed_args.action is not None: query['action'] = parsed_args.action if parsed_args.target_project is not None: - project_id = "*" - if parsed_args.target_project != "*": - identity_client = self.app.client_manager.identity - project_id = identity_common.find_project( + identity_client = ( + self.app.client_manager.sdk_connection.identity + ) + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.target_project, - ).id + ) + else: + project_id = "*" query['target_project_id'] = project_id + if parsed_args.marker is not None: + query['marker'] = parsed_args.marker + if parsed_args.limit is not None: + query['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + query['max_items'] = parsed_args.max_items data = client.rbac_policies(**query) @@ -309,12 +333,10 @@ def take_action(self, parsed_args): ) -# TODO(abhiraut): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class SetNetworkRBAC(common.NeutronCommandWithExtraArgs): _description = _("Set network RBAC policy properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'rbac_policy', @@ -340,19 +362,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_rbac_policy( parsed_args.rbac_policy, ignore_missing=False ) attrs = {} if parsed_args.target_project: - identity_client = self.app.client_manager.identity - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.target_project, parsed_args.target_project_domain, - ).id + ) attrs['target_tenant'] = project_id attrs.update( self._parse_extra_properties(parsed_args.extra_properties) @@ -363,7 +385,7 @@ def take_action(self, parsed_args): class ShowNetworkRBAC(command.ShowOne): _description = _("Display network RBAC policy details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'rbac_policy', @@ -372,7 +394,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_rbac_policy( parsed_args.rbac_policy, ignore_missing=False diff --git a/openstackclient/network/v2/network_segment.py b/openstackclient/network/v2/network_segment.py index 5e40108cab..35c7c293dd 100644 --- a/openstackclient/network/v2/network_segment.py +++ b/openstackclient/network/v2/network_segment.py @@ -13,19 +13,26 @@ """Network segment action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import segment as _segment from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.network import common LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _segment.Segment, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns @@ -37,7 +44,7 @@ class CreateNetworkSegment( ): _description = _("Create new network segment") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar='', help=_('New network segment name') @@ -81,7 +88,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = {} attrs['name'] = parsed_args.name @@ -107,7 +116,7 @@ def take_action(self, parsed_args): class DeleteNetworkSegment(command.Command): _description = _("Delete network segment(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network_segment', @@ -117,7 +126,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -148,7 +157,7 @@ def take_action(self, parsed_args): class ListNetworkSegment(command.Lister): _description = _("List network segments") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -160,13 +169,16 @@ def get_parser(self, prog_name): '--network', metavar='', help=_( - 'List network segments that belong to this ' + 'List only network segments associated with the specified ' 'network (name or ID)' ), ) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: network_client = self.app.client_manager.network filters = {} @@ -175,6 +187,13 @@ def take_action(self, parsed_args): parsed_args.network, ignore_missing=False ) filters = {'network_id': _network.id} + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items + data = network_client.segments(**filters) headers: tuple[str, ...] = ( @@ -211,7 +230,7 @@ def take_action(self, parsed_args): class SetNetworkSegment(common.NeutronCommandWithExtraArgs): _description = _("Set network segment properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network_segment', @@ -230,7 +249,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_segment( parsed_args.network_segment, ignore_missing=False @@ -249,7 +268,7 @@ def take_action(self, parsed_args): class ShowNetworkSegment(command.ShowOne): _description = _("Display network segment details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network_segment', @@ -258,7 +277,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_segment( parsed_args.network_segment, ignore_missing=False diff --git a/openstackclient/network/v2/network_segment_range.py b/openstackclient/network/v2/network_segment_range.py index e2716f13f5..7332816187 100644 --- a/openstackclient/network/v2/network_segment_range.py +++ b/openstackclient/network/v2/network_segment_range.py @@ -16,30 +16,35 @@ """Network segment action implementations""" +import argparse +from collections.abc import Iterable, Sequence import itertools import logging -import typing as ty +from typing import Any -from osc_lib.command import command +from openstack.network.v2 import network_segment_range as _segment_range from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common - LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _segment_range.NetworkSegmentRange, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'tenant_id'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns ) -def _get_ranges(item): +def _get_ranges(item: Any) -> Iterable[str]: item = sorted([int(i) for i in item]) for a, b in itertools.groupby(enumerate(item), lambda xy: xy[1] - xy[0]): c = list(b) @@ -48,18 +53,24 @@ def _get_ranges(item): ) -def _hack_tuple_value_update_by_index(tup, index, value): +def _hack_tuple_value_update_by_index( + tup: tuple[Any, ...], index: int, value: Any +) -> tuple[Any, ...]: lot = list(tup) lot[index] = value return tuple(lot) -def _is_prop_empty(columns, props, prop_name): +def _is_prop_empty( + columns: tuple[str, ...], props: tuple[Any, ...], prop_name: str +) -> bool: return True if not props[columns.index(prop_name)] else False -def _exchange_dict_keys_with_values(orig_dict): - updated_dict: dict[str, ty.Any] = {} +def _exchange_dict_keys_with_values( + orig_dict: dict[Any, Any], +) -> dict[str, Any]: + updated_dict: dict[str, Any] = {} for k, v in orig_dict.items(): k = [k] if not updated_dict.get(v): @@ -69,7 +80,9 @@ def _exchange_dict_keys_with_values(orig_dict): return updated_dict -def _update_available_from_props(columns, props): +def _update_available_from_props( + columns: tuple[str, ...], props: tuple[Any, ...] +) -> tuple[Any, ...]: index_available = columns.index('available') props = _hack_tuple_value_update_by_index( props, index_available, list(_get_ranges(props[index_available])) @@ -77,7 +90,9 @@ def _update_available_from_props(columns, props): return props -def _update_used_from_props(columns, props): +def _update_used_from_props( + columns: tuple[str, ...], props: tuple[Any, ...] +) -> tuple[Any, ...]: index_used = columns.index('used') updated_used = _exchange_dict_keys_with_values(props[index_used]) for k, v in updated_used.items(): @@ -86,7 +101,9 @@ def _update_used_from_props(columns, props): return props -def _update_additional_fields_from_props(columns, props): +def _update_additional_fields_from_props( + columns: tuple[str, ...], props: tuple[Any, ...] +) -> tuple[Any, ...]: props = _update_available_from_props(columns, props) props = _update_used_from_props(columns, props) return props @@ -97,7 +114,7 @@ class CreateNetworkSegmentRange( ): _description = _("Create new network segment range") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) shared_group = parser.add_mutually_exclusive_group() shared_group.add_argument( @@ -170,7 +187,9 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: network_client = self.app.client_manager.network try: # Verify that the extension exists. @@ -184,8 +203,6 @@ def take_action(self, parsed_args): ) % {'e': e} raise exceptions.CommandError(msg) - identity_client = self.app.client_manager.identity - if not parsed_args.private and parsed_args.project: msg = _("--project is only allowed with --private") raise exceptions.CommandError(msg) @@ -212,11 +229,12 @@ def take_action(self, parsed_args): attrs['name'] = parsed_args.name if parsed_args.project: - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) if project_id: attrs['project_id'] = project_id else: @@ -251,7 +269,7 @@ def take_action(self, parsed_args): class DeleteNetworkSegmentRange(command.Command): _description = _("Delete network segment range(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network_segment_range', @@ -261,7 +279,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: network_client = self.app.client_manager.network try: # Verify that the extension exists. @@ -304,7 +322,7 @@ def take_action(self, parsed_args): class ListNetworkSegmentRange(command.Lister): _description = _("List network segment ranges") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -315,29 +333,39 @@ def get_parser(self, prog_name): used_group.add_argument( '--used', action='store_true', - help=_('List network segment ranges that have segments in use'), + help=_( + 'List only network segment ranges that have segments in use' + ), ) used_group.add_argument( '--unused', action='store_true', help=_( - 'List network segment ranges that have segments not in use' + 'List only network segment ranges that have segments ' + 'not in use' ), ) available_group = parser.add_mutually_exclusive_group() available_group.add_argument( '--available', action='store_true', - help=_('List network segment ranges that have available segments'), + help=_( + 'List only network segment ranges that have available segments' + ), ) available_group.add_argument( '--unavailable', action='store_true', - help=_('List network segment ranges without available segments'), + help=_( + 'List only network segment ranges without available segments' + ), ) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], tuple[Any, ...]]: network_client = self.app.client_manager.network try: # Verify that the extension exists. @@ -351,7 +379,15 @@ def take_action(self, parsed_args): ) % {'e': e} raise exceptions.CommandError(msg) - data = network_client.network_segment_ranges() + filters = {} + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items + + data = network_client.network_segment_ranges(**filters) headers: tuple[str, ...] = ( 'ID', @@ -395,18 +431,25 @@ def take_action(self, parsed_args): 'available', ) - display_props: tuple[str, ...] = tuple() + display_props: tuple[Any, ...] = tuple() for s in data: props = utils.get_item_properties(s, columns) if ( - parsed_args.available - and _is_prop_empty(columns, props, 'available') - or parsed_args.unavailable - and not _is_prop_empty(columns, props, 'available') - or parsed_args.used - and _is_prop_empty(columns, props, 'used') - or parsed_args.unused - and not _is_prop_empty(columns, props, 'used') + ( + parsed_args.available + and _is_prop_empty(columns, props, 'available') + ) + or ( + parsed_args.unavailable + and not _is_prop_empty(columns, props, 'available') + ) + or ( + parsed_args.used and _is_prop_empty(columns, props, 'used') + ) + or ( + parsed_args.unused + and not _is_prop_empty(columns, props, 'used') + ) ): continue if parsed_args.long: @@ -419,7 +462,7 @@ def take_action(self, parsed_args): class SetNetworkSegmentRange(common.NeutronCommandWithExtraArgs): _description = _("Set network segment range properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network_segment_range', @@ -445,7 +488,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: network_client = self.app.client_manager.network try: # Verify that the extension exists. @@ -483,7 +526,7 @@ def take_action(self, parsed_args): class ShowNetworkSegmentRange(command.ShowOne): _description = _("Display network segment range details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'network_segment_range', @@ -492,7 +535,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: network_client = self.app.client_manager.network try: # Verify that the extension exists. diff --git a/openstackclient/network/v2/network_service_provider.py b/openstackclient/network/v2/network_service_provider.py index f743bfa6e2..8cd5115ef0 100644 --- a/openstackclient/network/v2/network_service_provider.py +++ b/openstackclient/network/v2/network_service_provider.py @@ -13,16 +13,22 @@ """Network Service Providers Implementation""" -from osc_lib.command import command +import argparse +from collections.abc import Iterable +from typing import Any + from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ class ListNetworkServiceProvider(command.Lister): _description = _("List Service Providers") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns = ( diff --git a/openstackclient/network/v2/network_trunk.py b/openstackclient/network/v2/network_trunk.py index f8f1c5bcbb..c644e88bc8 100644 --- a/openstackclient/network/v2/network_trunk.py +++ b/openstackclient/network/v2/network_trunk.py @@ -16,18 +16,25 @@ """Network trunk and subports action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging -import typing as ty +from typing import Any from cliff import columns as cliff_columns +from openstack.identity import v2 as identity_v2 +from openstack.identity import v3 as identity_v3 +from openstack.network import v2 as network_v2 +from openstack.network.v2 import trunk as _trunk from osc_lib.cli import format_columns from osc_lib.cli import identity as identity_utils from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils as osc_utils +from openstackclient import command from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) @@ -36,15 +43,15 @@ SUB_PORTS = 'sub_ports' -class AdminStateColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class AdminStateColumn(cliff_columns.FormattableColumn[bool]): + def human_readable(self) -> str: return 'UP' if self._value else 'DOWN' class CreateNetworkTrunk(command.ShowOne): """Create a network trunk for a given project""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar='', help=_("Name of the trunk to create") @@ -87,10 +94,16 @@ def get_parser(self, prog_name): identity_utils.add_project_owner_option_to_parser(parser) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.network - attrs = _get_attrs_for_trunk(self.app.client_manager, parsed_args) - obj = client.create_trunk(**attrs) + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + network_client = self.app.client_manager.network + identity_client = self.app.client_manager.sdk_connection.identity + + attrs = _get_attrs_for_trunk( + network_client, identity_client, parsed_args + ) + obj = network_client.create_trunk(**attrs) display_columns, columns = _get_columns(obj) data = osc_utils.get_dict_properties( obj, columns, formatters=_formatters @@ -101,7 +114,7 @@ def take_action(self, parsed_args): class DeleteNetworkTrunk(command.Command): """Delete a given network trunk""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'trunk', @@ -111,13 +124,16 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.network + def take_action(self, parsed_args: argparse.Namespace) -> None: + network_client = self.app.client_manager.network result = 0 for trunk in parsed_args.trunk: try: - trunk_id = client.find_trunk(trunk).id - client.delete_trunk(trunk_id) + trunk_id = network_client.find_trunk( + trunk, + ignore_missing=False, + ).id + network_client.delete_trunk(trunk_id) except Exception as e: result += 1 LOG.error( @@ -139,7 +155,7 @@ def take_action(self, parsed_args): class ListNetworkTrunk(command.Lister): """List all network trunks""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -149,9 +165,11 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.network - data = client.trunks() + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + network_client = self.app.client_manager.network + data = network_client.trunks() headers: tuple[str, ...] = ('ID', 'Name', 'Parent Port', 'Description') columns: tuple[str, ...] = ('id', 'name', 'port_id', 'description') if parsed_args.long: @@ -178,7 +196,7 @@ def take_action(self, parsed_args): class SetNetworkTrunk(command.Command): """Set network trunk properties""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'trunk', metavar="", help=_("Trunk to modify (name or ID)") @@ -214,12 +232,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.network - trunk_id = client.find_trunk(parsed_args.trunk) - attrs = _get_attrs_for_trunk(self.app.client_manager, parsed_args) + def take_action(self, parsed_args: argparse.Namespace) -> None: + network_client = self.app.client_manager.network + identity_client = self.app.client_manager.sdk_connection.identity + + trunk_id = network_client.find_trunk( + parsed_args.trunk, + ignore_missing=False, + ) + attrs = _get_attrs_for_trunk( + network_client, identity_client, parsed_args + ) try: - client.update_trunk(trunk_id, **attrs) + network_client.update_trunk(trunk_id, **attrs) except Exception as e: msg = _("Failed to set trunk '%(t)s': %(e)s") % { 't': parsed_args.trunk, @@ -228,10 +253,10 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) if parsed_args.set_subports: subport_attrs = _get_attrs_for_subports( - self.app.client_manager, parsed_args + network_client, parsed_args ) try: - client.add_trunk_subports(trunk_id, subport_attrs) + network_client.add_trunk_subports(trunk_id, subport_attrs) except Exception as e: msg = _("Failed to add subports to trunk '%(t)s': %(e)s") % { 't': parsed_args.trunk, @@ -243,17 +268,22 @@ def take_action(self, parsed_args): class ShowNetworkTrunk(command.ShowOne): """Show information of a given network trunk""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'trunk', metavar="", help=_("Trunk to display (name or ID)") ) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.network - trunk_id = client.find_trunk(parsed_args.trunk).id - obj = client.get_trunk(trunk_id) + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + network_client = self.app.client_manager.network + trunk_id = network_client.find_trunk( + parsed_args.trunk, + ignore_missing=False, + ).id + obj = network_client.get_trunk(trunk_id) display_columns, columns = _get_columns(obj) data = osc_utils.get_dict_properties( obj, columns, formatters=_formatters @@ -264,20 +294,25 @@ def take_action(self, parsed_args): class ListNetworkSubport(command.Lister): """List all subports for a given network trunk""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--trunk', required=True, metavar="", - help=_("List subports belonging to this trunk (name or ID)"), + help=_("List only subports belonging to this trunk (name or ID)"), ) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.network - trunk_id = client.find_trunk(parsed_args.trunk) - data = client.get_trunk_subports(trunk_id) + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + network_client = self.app.client_manager.network + trunk_id = network_client.find_trunk( + parsed_args.trunk, + ignore_missing=False, + ) + data = network_client.get_trunk_subports(trunk_id) headers: tuple[str, ...] = ( 'Port', 'Segmentation Type', @@ -303,7 +338,7 @@ def take_action(self, parsed_args): class UnsetNetworkTrunk(command.Command): """Unset subports from a given network trunk""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'trunk', @@ -323,11 +358,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.network - attrs = _get_attrs_for_subports(self.app.client_manager, parsed_args) - trunk_id = client.find_trunk(parsed_args.trunk) - client.delete_trunk_subports(trunk_id, attrs) + def take_action(self, parsed_args: argparse.Namespace) -> None: + network_client = self.app.client_manager.network + attrs = _get_attrs_for_subports(network_client, parsed_args) + trunk_id = network_client.find_trunk( + parsed_args.trunk, + ignore_missing=False, + ) + network_client.delete_trunk_subports(trunk_id, attrs) _formatters = { @@ -336,15 +374,21 @@ def take_action(self, parsed_args): } -def _get_columns(item): +def _get_columns( + item: _trunk.Trunk, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'tenant_id'] return osc_utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns ) -def _get_attrs_for_trunk(client_manager, parsed_args): - attrs: dict[str, ty.Any] = {} +def _get_attrs_for_trunk( + network_client: network_v2.Proxy, + identity_client: identity_v2.Proxy | identity_v3.Proxy, + parsed_args: argparse.Namespace, +) -> dict[str, Any]: + attrs: dict[str, Any] = {} if parsed_args.name is not None: attrs['name'] = str(parsed_args.name) if parsed_args.description is not None: @@ -354,34 +398,39 @@ def _get_attrs_for_trunk(client_manager, parsed_args): if parsed_args.disable: attrs['admin_state_up'] = False if 'parent_port' in parsed_args and parsed_args.parent_port is not None: - port_id = client_manager.network.find_port(parsed_args.parent_port)[ - 'id' - ] + port_id = network_client.find_port( + parsed_args.parent_port, + ignore_missing=False, + ).id attrs['port_id'] = port_id if 'add_subports' in parsed_args and parsed_args.add_subports is not None: attrs[SUB_PORTS] = _format_subports( - client_manager, parsed_args.add_subports + network_client, parsed_args.add_subports ) # "trunk set" command doesn't support setting project. if 'project' in parsed_args and parsed_args.project is not None: - identity_client = client_manager.identity - project_id = identity_utils.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id - attrs['tenant_id'] = project_id + ) + attrs['project_id'] = project_id return attrs -def _format_subports(client_manager, subports): +def _format_subports( + network_client: Any, subports: list[dict[str, Any]] +) -> list[dict[str, Any]]: attrs = [] for subport in subports: subport_attrs = {} if subport.get('port'): - port_id = client_manager.network.find_port(subport['port'])['id'] + port_id = network_client.find_port( + subport['port'], + ignore_missing=False, + ).id subport_attrs['port_id'] = port_id if subport.get('segmentation-id'): try: @@ -400,21 +449,22 @@ def _format_subports(client_manager, subports): return attrs -def _get_attrs_for_subports(client_manager, parsed_args): +def _get_attrs_for_subports( + network_client: Any, parsed_args: argparse.Namespace +) -> list[dict[str, Any]]: attrs = [] if 'set_subports' in parsed_args and parsed_args.set_subports is not None: - attrs = _format_subports(client_manager, parsed_args.set_subports) + attrs = _format_subports(network_client, parsed_args.set_subports) if ( 'unset_subports' in parsed_args and parsed_args.unset_subports is not None ): subports_list = [] for subport in parsed_args.unset_subports: - port_id = client_manager.network.find_port(subport)['id'] + port_id = network_client.find_port( + subport, + ignore_missing=False, + )['id'] subports_list.append({'port_id': port_id}) attrs = subports_list return attrs - - -def _get_id(client, id_or_name, resource): - return client.find_resource(resource, str(id_or_name))['id'] diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index 25dfdbbe83..930904f740 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -14,19 +14,22 @@ """Port action implementations""" import argparse +from collections.abc import Iterable, Sequence import copy import json import logging -import typing as ty +from typing import Any from cliff import columns as cliff_columns +from openstack.network.v2 import port as _port from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils from osc_lib.utils import tags as _tag +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -34,23 +37,23 @@ LOG = logging.getLogger(__name__) -class AdminStateColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class AdminStateColumn(cliff_columns.FormattableColumn[bool]): + def human_readable(self) -> str: return 'UP' if self._value else 'DOWN' class SubPortColumn(format_columns.ListDictColumn): - _value: ty.Any + _value: Any - def _retrieve_subports(self): + def _retrieve_subports(self) -> None: if isinstance(self._value, dict): self._value = self._value['sub_ports'] - def human_readable(self): + def human_readable(self) -> str: self._retrieve_subports() return super().human_readable() - def machine_readable(self): + def machine_readable(self) -> Any: self._retrieve_subports() return super().machine_readable() @@ -73,7 +76,9 @@ def machine_readable(self): _list_formatters.update({'trunk_details': SubPortColumn}) -def _get_columns(item): +def _get_columns( + item: _port.Port, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_data_mapping = { 'admin_state_up': 'is_admin_state_up', 'allowed_address_pairs': 'allowed_address_pairs', @@ -104,6 +109,8 @@ def _get_columns(item): 'port_security_enabled': 'is_port_security_enabled', 'project_id': 'project_id', 'propagate_uplink_status': 'propagate_uplink_status', + 'pvlan_type': 'pvlan_type', + 'pvlan_community': 'pvlan_community', 'resource_request': 'resource_request', 'revision_number': 'revision_number', 'qos_network_policy_id': 'qos_network_policy_id', @@ -127,7 +134,13 @@ class JSONKeyValueAction(argparse.Action): Ensures that ``dest`` is a dict """ - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Any, + option_string: str | None = None, + ) -> None: # Make sure we have an empty dict rather than None if getattr(namespace, self.dest, None) is None: setattr(namespace, self.dest, {}) @@ -148,8 +161,10 @@ def __call__(self, parser, namespace, values, option_string=None): raise argparse.ArgumentError(self, msg) -def _get_attrs(client_manager, parsed_args): - attrs = {} +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} if parsed_args.description is not None: attrs['description'] = parsed_args.description @@ -180,14 +195,12 @@ def _get_attrs(client_manager, parsed_args): if 'network' in parsed_args and parsed_args.network is not None: attrs['network_id'] = parsed_args.network if 'project' in parsed_args and parsed_args.project is not None: - # TODO(singhj): since 'project' logic is common among - # router, network, port etc., maybe move it to a common file. - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id if parsed_args.disable_port_security: @@ -247,10 +260,51 @@ def _get_attrs(client_manager, parsed_args): if parsed_args.trusted: attrs['trusted'] = True + if 'pvlan_type' in parsed_args and parsed_args.pvlan_type is not None: + attrs['pvlan_type'] = parsed_args.pvlan_type + if ( + 'pvlan_community' in parsed_args + and parsed_args.pvlan_community is not None + ): + attrs['pvlan_community'] = parsed_args.pvlan_community + + _validate_pvlan_port(attrs) + return attrs -def _prepare_fixed_ips(client_manager, parsed_args): +def _validate_pvlan_port(attrs: dict[str, Any]) -> None: + if (attrs.get('pvlan_type') or attrs.get('pvlan_community')) and attrs.get( + 'port_security_enabled' + ) is False: + msg = _( + "PVLAN attributes cannot be set when port security is disabled." + ) + raise exceptions.CommandError(msg) + + if attrs.get('pvlan_type') == 'community' and not attrs.get( + 'pvlan_community' + ): + msg = _( + "--pvlan-community is required when --pvlan-type is 'community'." + ) + raise exceptions.CommandError(msg) + + +def _validate_pvlan_network_port(attrs: dict[str, Any], network: Any) -> None: + if not (attrs.get('pvlan_type') or attrs.get('pvlan_community')): + return + if not network.pvlan: + msg = _( + "PVLAN attributes cannot be set on a port whose " + "network does not have PVLAN enabled." + ) + raise exceptions.CommandError(msg) + + +def _prepare_fixed_ips( + client_manager: Any, parsed_args: argparse.Namespace +) -> None: """Fix and properly format fixed_ip option. Appropriately convert any subnet names to their respective ids. @@ -281,7 +335,9 @@ def _prepare_fixed_ips(client_manager, parsed_args): parsed_args.fixed_ip = ips -def _prepare_filter_fixed_ips(client_manager, parsed_args): +def _prepare_filter_fixed_ips( + client_manager: Any, parsed_args: argparse.Namespace +) -> list[str]: """Fix and properly format fixed_ip option for filtering. Appropriately convert any subnet names to their respective ids. @@ -308,7 +364,9 @@ def _prepare_filter_fixed_ips(client_manager, parsed_args): return ips -def _add_updatable_args(parser, create=False): +def _add_updatable_args( + parser: argparse.ArgumentParser, create: bool = False +) -> None: parser.add_argument( '--description', metavar='', @@ -430,11 +488,31 @@ def _add_updatable_args(parser, create=False): "which expect it in this dictionary (for example, Nova)." ), ) + parser.add_argument( + '--pvlan-type', + metavar='', + choices=['promiscuous', 'isolated', 'community'], + dest='pvlan_type', + help=_( + "Set Private VLAN type for this port. Requires PVLAN service " + "plugin. Default: promiscuous." + ), + ) + parser.add_argument( + '--pvlan-community', + metavar='', + dest='pvlan_community', + help=_( + "Set PVLAN community name for this port. " + "Only applies when pvlan-type is 'community'. " + "Requires PVLAN service plugin. Default: None." + ), + ) -# TODO(abhiraut): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. -def _convert_address_pairs(parsed_args): +def _convert_address_pairs( + parsed_args: argparse.Namespace, +) -> list[dict[str, Any]]: ops = [] for opt in parsed_args.allowed_address_pairs: addr = {} @@ -445,7 +523,9 @@ def _convert_address_pairs(parsed_args): return ops -def _convert_extra_dhcp_options(parsed_args): +def _convert_extra_dhcp_options( + parsed_args: argparse.Namespace, +) -> list[dict[str, Any]]: dhcp_options = [] for opt in parsed_args.extra_dhcp_options: option = {} @@ -460,7 +540,7 @@ def _convert_extra_dhcp_options(parsed_args): # When we have multiple hints, we'll need to refactor this to allow # arbitrary combinations. But until then let's have it as simple as possible. -def _validate_port_hints(hints): +def _validate_port_hints(hints: dict[str, Any]) -> None: if hints not in ( {}, # by hint alias @@ -477,7 +557,7 @@ def _validate_port_hints(hints): # When we have multiple hints, we'll need to refactor this to expand aliases # without losing other hints. But until then let's have it as simple as # possible. -def _expand_port_hint_aliases(hints): +def _expand_port_hint_aliases(hints: dict[str, Any]) -> dict[str, Any]: if hints == {'ovs-tx-steering': 'thread'}: return {'openvswitch': {'other_config': {'tx-steering': 'thread'}}} elif hints == {'ovs-tx-steering': 'hash'}: @@ -489,7 +569,7 @@ def _expand_port_hint_aliases(hints): class CreatePort(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a new port") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -635,12 +715,14 @@ def get_parser(self, prog_name): _tag.add_tag_option_to_parser_for_create(parser, _('port')) return parser - def take_action(self, parsed_args): - client = self.app.client_manager.network - _network = client.find_network( + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + network_client = self.app.client_manager.network + network = network_client.find_network( parsed_args.network, ignore_missing=False ) - parsed_args.network = _network.id + parsed_args.network = network.id _prepare_fixed_ips(self.app.client_manager, parsed_args) attrs = _get_attrs(self.app.client_manager, parsed_args) @@ -654,7 +736,7 @@ def take_action(self, parsed_args): if parsed_args.security_groups is not None: attrs['security_group_ids'] = [ - client.find_security_group(sg, ignore_missing=False).id + network_client.find_security_group(sg, ignore_missing=False).id for sg in parsed_args.security_groups ] @@ -667,7 +749,7 @@ def take_action(self, parsed_args): attrs["extra_dhcp_opts"] = _convert_extra_dhcp_options(parsed_args) if parsed_args.qos_policy: - attrs['qos_policy_id'] = client.find_qos_policy( + attrs['qos_policy_id'] = network_client.find_qos_policy( parsed_args.qos_policy, ignore_missing=False ).id @@ -675,7 +757,9 @@ def take_action(self, parsed_args): _validate_port_hints(parsed_args.hint) expanded_hints = _expand_port_hint_aliases(parsed_args.hint) try: - client.find_extension('port-hints', ignore_missing=False) + network_client.find_extension( + 'port-hints', ignore_missing=False + ) except Exception as e: msg = _('Not supported by Network API: %(e)s') % {'e': e} raise exceptions.CommandError(msg) @@ -686,7 +770,7 @@ def take_action(self, parsed_args): in expanded_hints['openvswitch']['other_config'] ): try: - client.find_extension( + network_client.find_extension( 'port-hint-ovs-tx-steering', ignore_missing=False ) except Exception as e: @@ -695,7 +779,9 @@ def take_action(self, parsed_args): attrs['hints'] = expanded_hints set_tags_in_post = bool( - client.find_extension('tag-ports-during-bulk-creation') + network_client.find_extension( + 'tag-ports-during-bulk-creation', ignore_missing=True + ) ) if set_tags_in_post: if parsed_args.no_tag: @@ -707,14 +793,14 @@ def take_action(self, parsed_args): self._parse_extra_properties(parsed_args.extra_properties) ) - with common.check_missing_extension_if_error( - self.app.client_manager.network, attrs - ): - obj = client.create_port(**attrs) + _validate_pvlan_network_port(attrs, network) + + with common.check_missing_extension_if_error(network_client, attrs): + obj = network_client.create_port(**attrs) if not set_tags_in_post: # tags cannot be set when created, so tags need to be set later. - _tag.update_tags_for_set(client, obj, parsed_args) + _tag.update_tags_for_set(network_client, obj, parsed_args) display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns, formatters=_formatters) @@ -725,7 +811,7 @@ def take_action(self, parsed_args): class DeletePort(command.Command): _description = _("Delete port(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'port', @@ -735,7 +821,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -762,12 +848,10 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) -# TODO(abhiraut): Use only the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class ListPort(command.Lister): _description = _("List ports") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--device-owner', @@ -808,7 +892,7 @@ def get_parser(self, prog_name): parser.add_argument( '--mac-address', metavar='', - help=_("List only ports with this MAC address"), + help=_("List only ports with the specified MAC address"), ) parser.add_argument( '--long', @@ -819,12 +903,35 @@ def get_parser(self, prog_name): parser.add_argument( '--project', metavar='', - help=_("List ports according to their project (name or ID)"), + help=_("List only ports with the specified project (name or ID)"), + ) + parser.add_argument( + '--pvlan-type', + metavar='', + help=_("List only ports with the specified PVLAN type"), + ) + parser.add_argument( + '--pvlan-community', + metavar='', + help=_("List only ports within a specified PVLAN community"), + ) + pvlan_group = parser.add_mutually_exclusive_group() + pvlan_group.add_argument( + '--pvlan', + action='store_true', + default=False, + help=_("List only ports with PVLAN enabled"), + ) + pvlan_group.add_argument( + '--no-pvlan', + action='store_true', + default=False, + help=_("List only ports with PVLAN disabled"), ) parser.add_argument( '--name', metavar='', - help=_("List ports according to their name"), + help=_("List only ports with the specified name"), ) parser.add_argument( '--security-group', @@ -842,7 +949,7 @@ def get_parser(self, prog_name): metavar='', choices=('ACTIVE', 'BUILD', 'DOWN', 'ERROR'), help=_( - "List ports according to their status " + "List only ports with the specified status " "('ACTIVE', 'BUILD', 'DOWN', 'ERROR')" ), ) @@ -859,15 +966,17 @@ def get_parser(self, prog_name): "Desired IP and/or subnet for filtering ports " "(name or ID): subnet=,ip-address=," "ip-substring= " - "(repeat option to set multiple fixed IP addresses)" + "(repeat option to filter multiple fixed IP addresses)" ), ) _tag.add_tag_filtering_option_to_parser(parser, _('ports')) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: network_client = self.app.client_manager.network - identity_client = self.app.client_manager.identity columns = [ 'id', @@ -885,6 +994,14 @@ def take_action(self, parsed_args): ] filters = {} + if ( + parsed_args.pvlan + or parsed_args.no_pvlan + or parsed_args.pvlan_type is not None + or parsed_args.pvlan_community is not None + ): + columns.extend(['pvlan_type', 'pvlan_community']) + column_headers.extend(['PVLAN Type', 'PVLAN Community']) if parsed_args.long: columns.extend( ['security_groups', 'device_owner', 'tags', 'trunk_details'] @@ -892,7 +1009,6 @@ def take_action(self, parsed_args): column_headers.extend( ['Security Groups', 'Device Owner', 'Tags', 'Trunk subports'] ) - if parsed_args.device_owner is not None: filters['device_owner'] = parsed_args.device_owner if parsed_args.device_id is not None: @@ -921,11 +1037,12 @@ def take_action(self, parsed_args): if parsed_args.status: filters['status'] = parsed_args.status if parsed_args.project: - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) filters['project_id'] = project_id if parsed_args.name: filters['name'] = parsed_args.name @@ -935,11 +1052,26 @@ def take_action(self, parsed_args): ) if parsed_args.security_groups: filters['security_group_ids'] = parsed_args.security_groups + if parsed_args.pvlan_type is not None: + filters['pvlan_type'] = parsed_args.pvlan_type + if parsed_args.pvlan_community is not None: + filters['pvlan_community'] = parsed_args.pvlan_community + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items _tag.get_tag_filtering_args(parsed_args, filters) data = network_client.ports(fields=columns, **filters) + if parsed_args.pvlan: + data = (p for p in data if p.pvlan_type is not None) + elif parsed_args.no_pvlan: + data = (p for p in data if p.pvlan_type is None) + if parsed_args.long: columns = [ 'security_group_ids' if item == 'security_groups' else item @@ -959,12 +1091,10 @@ def take_action(self, parsed_args): ) -# TODO(abhiraut): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class SetPort(common.NeutronCommandWithExtraArgs): _description = _("Set port properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) _add_updatable_args(parser) admin_group = parser.add_mutually_exclusive_group() @@ -1118,7 +1248,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network _prepare_fixed_ips(self.app.client_manager, parsed_args) @@ -1207,6 +1337,10 @@ def take_action(self, parsed_args): self._parse_extra_properties(parsed_args.extra_properties) ) + if attrs.get('pvlan_type') or attrs.get('pvlan_community'): + network = client.find_network(obj.network_id, ignore_missing=False) + _validate_pvlan_network_port(attrs, network) + if attrs: with common.check_missing_extension_if_error( self.app.client_manager.network, attrs @@ -1220,14 +1354,16 @@ def take_action(self, parsed_args): class ShowPort(command.ShowOne): _description = _("Display port details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'port', metavar="", help=_("Port to display (name or ID)") ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_port(parsed_args.port, ignore_missing=False) display_columns, columns = _get_columns(obj) @@ -1235,12 +1371,10 @@ def take_action(self, parsed_args): return (display_columns, data) -# TODO(abhiraut): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class UnsetPort(common.NeutronUnsetCommandWithExtraArgs): _description = _("Unset port properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--fixed-ip', @@ -1316,6 +1450,25 @@ def get_parser(self, prog_name): default=False, help=_("Clear hints for the port"), ) + parser.add_argument( + '--device', + action='store_true', + default=False, + help=_("Clear device ID for the port."), + ) + parser.add_argument( + '--device-owner', + action='store_true', + default=False, + help=_("Clear device owner for the port."), + ) + parser.add_argument( + '--pvlan-community', + action='store_true', + default=False, + dest='pvlan_community', + help=_("Clear PVLAN community name for the port."), + ) _tag.add_tag_option_to_parser_for_unset(parser, _('port')) parser.add_argument( 'port', @@ -1325,7 +1478,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_port(parsed_args.port, ignore_missing=False) # SDK ignores update() if it receives a modified obj and attrs @@ -1382,6 +1535,12 @@ def take_action(self, parsed_args): attrs['binding:host_id'] = None if parsed_args.hints: attrs['hints'] = None + if parsed_args.device: + attrs['device_id'] = '' + if parsed_args.device_owner: + attrs['device_owner'] = '' + if parsed_args.pvlan_community: + attrs['pvlan_community'] = None attrs.update( self._parse_extra_properties(parsed_args.extra_properties) diff --git a/openstackclient/network/v2/router.py b/openstackclient/network/v2/router.py index 67e3de06fc..8c68d5a839 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -14,20 +14,23 @@ """Router action implementations""" import argparse +from collections.abc import Iterable, Sequence import collections import copy import json import logging -import typing as ty +from typing import Any from cliff import columns as cliff_columns +from openstack.network.v2 import router as _router from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils from osc_lib.utils import tags as _tag +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -35,26 +38,26 @@ LOG = logging.getLogger(__name__) -class AdminStateColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class AdminStateColumn(cliff_columns.FormattableColumn[bool]): + def human_readable(self) -> str: return 'UP' if self._value else 'DOWN' -class RouterInfoColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class RouterInfoColumn(cliff_columns.FormattableColumn[Any]): + def human_readable(self) -> str: try: return json.dumps(self._value) except (TypeError, KeyError): return '' -class RoutesColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class RoutesColumn(cliff_columns.FormattableColumn[Any]): + def human_readable(self) -> str: # Map the route keys to match --route option. for route in self._value or []: if 'nexthop' in route: route['gateway'] = route.pop('nexthop') - return utils.format_list_of_dicts(self._value) + return utils.format_list_of_dicts(self._value) or "" _formatters = { @@ -68,7 +71,9 @@ def human_readable(self): } -def _get_columns(item): +def _get_columns( + item: _router.Router, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { 'is_ha': 'ha', 'is_distributed': 'distributed', @@ -76,23 +81,32 @@ def _get_columns(item): } if hasattr(item, 'interfaces_info'): column_map['interfaces_info'] = 'interfaces_info' - invisible_columns = ['location', 'tenant_id'] + hidden_columns = ['location', 'tenant_id'] if item.is_ha is None: - invisible_columns.append('is_ha') + hidden_columns.append('is_ha') column_map.pop('is_ha') if item.is_distributed is None: - invisible_columns.append('is_distributed') + hidden_columns.append('is_distributed') column_map.pop('is_distributed') + if not hasattr(item, 'evpn_vni'): + hidden_columns.append('evpn_vni') return utils.get_osc_show_columns_for_sdk_resource( - item, column_map, invisible_columns + item, column_map, hidden_columns ) -def is_multiple_gateways_supported(n_client): - return n_client.find_extension("external-gateway-multihoming") is not None +def is_multiple_gateways_supported(n_client: Any) -> bool: + return ( + n_client.find_extension( + "external-gateway-multihoming", ignore_missing=True + ) + is not None + ) -def _passed_multiple_gateways(extension_supported, external_gateways): +def _passed_multiple_gateways( + extension_supported: bool, external_gateways: list[Any] +) -> bool: passed_multiple_gws = len(external_gateways) > 1 if passed_multiple_gws and not extension_supported: msg = _( @@ -104,13 +118,15 @@ def _passed_multiple_gateways(extension_supported, external_gateways): return passed_multiple_gws -def _get_external_gateway_attrs(client_manager, parsed_args): - attrs: dict[str, ty.Any] = {} +def _get_external_gateway_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} if parsed_args.external_gateways: - external_gateways: collections.defaultdict[str, list[dict]] = ( - collections.defaultdict(list) - ) + external_gateways: collections.defaultdict[ + str, list[dict[str, Any]] + ] = collections.defaultdict(list) n_client = client_manager.network first_network_id = '' @@ -201,8 +217,10 @@ def _get_external_gateway_attrs(client_manager, parsed_args): return attrs -def _get_attrs(client_manager, parsed_args): - attrs = {} +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} n_client = client_manager.network if parsed_args.name is not None: @@ -224,25 +242,31 @@ def _get_attrs(client_manager, parsed_args): attrs['description'] = parsed_args.description # "router set" command doesn't support setting project. if 'project' in parsed_args and parsed_args.project is not None: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id attrs.update(_get_external_gateway_attrs(client_manager, parsed_args)) # "router set" command doesn't support setting flavor_id. if 'flavor_id' in parsed_args and parsed_args.flavor_id is not None: - flavor = n_client.find_flavor(parsed_args.flavor_id) + flavor = n_client.find_flavor( + parsed_args.flavor_id, ignore_missing=False + ) attrs['flavor_id'] = flavor.id elif 'flavor' in parsed_args and parsed_args.flavor is not None: flavor = n_client.find_flavor(parsed_args.flavor, ignore_missing=False) attrs['flavor_id'] = flavor.id - for attr in ('enable_default_route_bfd', 'enable_default_route_ecmp'): + for attr in ( + 'enable_default_route_bfd', + 'enable_default_route_ecmp', + 'evpn_vni', + ): value = getattr(parsed_args, attr, None) if value is not None: attrs[attr] = value @@ -250,7 +274,20 @@ def _get_attrs(client_manager, parsed_args): return attrs -def _parser_add_bfd_ecmp_arguments(parser): +def _parse_evpn_vni(value: str) -> int: + try: + vni = int(value) + except ValueError: + raise argparse.ArgumentTypeError( + _("'%(value)s' is not a valid VNI (use a positive integer)") + % {'value': value} + ) + if vni <= 0: + raise argparse.ArgumentTypeError(_("VNI must be a positive integer")) + return vni + + +def _parser_add_bfd_ecmp_arguments(parser: argparse.ArgumentParser) -> None: """Helper to add BFD and ECMP args for CreateRouter and SetRouter.""" parser.add_argument( '--enable-default-route-bfd', @@ -291,7 +328,9 @@ def _parser_add_bfd_ecmp_arguments(parser): ) -def _command_check_bfd_ecmp_supported(attrs, client): +def _command_check_bfd_ecmp_supported( + attrs: dict[str, Any], client: Any +) -> None: """Helper to check for server side support when bfd/ecmp attrs provided. :raises: exceptions.CommandError @@ -311,7 +350,7 @@ def _command_check_bfd_ecmp_supported(attrs, client): class AddPortToRouter(command.Command): _description = _("Add a port to a router") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -323,19 +362,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network port = client.find_port(parsed_args.port, ignore_missing=False) client.add_interface_to_router( client.find_router(parsed_args.router, ignore_missing=False), - port_id=port.id, + port=port.id, ) class AddSubnetToRouter(command.Command): _description = _("Add a subnet to a router") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -347,21 +386,33 @@ def get_parser(self, prog_name): metavar='', help=_("Subnet to be added (name or ID)"), ) + parser.add_argument( + '--advertise-host', + action='store_true', + default=False, + dest='advertise_host', + help=_( + "Mark the subnet's prefixes to be advertised as host " + "routes within the router's EVPN VNI. " + "Only valid for EVPN routers." + ), + ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network subnet = client.find_subnet(parsed_args.subnet, ignore_missing=False) client.add_interface_to_router( client.find_router(parsed_args.router, ignore_missing=False), - subnet_id=subnet.id, + subnet=subnet.id, + advertise_host=parsed_args.advertise_host, ) class AddExtraRoutesToRouter(command.ShowOne): _description = _("Add extra static routes to a router's routing table.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -391,7 +442,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: if parsed_args.routes is not None: for route in parsed_args.routes: route['nexthop'] = route.pop('gateway') @@ -412,7 +465,7 @@ class RemoveExtraRoutesFromRouter(command.ShowOne): "Remove extra static routes from a router's routing table." ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -442,7 +495,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: if parsed_args.routes is not None: for route in parsed_args.routes: route['nexthop'] = route.pop('gateway') @@ -458,12 +513,10 @@ def take_action(self, parsed_args): return (display_columns, data) -# TODO(yanxing'an): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class CreateRouter(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a new router") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar='', help=_("New router name") @@ -587,10 +640,30 @@ def get_parser(self, prog_name): metavar='', help=_('Attach QoS policy to router gateway IPs'), ) + evpn_group = parser.add_mutually_exclusive_group() + evpn_group.add_argument( + '--evpn-vni', + metavar='', + default=None, + type=_parse_evpn_vni, + dest='evpn_vni', + help=_("Associate the router with an EVPN identified by a VNI."), + ) + evpn_group.add_argument( + '--auto-evpn-vni', + action='store_const', + dest='evpn_vni', + const=0, + help=_( + "Associate the router with an EVPN using an auto-assigned VNI." + ), + ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) @@ -654,7 +727,7 @@ def take_action(self, parsed_args): class DeleteRouter(command.Command): _description = _("Delete router(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -664,7 +737,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -691,12 +764,10 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) -# TODO(yanxing'an): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class ListRouter(command.Lister): _description = _("List routers") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--name', @@ -719,20 +790,26 @@ def get_parser(self, prog_name): parser.add_argument( '--project', metavar='', - help=_("List routers according to their project (name or ID)"), + help=_( + "List only routers with the specified project (name or ID)" + ), ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--agent', metavar='', - help=_("List routers hosted by an agent (ID only)"), + help=_( + "List only routers hosted by the specified agent (ID only)" + ), ) _tag.add_tag_filtering_option_to_parser(parser, _('routers')) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: client = self.app.client_manager.network columns: tuple[str, ...] = ( @@ -763,23 +840,34 @@ def take_action(self, parsed_args): args['is_admin_state_up'] = False if parsed_args.project: - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) args['project_id'] = project_id + if parsed_args.marker is not None: + args['marker'] = parsed_args.marker + if parsed_args.limit is not None: + args['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + args['max_items'] = parsed_args.max_items + _tag.get_tag_filtering_args(parsed_args, args) - if parsed_args.agent is not None: + if parsed_args.agent is None: + data = list(client.routers(**args)) + else: agent = client.get_agent(parsed_args.agent) - data = client.agent_hosted_routers(agent) # NOTE: Networking API does not support filtering by parameters, # so we need filtering in the client side. - data = [d for d in data if self._filter_match(d, args)] - else: - data = client.routers(**args) + data = [ + d + for d in client.agent_hosted_routers(agent) + if self._filter_match(d, args) + ] # check if "HA" and "Distributed" columns should be displayed also data = list(data) @@ -823,7 +911,7 @@ def take_action(self, parsed_args): ) @staticmethod - def _filter_match(data, conditions): + def _filter_match(data: Any, conditions: dict[str, Any]) -> bool: for key, value in conditions.items(): try: if getattr(data, key) != value: @@ -840,7 +928,7 @@ def _filter_match(data, conditions): class RemovePortFromRouter(command.Command): _description = _("Remove a port from a router") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -854,19 +942,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network port = client.find_port(parsed_args.port, ignore_missing=False) client.remove_interface_from_router( client.find_router(parsed_args.router, ignore_missing=False), - port_id=port.id, + port=port.id, ) class RemoveSubnetFromRouter(command.Command): _description = _("Remove a subnet from a router") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -882,21 +970,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network subnet = client.find_subnet(parsed_args.subnet, ignore_missing=False) client.remove_interface_from_router( client.find_router(parsed_args.router, ignore_missing=False), - subnet_id=subnet.id, + subnet=subnet.id, ) -# TODO(yanxing'an): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class SetRouter(common.NeutronCommandWithExtraArgs): _description = _("Set router properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -1040,7 +1126,7 @@ def get_parser(self, prog_name): _parser_add_bfd_ecmp_arguments(parser) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_router(parsed_args.router, ignore_missing=False) @@ -1130,7 +1216,7 @@ def take_action(self, parsed_args): class ShowRouter(command.ShowOne): _description = _("Display router details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -1139,7 +1225,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_router(parsed_args.router, ignore_missing=False) interfaces_info = [] @@ -1166,7 +1254,7 @@ def take_action(self, parsed_args): class UnsetRouter(common.NeutronUnsetCommandWithExtraArgs): _description = _("Unset router properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--route', @@ -1206,7 +1294,7 @@ def get_parser(self, prog_name): _tag.add_tag_option_to_parser_for_unset(parser, _('router')) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_router(parsed_args.router, ignore_missing=False) tmp_routes = copy.deepcopy(obj.routes) @@ -1261,7 +1349,7 @@ def take_action(self, parsed_args): class AddGatewayToRouter(command.ShowOne): _description = _("Add router gateway") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -1293,7 +1381,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network if not is_multiple_gateways_supported(client): msg = _( @@ -1328,7 +1418,7 @@ def take_action(self, parsed_args): class RemoveGatewayFromRouter(command.ShowOne): _description = _("Remove router gateway") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'router', @@ -1360,7 +1450,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network if not is_multiple_gateways_supported(client): msg = _( diff --git a/openstackclient/network/v2/security_group.py b/openstackclient/network/v2/security_group.py index ddf6c786ec..9f0b76c5fd 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -9,28 +9,34 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# """Security Group action implementations""" import argparse +from collections.abc import Iterable, Sequence +import logging +from typing import Any, cast from cliff import columns as cliff_columns -from osc_lib.command import command +from openstack.network.v2 import security_group as _security_group +from osc_lib import exceptions from osc_lib import utils from osc_lib.utils import tags as _tag -from openstackclient.api import compute_v2 +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common -from openstackclient.network import utils as network_utils + +LOG = logging.getLogger(__name__) -def _format_network_security_group_rules(sg_rules): - # For readability and to align with formatting compute security group - # rules, trim keys with caller known (e.g. security group and tenant ID) - # or empty values. +def _format_network_security_group_rules( + sg_rules: list[dict[str, Any]], +) -> str: + # For readability, trim keys with caller known (e.g. security group and + # tenant ID) or empty values. for sg_rule in sg_rules: empty_keys = [k for k, v in sg_rule.items() if not v] for key in empty_keys: @@ -38,54 +44,22 @@ def _format_network_security_group_rules(sg_rules): sg_rule.pop('security_group_id', None) sg_rule.pop('tenant_id', None) sg_rule.pop('project_id', None) - return utils.format_list_of_dicts(sg_rules) - - -def _format_compute_security_group_rule(sg_rule): - info = network_utils.transform_compute_security_group_rule(sg_rule) - # Trim parent security group ID since caller has this information. - info.pop('parent_group_id', None) - # Trim keys with empty string values. - keys_to_trim = [ - 'ip_protocol', - 'ip_range', - 'port_range', - 'remote_security_group', - ] - for key in keys_to_trim: - if key in info and not info[key]: - info.pop(key) - return utils.format_dict(info) - - -def _format_compute_security_group_rules(sg_rules): - rules = [] - for sg_rule in sg_rules: - rules.append(_format_compute_security_group_rule(sg_rule)) - return utils.format_list(rules, separator='\n') + return utils.format_list_of_dicts(sg_rules) or "" -class NetworkSecurityGroupRulesColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class NetworkSecurityGroupRulesColumn(cliff_columns.FormattableColumn[Any]): + def human_readable(self) -> str: return _format_network_security_group_rules(self._value) -class ComputeSecurityGroupRulesColumn(cliff_columns.FormattableColumn): - def human_readable(self): - return _format_compute_security_group_rules(self._value) - - -_formatters_network = { +_formatters = { 'security_group_rules': NetworkSecurityGroupRulesColumn, } -_formatters_compute = { - 'rules': ComputeSecurityGroupRulesColumn, -} - - -def _get_columns(item): +def _get_columns( + item: _security_group.SecurityGroup, +) -> tuple[tuple[str, ...], tuple[str, ...]]: # We still support Nova managed security groups, where we have tenant_id. column_map = { 'security_group_rules': 'rules', @@ -96,14 +70,11 @@ def _get_columns(item): ) -# TODO(abhiraut): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. -class CreateSecurityGroup( - common.NetworkAndComputeShowOne, common.NeutronCommandWithExtraArgs -): +class CreateSecurityGroup(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a new security group") - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( "name", metavar="", help=_("New security group name") ) @@ -112,13 +83,10 @@ def update_parser_common(self, parser): metavar="", help=_("Security group description"), ) - return parser - - def update_parser_network(self, parser): parser.add_argument( '--project', metavar='', - help=self.enhance_help_neutron(_("Owner's project (name or ID)")), + help=_("Owner's project (name or ID)"), ) stateful_group = parser.add_mutually_exclusive_group() stateful_group.add_argument( @@ -133,21 +101,20 @@ def update_parser_network(self, parser): default=None, help=_("Security group is stateless"), ) - identity_common.add_project_domain_option_to_parser( - parser, enhance_help=self.enhance_help_neutron - ) - _tag.add_tag_option_to_parser_for_create( - parser, _('security group'), enhance_help=self.enhance_help_neutron - ) + identity_common.add_project_domain_option_to_parser(parser) + _tag.add_tag_option_to_parser_for_create(parser, _('security group')) return parser - def _get_description(self, parsed_args): + def _get_description(self, parsed_args: argparse.Namespace) -> str: if parsed_args.description is not None: - return parsed_args.description + return cast(str, parsed_args.description) else: - return parsed_args.name + return cast(str, parsed_args.name) - def take_action_network(self, client, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network # Build the create attributes. attrs = {} attrs['name'] = parsed_args.name @@ -157,12 +124,12 @@ def take_action_network(self, client, parsed_args): if parsed_args.stateless: attrs['stateful'] = False if parsed_args.project is not None: - identity_client = self.app.client_manager.identity - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id attrs.update( self._parse_extra_properties(parsed_args.extra_properties) @@ -174,33 +141,16 @@ def take_action_network(self, client, parsed_args): _tag.update_tags_for_set(client, obj, parsed_args) display_columns, property_columns = _get_columns(obj) data = utils.get_item_properties( - obj, property_columns, formatters=_formatters_network - ) - return (display_columns, data) - - def take_action_compute(self, client, parsed_args): - description = self._get_description(parsed_args) - obj = compute_v2.create_security_group( - client, - parsed_args.name, - description, - ) - display_columns = ('description', 'id', 'name', 'project_id', 'rules') - property_columns = ('description', 'id', 'name', 'tenant_id', 'rules') - data = utils.get_dict_properties( - obj, property_columns, formatters=_formatters_compute + obj, property_columns, formatters=_formatters ) return (display_columns, data) -class DeleteSecurityGroup(common.NetworkAndComputeDelete): +class DeleteSecurityGroup(command.Command): _description = _("Delete security group(s)") - # Used by base class to find resources in parsed_args. - resource = 'group' - r = None - - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'group', metavar='', @@ -209,103 +159,124 @@ def update_parser_common(self, parser): ) return parser - def take_action_network(self, client, parsed_args): - obj = client.find_security_group(self.r, ignore_missing=False) - client.delete_security_group(obj) + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 + + for group in parsed_args.group: + try: + obj = client.find_security_group(group, ignore_missing=False) + client.delete_security_group(obj) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete security group with " + "name or ID '%(group)s': %(e)s" + ), + {'group': group, 'e': e}, + ) - def take_action_compute(self, client, parsed_args): - security_group = compute_v2.find_security_group(client, self.r) - compute_v2.delete_security_group(client, security_group['id']) + if result > 0: + total = len(parsed_args.group) + msg = _("%(result)s of %(total)s groups failed to delete.") % { + 'result': result, + 'total': total, + } + raise exceptions.CommandError(msg) -# TODO(rauta): Use the SDK resource mapped attribute names once -# the OSC minimum requirements include SDK 1.0. -class ListSecurityGroup(common.NetworkAndComputeLister): +class ListSecurityGroup(command.Lister): _description = _("List security groups") - FIELDS_TO_RETRIEVE = ['id', 'name', 'description', 'project_id', 'tags'] - - def update_parser_network(self, parser): - if not self.is_docs_build: - # Maintain and hide the argument for backwards compatibility. - # Network will always return all projects for an admin. - parser.add_argument( - '--all-projects', - action='store_true', - default=False, - help=argparse.SUPPRESS, - ) + FIELDS_TO_RETRIEVE = [ + 'id', + 'name', + 'description', + 'project_id', + 'tags', + 'shared', + ] + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( '--project', metavar='', - help=self.enhance_help_neutron( - _("List security groups according to the project (name or ID)") + help=_( + "List only security groups with the specified project " + "(name or ID)" ), ) - identity_common.add_project_domain_option_to_parser( - parser, enhance_help=self.enhance_help_neutron - ) - _tag.add_tag_filtering_option_to_parser( - parser, _('security group'), enhance_help=self.enhance_help_neutron - ) - return parser + identity_common.add_project_domain_option_to_parser(parser) - def update_parser_compute(self, parser): - parser.add_argument( - '--all-projects', + shared_group = parser.add_mutually_exclusive_group() + shared_group.add_argument( + '--share', action='store_true', - default=False, - help=self.enhance_help_nova_network( - _("Display information from all projects (admin only)") - ), + dest='shared', + default=None, + help=_("List only security groups shared between projects"), + ) + shared_group.add_argument( + '--no-share', + action='store_false', + dest='shared', + default=None, + help=_("List only security groups not shared between projects"), ) + + _tag.add_tag_filtering_option_to_parser(parser, _('security group')) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action_network(self, client, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network filters = {} + if parsed_args.project: - identity_client = self.app.client_manager.identity - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) filters['project_id'] = project_id + if parsed_args.shared is not None: + filters['shared'] = parsed_args.shared + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items _tag.get_tag_filtering_args(parsed_args, filters) data = client.security_groups( fields=self.FIELDS_TO_RETRIEVE, **filters ) - columns = ("id", "name", "description", "project_id", "tags") - column_headers = ("ID", "Name", "Description", "Project", "Tags") - return ( - column_headers, - ( - utils.get_item_properties( - s, - columns, - ) - for s in data - ), + columns = ( + "id", + "name", + "description", + "project_id", + "tags", + "is_shared", ) - - def take_action_compute(self, client, parsed_args): - data = compute_v2.list_security_groups( - # TODO(dtroyer): add limit, marker - client, - all_projects=parsed_args.all_projects, + column_headers = ( + "ID", + "Name", + "Description", + "Project", + "Tags", + "Shared", ) - - columns: tuple[str, ...] = ("id", "name", "description") - column_headers: tuple[str, ...] = ("ID", "Name", "Description") - if parsed_args.all_projects: - columns += ('tenant_id',) - column_headers += ('Project',) return ( column_headers, ( - utils.get_dict_properties( + utils.get_item_properties( s, columns, ) @@ -314,12 +285,11 @@ def take_action_compute(self, client, parsed_args): ) -class SetSecurityGroup( - common.NetworkAndComputeCommand, common.NeutronCommandWithExtraArgs -): +class SetSecurityGroup(common.NeutronCommandWithExtraArgs): _description = _("Set security group properties") - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'group', metavar='', @@ -346,15 +316,11 @@ def update_parser_common(self, parser): default=None, help=_("Security group is stateless"), ) + _tag.add_tag_option_to_parser_for_set(parser, _('security group')) return parser - def update_parser_network(self, parser): - _tag.add_tag_option_to_parser_for_set( - parser, _('security group'), enhance_help=self.enhance_help_neutron - ) - return parser - - def take_action_network(self, client, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network obj = client.find_security_group( parsed_args.group, ignore_missing=False ) @@ -378,29 +344,12 @@ def take_action_network(self, client, parsed_args): # tags is a subresource and it needs to be updated separately. _tag.update_tags_for_set(client, obj, parsed_args) - def take_action_compute(self, client, parsed_args): - security_group = compute_v2.find_security_group( - client, parsed_args.group - ) - - params = {} - if parsed_args.name is not None: - params['name'] = parsed_args.name - if parsed_args.description is not None: - params['description'] = parsed_args.description - - # NOTE(rtheis): Previous behavior did not raise a CommandError - # if there were no updates. Maintain this behavior and issue - # the update. - compute_v2.update_security_group( - client, security_group['id'], **params - ) - -class ShowSecurityGroup(common.NetworkAndComputeShowOne): +class ShowSecurityGroup(command.ShowOne): _description = _("Display security group details") - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'group', metavar='', @@ -408,22 +357,16 @@ def update_parser_common(self, parser): ) return parser - def take_action_network(self, client, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network obj = client.find_security_group( parsed_args.group, ignore_missing=False ) display_columns, property_columns = _get_columns(obj) data = utils.get_item_properties( - obj, property_columns, formatters=_formatters_network - ) - return (display_columns, data) - - def take_action_compute(self, client, parsed_args): - obj = compute_v2.find_security_group(client, parsed_args.group) - display_columns = ('description', 'id', 'name', 'project_id', 'rules') - property_columns = ('description', 'id', 'name', 'tenant_id', 'rules') - data = utils.get_dict_properties( - obj, property_columns, formatters=_formatters_compute + obj, property_columns, formatters=_formatters ) return (display_columns, data) @@ -431,7 +374,7 @@ def take_action_compute(self, client, parsed_args): class UnsetSecurityGroup(command.Command): _description = _("Unset security group properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group', @@ -441,7 +384,7 @@ def get_parser(self, prog_name): _tag.add_tag_option_to_parser_for_unset(parser, _('security group')) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_security_group( parsed_args.group, ignore_missing=False diff --git a/openstackclient/network/v2/security_group_rule.py b/openstackclient/network/v2/security_group_rule.py index 4907fe66d3..c51c2b0ac4 100644 --- a/openstackclient/network/v2/security_group_rule.py +++ b/openstackclient/network/v2/security_group_rule.py @@ -14,13 +14,17 @@ """Security Group Rule action implementations""" import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +from openstack.network.v2 import security_group_rule as _security_group_rule from osc_lib.cli import parseractions from osc_lib import exceptions from osc_lib import utils -from openstackclient.api import compute_v2 +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -29,21 +33,22 @@ LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _security_group_rule.SecurityGroupRule, +) -> tuple[tuple[str, ...], tuple[str, ...]]: hidden_columns = ['location', 'name', 'tenant_id', 'tags'] return utils.get_osc_show_columns_for_sdk_resource( item, {}, hidden_columns ) -# TODO(abhiraut): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class CreateSecurityGroupRule( - common.NetworkAndComputeShowOne, common.NeutronCommandWithExtraArgs + command.ShowOne, common.NeutronCommandWithExtraArgs ): _description = _("Create a new security group rule") - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'group', metavar='', @@ -64,24 +69,11 @@ def update_parser_common(self, parser): metavar="", help=_("Remote security group (name or ID)"), ) - if self.is_neutron: - remote_group.add_argument( - "--remote-address-group", - metavar="", - help=_("Remote address group (name or ID)"), - ) - - # NOTE(efried): The --dst-port, --protocol, and --proto options exist - # for both nova-network and neutron, but differ slightly. For the sake - # of the docs build, which has to account for both variants, but only - # add each to the parser once, they are handled here rather than in the - # _network- or _compute-specific methods below. - - # --dst-port has a default for nova-net only - if self.is_nova_network: - dst_port_default = dict(default=(0, 0)) - else: - dst_port_default = {} + remote_group.add_argument( + "--remote-address-group", + metavar="", + help=_("Remote address group (name or ID)"), + ) parser.add_argument( '--dst-port', metavar='', @@ -91,115 +83,69 @@ def update_parser_common(self, parser): "ending port range: 137:139. Required for IP protocols TCP " "and UDP. Ignored for ICMP IP protocols." ), - **dst_port_default, - ) - - # NOTE(rtheis): Support either protocol option name for now. - # However, consider deprecating and then removing --proto in - # a future release. - protocol_group = parser.add_mutually_exclusive_group() - # --proto[col] has choices for nova-network only - if self.is_nova_network: - proto_choices = dict(choices=['icmp', 'tcp', 'udp']) - else: - proto_choices = {} - protocol_help_compute = _("IP protocol (icmp, tcp, udp; default: tcp)") - protocol_help_network = _( - "IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, ipv6-encap, " - "ipv6-frag, ipv6-icmp, ipv6-nonxt, ipv6-opts, ipv6-route, ospf, " - "pgm, rsvp, sctp, tcp, udp, udplite, vrrp and integer " - "representations [0-255] or any; default: any (all protocols))" ) - if self.is_nova_network: - protocol_help = protocol_help_compute - elif self.is_neutron: - protocol_help = protocol_help_network - else: - # Docs build: compose help for both nova-network and neutron - protocol_help = self.split_help( - protocol_help_network, protocol_help_compute - ) - - protocol_group.add_argument( + parser.add_argument( '--protocol', metavar='', type=network_utils.convert_to_lowercase, - help=protocol_help, - **proto_choices, + help=_( + "IP protocol (ah, dccp, egp, esp, gre, icmp, igmp, " + "ipv6-encap, ipv6-frag, ipv6-icmp, ipv6-nonxt, ipv6-opts, " + "ipv6-route, ospf, pgm, rsvp, sctp, tcp, udp, udplite, vrrp " + "and integer representations [0-255] or any; " + "default: any (all protocols))" + ), ) - if not self.is_docs_build: - protocol_group.add_argument( - '--proto', - metavar='', - type=network_utils.convert_to_lowercase, - help=argparse.SUPPRESS, - **proto_choices, - ) - - return parser - - def update_parser_network(self, parser): parser.add_argument( '--description', metavar='', - help=self.enhance_help_neutron( - _("Set security group rule description") - ), + help=_("Set security group rule description"), ) parser.add_argument( '--icmp-type', metavar='', type=int, - help=self.enhance_help_neutron( - _("ICMP type for ICMP IP protocols") - ), + help=_("ICMP type for ICMP IP protocols"), ) parser.add_argument( '--icmp-code', metavar='', type=int, - help=self.enhance_help_neutron( - _("ICMP code for ICMP IP protocols") - ), + help=_("ICMP code for ICMP IP protocols"), ) direction_group = parser.add_mutually_exclusive_group() direction_group.add_argument( '--ingress', action='store_true', - help=self.enhance_help_neutron( - _("Rule applies to incoming network traffic (default)") - ), + help=_("Rule applies to incoming network traffic (default)"), ) direction_group.add_argument( '--egress', action='store_true', - help=self.enhance_help_neutron( - _("Rule applies to outgoing network traffic") - ), + help=_("Rule applies to outgoing network traffic"), ) parser.add_argument( '--ethertype', metavar='', choices=['IPv4', 'IPv6'], type=network_utils.convert_ipvx_case, - help=self.enhance_help_neutron( - _( - "Ethertype of network traffic " - "(IPv4, IPv6; default: based on IP protocol)" - ) + help=_( + "Ethertype of network traffic " + "(IPv4, IPv6; default: based on IP protocol)" ), ) parser.add_argument( '--project', metavar='', - help=self.enhance_help_neutron(_("Owner's project (name or ID)")), - ) - identity_common.add_project_domain_option_to_parser( - parser, enhance_help=self.enhance_help_neutron + help=_("Owner's project (name or ID)"), ) + identity_common.add_project_domain_option_to_parser(parser) return parser - def take_action_network(self, client, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network # Get the security group ID to hold the rule. security_group_id = client.find_security_group( parsed_args.group, ignore_missing=False @@ -273,12 +219,12 @@ def take_action_network(self, client, parsed_args): attrs['remote_ip_prefix'] = '::/0' attrs['security_group_id'] = security_group_id if parsed_args.project is not None: - identity_client = self.app.client_manager.identity - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id attrs.update( @@ -291,46 +237,12 @@ def take_action_network(self, client, parsed_args): data = utils.get_item_properties(obj, columns) return (display_columns, data) - def take_action_compute(self, client, parsed_args): - group = compute_v2.find_security_group(client, parsed_args.group) - protocol = network_utils.get_protocol( - parsed_args, default_protocol='tcp' - ) - if protocol == 'icmp': - from_port, to_port = -1, -1 - else: - from_port, to_port = parsed_args.dst_port - - remote_ip = None - if parsed_args.remote_group is not None: - parsed_args.remote_group = compute_v2.find_security_group( - client, parsed_args.remote_group - )['id'] - if parsed_args.remote_ip is not None: - remote_ip = parsed_args.remote_ip - else: - remote_ip = '0.0.0.0/0' - - obj = compute_v2.create_security_group_rule( - client, - security_group_id=group['id'], - ip_protocol=protocol, - from_port=from_port, - to_port=to_port, - remote_ip=remote_ip, - remote_group=parsed_args.remote_group, - ) - return network_utils.format_security_group_rule_show(obj) - -class DeleteSecurityGroupRule(common.NetworkAndComputeDelete): +class DeleteSecurityGroupRule(command.Command): _description = _("Delete security group rule(s)") - # Used by base class to find resources in parsed_args. - resource = 'rule' - r = None - - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'rule', metavar='', @@ -339,117 +251,110 @@ def update_parser_common(self, parser): ) return parser - def take_action_network(self, client, parsed_args): - obj = client.find_security_group_rule(self.r, ignore_missing=False) - client.delete_security_group_rule(obj) + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 - def take_action_compute(self, client, parsed_args): - compute_v2.delete_security_group_rule(client, self.r) + for rule in parsed_args.rule: + try: + obj = client.find_security_group_rule( + rule, ignore_missing=False + ) + client.delete_security_group_rule(obj) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete security group rule with " + "name or ID '%(rule)s': %(e)s" + ), + {'rule': rule, 'e': e}, + ) + + if result > 0: + total = len(parsed_args.rule) + msg = _("%(result)s of %(total)s rules failed to delete.") % { + 'result': result, + 'total': total, + } + raise exceptions.CommandError(msg) -class ListSecurityGroupRule(common.NetworkAndComputeLister): +class ListSecurityGroupRule(command.Lister): _description = _("List security group rules") - def _format_network_security_group_rule(self, rule): + @staticmethod + def _format_network_security_group_rule( + rule: _security_group_rule.SecurityGroupRule, + ) -> dict[str, object]: """Transform the SDK SecurityGroupRule object to a dict The SDK object gets in the way of reformatting columns... Create port_range column from port_range_min and port_range_max """ - rule = rule.to_dict() - rule['port_range'] = network_utils.format_network_port_range(rule) - rule['remote_ip_prefix'] = network_utils.format_remote_ip_prefix(rule) - return rule + data = rule.to_dict() + data['port_range'] = network_utils.format_network_port_range(data) + data['remote_ip_prefix'] = network_utils.format_remote_ip_prefix(data) + return data - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'group', metavar='', nargs='?', help=_("List all rules in this security group (name or ID)"), ) - return parser - - def update_parser_network(self, parser): - if not self.is_docs_build: - # Accept but hide the argument for consistency with compute. - # Network will always return all projects for an admin. - parser.add_argument( - '--all-projects', - action='store_true', - default=False, - help=argparse.SUPPRESS, - ) - parser.add_argument( '--protocol', metavar='', type=network_utils.convert_to_lowercase, - help=self.enhance_help_neutron( - _( - "List rules by the IP protocol (ah, dhcp, egp, esp, gre, " - "icmp, igmp, ipv6-encap, ipv6-frag, ipv6-icmp, " - "ipv6-nonxt, ipv6-opts, ipv6-route, ospf, pgm, rsvp, " - "sctp, tcp, udp, udplite, vrrp and integer " - "representations [0-255] or any; " - "default: any (all protocols))" - ) + help=_( + "List only rules with the specified IP protocol " + "(ah, dhcp, egp, esp, gre, icmp, igmp, ipv6-encap, " + "ipv6-frag, ipv6-icmp, ipv6-nonxt, ipv6-opts, ipv6-route, " + "ospf, pgm, rsvp, sctp, tcp, udp, udplite, vrrp and integer " + "representations [0-255] or any; " + "default: any (all protocols))" ), ) parser.add_argument( '--ethertype', metavar='', type=network_utils.convert_to_lowercase, - help=self.enhance_help_neutron( - _("List rules by the Ethertype (IPv4 or IPv6)") + help=_( + "List only rules with the specified Ethertype (IPv4 or IPv6)" ), ) direction_group = parser.add_mutually_exclusive_group() direction_group.add_argument( '--ingress', action='store_true', - help=self.enhance_help_neutron( - _("List rules applied to incoming network traffic") - ), + help=_("List only rules applied to incoming network traffic"), ) direction_group.add_argument( '--egress', action='store_true', - help=self.enhance_help_neutron( - _("List rules applied to outgoing network traffic") - ), + help=_("List only rules applied to outgoing network traffic"), ) parser.add_argument( '--long', action='store_true', default=False, - help=self.enhance_help_neutron( - _("**Deprecated** This argument is no longer needed") - ), + help=_("**Deprecated** This argument is no longer needed"), ) - return parser - - def update_parser_compute(self, parser): parser.add_argument( - '--all-projects', - action='store_true', - default=False, - help=self.enhance_help_nova_network( - _("Display information from all projects (admin only)") - ), + '--project', + metavar='', + help=_("List only rules with the specified project (name or ID)"), ) - if not self.is_docs_build: - # Accept but hide the argument for consistency with network. - # There are no additional fields to display at this time. - parser.add_argument( - '--long', - action='store_false', - default=False, - help=argparse.SUPPRESS, - ) + identity_common.add_project_domain_option_to_parser(parser) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def _get_column_headers(self, parsed_args): + def _get_column_headers( + self, parsed_args: argparse.Namespace + ) -> tuple[str, ...]: column_headers: tuple[str, ...] = ( 'ID', 'IP Protocol', @@ -458,14 +363,16 @@ def _get_column_headers(self, parsed_args): 'Port Range', 'Direction', 'Remote Security Group', + 'Remote Address Group', ) - if self.is_neutron: - column_headers += ('Remote Address Group',) if parsed_args.group is None: column_headers += ('Security Group',) return column_headers - def take_action_network(self, client, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network if parsed_args.long: msg = _( "The --long option has been deprecated and is no longer needed" @@ -503,6 +410,21 @@ def take_action_network(self, client, parsed_args): query['direction'] = 'egress' if parsed_args.protocol is not None: query['protocol'] = parsed_args.protocol + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + query['project_id'] = project_id + + if parsed_args.marker is not None: + query['marker'] = parsed_args.marker + if parsed_args.limit is not None: + query['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + query['max_items'] = parsed_args.max_items rules = [ self._format_network_security_group_rule(r) @@ -520,57 +442,12 @@ def take_action_network(self, client, parsed_args): ), ) - def take_action_compute(self, client, parsed_args): - column_headers = self._get_column_headers(parsed_args) - columns: tuple[str, ...] = ( - "ID", - "IP Protocol", - "Ethertype", - "IP Range", - "Port Range", - "Remote Security Group", - ) - - rules_to_list = [] - if parsed_args.group is not None: - security_group = compute_v2.find_security_group( - client, parsed_args.group - ) - rules_to_list = security_group['rules'] - else: - columns += ('parent_group_id',) - for security_group in compute_v2.list_security_groups( - client, all_projects=parsed_args.all_projects - ): - rules_to_list.extend(security_group['rules']) - - # NOTE(rtheis): Turn the raw rules into resources. - rules = [] - for rule in rules_to_list: - rules.append( - network_utils.transform_compute_security_group_rule(rule), - ) - # rules.append(compute_secgroup_rules.SecurityGroupRule( - # client.security_group_rules, - # network_utils.transform_compute_security_group_rule(rule), - # )) - - return ( - column_headers, - ( - utils.get_dict_properties( - s, - columns, - ) - for s in rules - ), - ) - -class ShowSecurityGroupRule(common.NetworkAndComputeShowOne): +class ShowSecurityGroupRule(command.ShowOne): _description = _("Display security group rule details") - def update_parser_common(self, parser): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) parser.add_argument( 'rule', metavar="", @@ -578,7 +455,10 @@ def update_parser_common(self, parser): ) return parser - def take_action_network(self, client, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network obj = client.find_security_group_rule( parsed_args.rule, ignore_missing=False ) @@ -590,27 +470,3 @@ def take_action_network(self, client, parsed_args): display_columns, columns = _get_columns(obj) data = utils.get_item_properties(obj, columns) return (display_columns, data) - - def take_action_compute(self, client, parsed_args): - # NOTE(rtheis): Unfortunately, compute does not have an API - # to get or list security group rules so parse through the - # security groups to find all accessible rules in search of - # the requested rule. - obj = None - security_group_rules = [] - for security_group in compute_v2.list_security_groups(client): - security_group_rules.extend(security_group['rules']) - for security_group_rule in security_group_rules: - if parsed_args.rule == str(security_group_rule.get('id')): - obj = security_group_rule - break - - if obj is None: - msg = ( - _("Could not find security group rule with ID '%s'") - % parsed_args.rule - ) - raise exceptions.CommandError(msg) - - # NOTE(rtheis): Format security group rule - return network_utils.format_security_group_rule_show(obj) diff --git a/openstackclient/network/v2/security_groups_default_statefulness.py b/openstackclient/network/v2/security_groups_default_statefulness.py new file mode 100644 index 0000000000..8eb28ff878 --- /dev/null +++ b/openstackclient/network/v2/security_groups_default_statefulness.py @@ -0,0 +1,243 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Security Groups Default Statefulness action implementations""" + +import argparse +import logging +from collections.abc import Iterable, Sequence +from typing import Any + +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + +LOG = logging.getLogger(__name__) + + +def _get_columns(item: Any) -> tuple[tuple[str, ...], tuple[str, ...]]: + hidden_columns = ['location', 'name', 'revision_number'] + return utils.get_osc_show_columns_for_sdk_resource( + item, {}, hidden_columns + ) + + +class CreateSecurityGroupDefaultStatefulness(command.ShowOne): + _description = _( + "Create a default statefulness setting for security groups" + ) + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + + stateful_group = parser.add_mutually_exclusive_group(required=True) + stateful_group.add_argument( + "--stateful", + action='store_true', + default=None, + dest='stateful', + help=_("Set default statefulness to stateful"), + ) + stateful_group.add_argument( + "--stateless", + action='store_false', + default=None, + dest='stateful', + help=_("Set default statefulness to stateless"), + ) + parser.add_argument( + '--project', + metavar='', + help=_( + "Apply the setting to this project (name or ID). " + "If not specified, the setting applies system-wide" + ), + ) + identity_common.add_project_domain_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs: dict[str, Any] = {} + if parsed_args.stateful is not None: + attrs['stateful'] = parsed_args.stateful + project_id = None + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + # Always set this, even if `None`, otherwise the Neutron API will + # fulfill this value with the user project ID instead. + attrs['project_id'] = project_id + + obj = client.create_security_groups_default_statefulness(**attrs) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return (display_columns, data) + + +class DeleteSecurityGroupDefaultStatefulness(command.Command): + _description = _("Delete security group default statefulness setting(s)") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'setting', + metavar='', + nargs="+", + help=_("Default statefulness setting(s) to delete (ID only)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + result = 0 + client = self.app.client_manager.network + for s in parsed_args.setting: + try: + obj = client.find_security_groups_default_statefulness( + s, ignore_missing=False + ) + client.delete_security_groups_default_statefulness(obj) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete default statefulness setting " + "with ID '%(id)s': %(e)s" + ), + {'id': s, 'e': e}, + ) + + if result > 0: + total = len(parsed_args.setting) + msg = _( + "%(result)s of %(total)s default statefulness settings " + "failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListSecurityGroupDefaultStatefulness(command.Lister): + _description = _("List security group default statefulness settings") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--project', + metavar='', + help=_("List only settings for this project (name or ID)"), + ) + identity_common.add_project_domain_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + column_headers = ( + 'ID', + 'Project ID', + 'Stateful', + ) + columns = ( + 'id', + 'project_id', + 'stateful', + ) + + query: dict[str, Any] = {} + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + query['project_id'] = project_id + + data = client.security_groups_default_statefulness(**query) + + return ( + column_headers, + (utils.get_item_properties(s, columns) for s in data), + ) + + +class SetSecurityGroupDefaultStatefulness(command.Command): + _description = _("Update a security group default statefulness setting") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'setting', + metavar='', + help=_("Default statefulness setting to modify (ID only)"), + ) + stateful_group = parser.add_mutually_exclusive_group(required=True) + stateful_group.add_argument( + "--stateful", + action='store_true', + default=None, + dest='stateful', + help=_("Set default statefulness to stateful"), + ) + stateful_group.add_argument( + "--stateless", + action='store_false', + default=None, + dest='stateful', + help=_("Set default statefulness to stateless"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + obj = client.find_security_groups_default_statefulness( + parsed_args.setting, ignore_missing=False + ) + attrs: dict[str, Any] = {} + if parsed_args.stateful is not None: + attrs['stateful'] = parsed_args.stateful + client.update_security_groups_default_statefulness(obj, **attrs) + + +class ShowSecurityGroupDefaultStatefulness(command.ShowOne): + _description = _("Show a security group default statefulness setting") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'setting', + metavar='', + help=_("Default statefulness setting to display (ID only)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + obj = client.find_security_groups_default_statefulness( + parsed_args.setting, ignore_missing=False + ) + display_columns, columns = _get_columns(obj) + data = utils.get_item_properties(obj, columns) + return (display_columns, data) diff --git a/openstackclient/network/v2/subnet.py b/openstackclient/network/v2/subnet.py index 30196bf86d..fd548a9541 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -13,18 +13,22 @@ """Subnet action implementations""" +import argparse import copy +from collections.abc import Iterable, Sequence import logging -import typing as ty +from typing import Any from cliff import columns as cliff_columns +from openstack.network.v2 import subnet as _subnet from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils from osc_lib.utils import tags as _tag +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common @@ -32,7 +36,9 @@ LOG = logging.getLogger(__name__) -def _update_arguments(obj_list, parsed_args_list, option): +def _update_arguments( + obj_list: list[Any], parsed_args_list: list[Any], option: str +) -> None: for item in parsed_args_list: try: obj_list.remove(item) @@ -44,8 +50,8 @@ def _update_arguments(obj_list, parsed_args_list, option): raise exceptions.CommandError(msg) -class AllocationPoolsColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class AllocationPoolsColumn(cliff_columns.FormattableColumn[Any]): + def human_readable(self) -> str: pool_formatted = [ '{}-{}'.format(pool.get('start', ''), pool.get('end', '')) for pool in self._value @@ -53,18 +59,19 @@ def human_readable(self): return ','.join(pool_formatted) -class HostRoutesColumn(cliff_columns.FormattableColumn): - def human_readable(self): +class HostRoutesColumn(cliff_columns.FormattableColumn[Any]): + def human_readable(self) -> str: # Map the host route keys to match --host-route option. - return utils.format_list_of_dicts( - convert_entries_to_gateway(self._value) + return ( + utils.format_list_of_dicts(convert_entries_to_gateway(self._value)) + or "" ) -class UnsortedListColumn(cliff_columns.FormattableColumn): +class UnsortedListColumn(cliff_columns.FormattableColumn[list[Any]]): # format_columns.ListColumn sorts the output, but for things like # DNS server addresses the order matters - def human_readable(self): + def human_readable(self) -> str: return ', '.join(self._value) @@ -77,7 +84,9 @@ def human_readable(self): } -def _get_common_parse_arguments(parser, is_create=True): +def _get_common_parse_arguments( + parser: argparse.ArgumentParser, is_create: bool = True +) -> None: parser.add_argument( '--allocation-pool', metavar='start=,end=', @@ -159,7 +168,9 @@ def _get_common_parse_arguments(parser, is_create=True): ) -def _get_columns(item): +def _get_columns( + item: _subnet.Subnet, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { 'is_dhcp_enabled': 'enable_dhcp', 'subnet_pool_id': 'subnetpool_id', @@ -176,7 +187,9 @@ def _get_columns(item): ) -def convert_entries_to_nexthop(entries): +def convert_entries_to_nexthop( + entries: list[dict[str, Any]], +) -> list[dict[str, Any]]: # Change 'gateway' entry to 'nexthop' changed_entries = copy.deepcopy(entries) for entry in changed_entries: @@ -187,7 +200,9 @@ def convert_entries_to_nexthop(entries): return changed_entries -def convert_entries_to_gateway(entries): +def convert_entries_to_gateway( + entries: list[dict[str, Any]], +) -> list[dict[str, Any]]: # Change 'nexthop' entry to 'gateway' changed_entries = copy.deepcopy(entries) for entry in changed_entries: @@ -198,7 +213,11 @@ def convert_entries_to_gateway(entries): return changed_entries -def _get_attrs(client_manager, parsed_args, is_create=True): +def _get_attrs( + client_manager: Any, + parsed_args: argparse.Namespace, + is_create: bool = True, +) -> dict[str, Any]: attrs = {} client = client_manager.network if 'name' in parsed_args and parsed_args.name is not None: @@ -206,12 +225,12 @@ def _get_attrs(client_manager, parsed_args, is_create=True): if is_create: if 'project' in parsed_args and parsed_args.project is not None: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id attrs['network_id'] = client.find_network( parsed_args.network, ignore_missing=False @@ -287,12 +306,10 @@ def _get_attrs(client_manager, parsed_args, is_create=True): return attrs -# TODO(abhiraut): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class CreateSubnet(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create a subnet") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar='', help=_("New subnet name") @@ -419,7 +436,9 @@ def get_parser(self, prog_name): _tag.add_tag_option_to_parser_for_create(parser, _('subnet')) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) attrs.update( @@ -436,7 +455,7 @@ def take_action(self, parsed_args): class DeleteSubnet(command.Command): _description = _("Delete subnet(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'subnet', @@ -446,7 +465,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -473,12 +492,10 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) -# TODO(abhiraut): Use only the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class ListSubnet(command.Lister): _description = _("List subnets") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -493,7 +510,7 @@ def get_parser(self, prog_name): metavar='', dest='ip_version', help=_( - "List only subnets of given IP version in output. " + "List only subnets with the specified IP version. " "Allowed values for IP version are 4 and 6." ), ) @@ -501,12 +518,12 @@ def get_parser(self, prog_name): dhcp_enable_group.add_argument( '--dhcp', action='store_true', - help=_("List subnets which have DHCP enabled"), + help=_("List only subnets which have DHCP enabled"), ) dhcp_enable_group.add_argument( '--no-dhcp', action='store_true', - help=_("List subnets which have DHCP disabled"), + help=_("List only subnets which have DHCP disabled"), ) parser.add_argument( '--service-type', @@ -514,7 +531,7 @@ def get_parser(self, prog_name): action='append', dest='service_types', help=_( - "List only subnets of a given service type in output, " + "List only subnets with the specified service type, " "for example, network:floatingip_agent_gateway. " "Must be a valid device owner value for a network port " "(repeat option to list multiple service types)." @@ -524,8 +541,7 @@ def get_parser(self, prog_name): '--project', metavar='', help=_( - "List only subnets which belong to a given project " - "in output (name or ID)" + "List only subnets with the specified project (name or ID)" ), ) identity_common.add_project_domain_option_to_parser(parser) @@ -533,26 +549,26 @@ def get_parser(self, prog_name): '--network', metavar='', help=_( - "List only subnets which belong to a given network " - "in output (name or ID)" + "List only subnets which belong to the specified network " + "(name or ID)" ), ) parser.add_argument( '--gateway', metavar='', - help=_("List only subnets of given gateway IP in output"), + help=_("List only subnets with the specified gateway IP"), ) parser.add_argument( '--name', metavar='', - help=_("List only subnets of given name in output"), + help=_("List only subnets with the specified name"), ) parser.add_argument( '--subnet-range', metavar='', help=_( - "List only subnets of given subnet range " - "(in CIDR notation) in output. " + "List only subnets with the specified subnet range " + "(in CIDR notation). " "For example, --subnet-range 10.10.0.0/16" ), ) @@ -560,17 +576,21 @@ def get_parser(self, prog_name): '--subnet-pool', metavar='', help=_( - "List only subnets which belong to a given subnet pool " - "in output (name or ID)" + "List only subnets which belong to the specified subnet pool " + "(name or ID)" ), ) _tag.add_tag_filtering_option_to_parser(parser, _('subnets')) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: network_client = self.app.client_manager.network + filters = {} + if parsed_args.ip_version: filters['ip_version'] = parsed_args.ip_version if parsed_args.dhcp: @@ -582,11 +602,12 @@ def take_action(self, parsed_args): if parsed_args.service_types: filters['service_types'] = parsed_args.service_types if parsed_args.project: - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) filters['project_id'] = project_id if parsed_args.network: network_id = network_client.find_network( @@ -604,7 +625,14 @@ def take_action(self, parsed_args): parsed_args.subnet_pool, ignore_missing=False ).id filters['subnetpool_id'] = subnetpool_id + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items _tag.get_tag_filtering_args(parsed_args, filters) + data = network_client.subnets(**filters) headers: tuple[str, ...] = ('ID', 'Name', 'Network', 'Subnet') @@ -646,12 +674,10 @@ def take_action(self, parsed_args): ) -# TODO(abhiraut): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class SetSubnet(common.NeutronCommandWithExtraArgs): _description = _("Set subnet properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'subnet', @@ -708,7 +734,7 @@ def get_parser(self, prog_name): _get_common_parse_arguments(parser, is_create=False) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_subnet(parsed_args.subnet, ignore_missing=False) attrs = _get_attrs( @@ -744,7 +770,7 @@ def take_action(self, parsed_args): class ShowSubnet(command.ShowOne): _description = _("Display subnet details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'subnet', @@ -753,7 +779,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: obj = self.app.client_manager.network.find_subnet( parsed_args.subnet, ignore_missing=False ) @@ -765,7 +793,7 @@ def take_action(self, parsed_args): class UnsetSubnet(common.NeutronUnsetCommandWithExtraArgs): _description = _("Unset subnet properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--allocation-pool', @@ -828,11 +856,11 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_subnet(parsed_args.subnet, ignore_missing=False) - attrs: dict[str, ty.Any] = {} + attrs: dict[str, Any] = {} if parsed_args.gateway: attrs['gateway_ip'] = None if parsed_args.dns_nameservers: diff --git a/openstackclient/network/v2/subnet_pool.py b/openstackclient/network/v2/subnet_pool.py index 2917c40261..9da9a59bca 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -13,24 +13,30 @@ """Subnet pool action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +from openstack.network.v2 import subnet_pool as _subnet_pool from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils from osc_lib.utils import tags as _tag +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common from openstackclient.network import common - LOG = logging.getLogger(__name__) -def _get_columns(item): +def _get_columns( + item: _subnet_pool.SubnetPool, +) -> tuple[tuple[str, ...], tuple[str, ...]]: column_map = { 'default_prefix_length': 'default_prefixlen', 'is_shared': 'shared', @@ -49,7 +55,9 @@ def _get_columns(item): } -def _get_attrs(client_manager, parsed_args): +def _get_attrs( + client_manager: Any, parsed_args: argparse.Namespace +) -> dict[str, Any]: attrs = {} network_client = client_manager.network @@ -83,12 +91,12 @@ def _get_attrs(client_manager, parsed_args): # "subnet pool set" command doesn't support setting project. if 'project' in parsed_args and parsed_args.project is not None: - identity_client = client_manager.identity - project_id = identity_common.find_project( + identity_client = client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) attrs['project_id'] = project_id if parsed_args.description is not None: @@ -100,7 +108,9 @@ def _get_attrs(client_manager, parsed_args): return attrs -def _add_prefix_options(parser, for_create=False): +def _add_prefix_options( + parser: argparse.ArgumentParser, for_create: bool = False +) -> None: parser.add_argument( '--pool-prefix', metavar='', @@ -135,7 +145,7 @@ def _add_prefix_options(parser, for_create=False): ) -def _add_default_options(parser): +def _add_default_options(parser: argparse.ArgumentParser) -> None: default_group = parser.add_mutually_exclusive_group() default_group.add_argument( '--default', @@ -149,12 +159,10 @@ def _add_default_options(parser): ) -# TODO(rtheis): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class CreateSubnetPool(command.ShowOne, common.NeutronCommandWithExtraArgs): _description = _("Create subnet pool") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', metavar='', help=_("Name of the new subnet pool") @@ -205,7 +213,9 @@ def get_parser(self, prog_name): _tag.add_tag_option_to_parser_for_create(parser, _('subnet pool')) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network attrs = _get_attrs(self.app.client_manager, parsed_args) # NeutronServer expects prefixes to be a List @@ -225,7 +235,7 @@ def take_action(self, parsed_args): class DeleteSubnetPool(command.Command): _description = _("Delete subnet pool(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'subnet_pool', @@ -235,7 +245,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network result = 0 @@ -261,12 +271,10 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) -# TODO(rtheis): Use only the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class ListSubnetPool(command.Lister): _description = _("List subnet pools") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -278,26 +286,27 @@ def get_parser(self, prog_name): shared_group.add_argument( '--share', action='store_true', - help=_("List subnet pools shared between projects"), + help=_("List only subnet pools shared between projects"), ) shared_group.add_argument( '--no-share', action='store_true', - help=_("List subnet pools not shared between projects"), + help=_("List only subnet pools not shared between projects"), ) default_group = parser.add_mutually_exclusive_group() default_group.add_argument( '--default', action='store_true', help=_( - "List subnet pools used as the default external subnet pool" + "List only subnet pools used as the default external " + "subnet pool" ), ) default_group.add_argument( '--no-default', action='store_true', help=_( - "List subnet pools not used as the default external " + "List only subnet pools not used as the default external " "subnet pool" ), ) @@ -305,30 +314,33 @@ def get_parser(self, prog_name): '--project', metavar='', help=_( - "List subnet pools according to their project (name or ID)" + "List only subnet pools with the specified project " + "(name or ID)" ), ) identity_common.add_project_domain_option_to_parser(parser) parser.add_argument( '--name', metavar='', - help=_("List only subnet pools of given name in output"), + help=_("List only subnet pools with the specified name"), ) parser.add_argument( '--address-scope', metavar='', help=_( - "List only subnet pools of given address scope " - "in output (name or ID)" + "List only subnet pools with the specified address scope " + "(name or ID)" ), ) _tag.add_tag_filtering_option_to_parser(parser, _('subnet pools')) + pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: network_client = self.app.client_manager.network - filters = {} + filters: dict[str, Any] = {} if parsed_args.share: filters['shared'] = True filters['is_shared'] = True @@ -340,11 +352,12 @@ def take_action(self, parsed_args): elif parsed_args.no_default: filters['is_default'] = False if parsed_args.project: - project_id = identity_common.find_project( + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) filters['project_id'] = project_id if parsed_args.name is not None: filters['name'] = parsed_args.name @@ -353,7 +366,14 @@ def take_action(self, parsed_args): parsed_args.address_scope, ignore_missing=False ) filters['address_scope_id'] = address_scope.id + if parsed_args.marker is not None: + filters['marker'] = parsed_args.marker + if parsed_args.limit is not None: + filters['limit'] = parsed_args.limit + if parsed_args.max_items is not None: + filters['max_items'] = parsed_args.max_items _tag.get_tag_filtering_args(parsed_args, filters) + data = network_client.subnet_pools(**filters) headers: tuple[str, ...] = ('ID', 'Name', 'Prefixes') @@ -387,12 +407,10 @@ def take_action(self, parsed_args): ) -# TODO(rtheis): Use the SDK resource mapped attribute names once the -# OSC minimum requirements include SDK 1.0. class SetSubnetPool(common.NeutronCommandWithExtraArgs): _description = _("Set subnet pool properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'subnet_pool', @@ -438,7 +456,7 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_subnet_pool( parsed_args.subnet_pool, ignore_missing=False @@ -463,7 +481,7 @@ def take_action(self, parsed_args): class ShowSubnetPool(command.ShowOne): _description = _("Display subnet pool details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'subnet_pool', @@ -472,7 +490,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: client = self.app.client_manager.network obj = client.find_subnet_pool( parsed_args.subnet_pool, ignore_missing=False @@ -485,7 +505,7 @@ def take_action(self, parsed_args): class UnsetSubnetPool(command.Command): _description = _("Unset subnet pool properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'subnet_pool', @@ -495,7 +515,7 @@ def get_parser(self, prog_name): _tag.add_tag_option_to_parser_for_unset(parser, _('subnet pool')) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: client = self.app.client_manager.network obj = client.find_subnet_pool( parsed_args.subnet_pool, ignore_missing=False diff --git a/openstackclient/network/v2/taas/__init__.py b/openstackclient/network/v2/taas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/network/v2/taas/tap_flow.py b/openstackclient/network/v2/taas/tap_flow.py new file mode 100644 index 0000000000..9f29886a4f --- /dev/null +++ b/openstackclient/network/v2/taas/tap_flow.py @@ -0,0 +1,270 @@ +# All Rights Reserved 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 logging +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack.network.v2 import tap_flow as _tap_flow +from osc_lib.cli import format_columns +from osc_lib.cli import identity as identity_utils +from osc_lib import exceptions +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + +LOG = logging.getLogger(__name__) + +TAP_FLOW = 'tap_flow' +TAP_FLOWS = f'{TAP_FLOW}s' + +_attr_map = [ + ('id', 'ID', column_util.LIST_BOTH), + ('tenant_id', 'Tenant', column_util.LIST_LONG_ONLY), + ('name', 'Name', column_util.LIST_BOTH), + ('status', 'Status', column_util.LIST_BOTH), + ('source_port', 'source_port', column_util.LIST_BOTH), + ('tap_service_id', 'tap_service_id', column_util.LIST_BOTH), + ('direction', 'Direction', column_util.LIST_BOTH), +] + +_formatters = { + 'vlan_filter': format_columns.ListColumn, +} + + +def _add_updatable_args(parser: argparse.ArgumentParser) -> None: + parser.add_argument('--name', help=_('Name of the tap flow.')) + parser.add_argument( + '--description', help=_('Description of the tap flow.') + ) + + +def _get_columns( + item: _tap_flow.TapFlow, +) -> tuple[tuple[str, ...], tuple[str, ...]]: + column_map: dict[str, str] = {} + hidden_columns = ['location', 'tenant_id'] + return osc_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns + ) + + +class CreateTapFlow(command.ShowOne): + _description = _("Create a new tap flow.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + identity_utils.add_project_owner_option_to_parser(parser) + _add_updatable_args(parser) + parser.add_argument( + '--port', + required=True, + metavar="SOURCE_PORT", + help=_('Source port (name or ID) to monitor.'), + ) + parser.add_argument( + '--tap-service', + required=True, + metavar="TAP_SERVICE", + help=_( + 'Tap service (name or ID) to associate with this tap flow.' + ), + ) + parser.add_argument( + '--direction', + required=True, + metavar="DIRECTION", + choices=['IN', 'OUT', 'BOTH'], + type=lambda s: s.upper(), + help=_( + 'Direction of the Tap flow. Valid options are: ' + 'IN, OUT and BOTH' + ), + ) + parser.add_argument( + '--vlan-filter', + required=False, + metavar="VLAN_FILTER", + help=_('VLAN IDs to mirror in the form of range string.'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if parsed_args.port is not None: + source_port = client.find_port( + parsed_args.port, ignore_missing=False + ).id + attrs['source_port'] = source_port + if parsed_args.tap_service is not None: + tap_service_id = client.find_tap_service( + parsed_args.tap_service, ignore_missing=False + ).id + attrs['tap_service_id'] = tap_service_id + if parsed_args.direction is not None: + attrs['direction'] = parsed_args.direction + if parsed_args.vlan_filter is not None: + attrs['vlan_filter'] = parsed_args.vlan_filter + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + obj = client.create_tap_flow(**attrs) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties(obj, columns) + return display_columns, data + + +class ListTapFlow(command.Lister): + _description = _("List tap flows.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + identity_utils.add_project_owner_option_to_parser(parser) + + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + params = {} + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + params['project_id'] = project_id + objs = client.tap_flows(retrieve_all=True, params=params) + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=True + ) + return ( + headers, + ( + osc_utils.get_dict_properties( + s, columns, formatters=_formatters + ) + for s in objs + ), + ) + + +class ShowTapFlow(command.ShowOne): + _description = _("Show tap flow details.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + TAP_FLOW, + metavar=f"<{TAP_FLOW}>", + help=_("Tap flow to display (name or ID)."), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + id = client.find_tap_flow( + parsed_args.tap_flow, ignore_missing=False + ).id + obj = client.get_tap_flow(id) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteTapFlow(command.Command): + _description = _("Delete a tap flow.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + TAP_FLOW, + metavar=f"<{TAP_FLOW}>", + nargs="+", + help=_("Tap flow to delete (name or ID)."), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + fails = 0 + for id_or_name in parsed_args.tap_flow: + try: + id = client.find_tap_flow(id_or_name, ignore_missing=False).id + client.delete_tap_flow(id) + except Exception as e: + fails += 1 + LOG.error( + "Failed to delete tap flow with name or ID " + "'%(id_or_name)s': %(e)s", + {'id_or_name': id_or_name, 'e': e}, + ) + if fails > 0: + msg = _("Failed to delete %(fails)s of %(total)s tap flow.") % { + 'fails': fails, + 'total': len(parsed_args.tap_flow), + } + raise exceptions.CommandError(msg) + + +class UpdateTapFlow(command.ShowOne): + _description = _("Update a tap flow.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + TAP_FLOW, + metavar=f"<{TAP_FLOW}>", + help=_("Tap flow to modify (name or ID)."), + ) + _add_updatable_args(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + original_t_f = client.find_tap_flow( + parsed_args.tap_flow, ignore_missing=False + ).id + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + obj = client.update_tap_flow(original_t_f, **attrs) + columns, display_columns = column_util.get_columns(obj, _attr_map) + data = osc_utils.get_dict_properties(obj, columns) + return display_columns, data diff --git a/openstackclient/network/v2/taas/tap_mirror.py b/openstackclient/network/v2/taas/tap_mirror.py new file mode 100644 index 0000000000..0111bf2ead --- /dev/null +++ b/openstackclient/network/v2/taas/tap_mirror.py @@ -0,0 +1,255 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 logging +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack.network.v2 import tap_mirror as _tap_mirror +from osc_lib.cli import identity as identity_utils +from osc_lib import exceptions +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network.v2 import port as osc_port +from openstackclient.network.v2.taas import tap_service + +LOG = logging.getLogger(__name__) + +TAP_MIRROR = 'tap_mirror' +TAP_MIRRORS = f'{TAP_MIRROR}s' + +_attr_map = [ + ('id', 'ID', column_util.LIST_BOTH), + ('tenant_id', 'Tenant', column_util.LIST_LONG_ONLY), + ('name', 'Name', column_util.LIST_BOTH), + ('port_id', 'Port', column_util.LIST_BOTH), + ('directions', 'Directions', column_util.LIST_LONG_ONLY), + ('remote_ip', 'Remote IP', column_util.LIST_BOTH), + ('mirror_type', 'Mirror Type', column_util.LIST_LONG_ONLY), +] + + +def _get_columns( + item: _tap_mirror.TapMirror, +) -> tuple[tuple[str, ...], tuple[str, ...]]: + column_map: dict[str, str] = {} + hidden_columns = ['location', 'tenant_id'] + return osc_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns + ) + + +class CreateTapMirror(command.ShowOne): + _description = _("Create a new tap mirror.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + identity_utils.add_project_owner_option_to_parser(parser) + tap_service._add_updatable_args(parser) + parser.add_argument( + '--port', + dest='port_id', + required=True, + metavar="PORT", + help=_('Port (name or ID) to which the Tap Mirror is connected.'), + ) + parser.add_argument( + '--directions', + dest='directions', + action=osc_port.JSONKeyValueAction, + required=True, + help=_( + 'Dictionary of direction and tunnel_id. Valid directions are: ' + 'IN and OUT.' + ), + ) + parser.add_argument( + '--remote-ip', + dest='remote_ip', + required=True, + help=_( + 'Remote IP address for the tap mirror (remote end of the ' + 'GRE or ERSPAN v1 tunnel).' + ), + ) + parser.add_argument( + '--mirror-type', + dest='mirror_type', + required=True, + help=_('Mirror type. Valid values are: gre and erspanv1.'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if parsed_args.port_id is not None: + port_id = client.find_port( + parsed_args.port_id, ignore_missing=False + ).id + attrs['port_id'] = port_id + if parsed_args.directions is not None: + attrs['directions'] = parsed_args.directions + if parsed_args.remote_ip is not None: + attrs['remote_ip'] = parsed_args.remote_ip + if parsed_args.mirror_type is not None: + attrs['mirror_type'] = parsed_args.mirror_type + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + obj = client.create_tap_mirror(**attrs) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties(obj, columns) + return display_columns, data + + +class ListTapMirror(command.Lister): + _description = _("List tap mirrors.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + identity_utils.add_project_owner_option_to_parser(parser) + + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + params = {} + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + params['project_id'] = project_id + objs = client.tap_mirrors(retrieve_all=True, params=params) + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=True + ) + return ( + headers, + (osc_utils.get_dict_properties(s, columns) for s in objs), + ) + + +class ShowTapMirror(command.ShowOne): + _description = _("Show tap mirror details.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + TAP_MIRROR, + metavar=f"<{TAP_MIRROR}>", + help=_("Tap mirror to display (name or ID)."), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + id = client.find_tap_mirror( + parsed_args.tap_mirror, ignore_missing=False + ).id + obj = client.get_tap_mirror(id) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteTapMirror(command.Command): + _description = _("Delete a tap mirror.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + TAP_MIRROR, + metavar=f"<{TAP_MIRROR}>", + nargs="+", + help=_("Tap mirror to delete (name or ID)."), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + fails = 0 + for id_or_name in parsed_args.tap_mirror: + try: + id = client.find_tap_mirror( + id_or_name, ignore_missing=False + ).id + + client.delete_tap_mirror(id) + LOG.warning("Tap Mirror %(id)s deleted", {'id': id}) + except Exception as e: + fails += 1 + LOG.error( + "Failed to delete Tap Mirror with name or ID " + "'%(id_or_name)s': %(e)s", + {'id_or_name': id_or_name, 'e': e}, + ) + if fails > 0: + msg = _("Failed to delete %(fails)s of %(total)s Tap Mirror.") % { + 'fails': fails, + 'total': len(parsed_args.tap_mirror), + } + raise exceptions.CommandError(msg) + + +class UpdateTapMirror(command.ShowOne): + _description = _("Update a tap mirror.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + TAP_MIRROR, + metavar=f"<{TAP_MIRROR}>", + help=_("Tap mirror to modify (name or ID)."), + ) + tap_service._add_updatable_args(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + original_t_s = client.find_tap_mirror( + parsed_args.tap_mirror, ignore_missing=False + ).id + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + obj = client.update_tap_mirror(original_t_s, **attrs) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties(obj, columns) + return display_columns, data diff --git a/openstackclient/network/v2/taas/tap_service.py b/openstackclient/network/v2/taas/tap_service.py new file mode 100644 index 0000000000..8f2a7173fb --- /dev/null +++ b/openstackclient/network/v2/taas/tap_service.py @@ -0,0 +1,229 @@ +# All Rights Reserved 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 logging +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack.network.v2 import tap_service as _tap_service +from osc_lib.cli import identity as identity_utils +from osc_lib import exceptions +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + +LOG = logging.getLogger(__name__) + +TAP_SERVICE = 'tap_service' +TAP_SERVICES = f'{TAP_SERVICE}s' + +_attr_map = [ + ('id', 'ID', column_util.LIST_BOTH), + ('tenant_id', 'Tenant', column_util.LIST_LONG_ONLY), + ('name', 'Name', column_util.LIST_BOTH), + ('port_id', 'Port', column_util.LIST_BOTH), + ('status', 'Status', column_util.LIST_BOTH), +] + + +def _add_updatable_args(parser: argparse.ArgumentParser) -> None: + parser.add_argument('--name', help=_('Name of the tap service.')) + parser.add_argument( + '--description', help=_('Description of the tap service.') + ) + + +def _get_columns( + item: _tap_service.TapService, +) -> tuple[tuple[str, ...], tuple[str, ...]]: + column_map: dict[str, str] = {} + hidden_columns = ['location', 'tenant_id'] + return osc_utils.get_osc_show_columns_for_sdk_resource( + item, column_map, hidden_columns + ) + + +class CreateTapService(command.ShowOne): + _description = _("Create a new tap service.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + identity_utils.add_project_owner_option_to_parser(parser) + _add_updatable_args(parser) + parser.add_argument( + '--port', + dest='port_id', + required=True, + metavar="PORT", + help=_('Port (name or ID) to connect to the tap service.'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + if parsed_args.port_id is not None: + port_id = client.find_port( + parsed_args.port_id, ignore_missing=False + ).id + attrs['port_id'] = port_id + if 'project' in parsed_args and parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + obj = client.create_tap_service(**attrs) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties(obj, columns) + return display_columns, data + + +class ListTapService(command.Lister): + _description = _("List tap services.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + identity_utils.add_project_owner_option_to_parser(parser) + + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + params = {} + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + params['project_id'] = project_id + objs = client.tap_services(retrieve_all=True, params=params) + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=True + ) + return ( + headers, + (osc_utils.get_dict_properties(s, columns) for s in objs), + ) + + +class ShowTapService(command.ShowOne): + _description = _("Show tap service details.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + TAP_SERVICE, + metavar=f"<{TAP_SERVICE}>", + help=_("Tap service to display (name or ID)."), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + id = client.find_tap_service( + parsed_args.tap_service, ignore_missing=False + ).id + obj = client.get_tap_service(id) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteTapService(command.Command): + _description = _("Delete a tap service.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + TAP_SERVICE, + metavar=f"<{TAP_SERVICE}>", + nargs="+", + help=_("Tap service to delete (name or ID)."), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + fails = 0 + for id_or_name in parsed_args.tap_service: + try: + id = client.find_tap_service( + id_or_name, ignore_missing=False + ).id + + client.delete_tap_service(id) + LOG.warning("Tap service %(id)s deleted", {'id': id}) + except Exception as e: + fails += 1 + LOG.error( + "Failed to delete tap service with name or ID " + "'%(id_or_name)s': %(e)s", + {'id_or_name': id_or_name, 'e': e}, + ) + if fails > 0: + msg = _("Failed to delete %(fails)s of %(total)s tap service.") % { + 'fails': fails, + 'total': len(parsed_args.tap_service), + } + raise exceptions.CommandError(msg) + + +class UpdateTapService(command.ShowOne): + _description = _("Update a tap service.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + TAP_SERVICE, + metavar=f"<{TAP_SERVICE}>", + help=_("Tap service to modify (name or ID)."), + ) + _add_updatable_args(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + original_t_s = client.find_tap_service( + parsed_args.tap_service, ignore_missing=False + ).id + attrs = {} + if parsed_args.name is not None: + attrs['name'] = parsed_args.name + if parsed_args.description is not None: + attrs['description'] = parsed_args.description + obj = client.update_tap_service(original_t_s, **attrs) + display_columns, columns = _get_columns(obj) + data = osc_utils.get_dict_properties(obj, columns) + return display_columns, data diff --git a/openstackclient/network/v2/vpnaas/__init__.py b/openstackclient/network/v2/vpnaas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/network/v2/vpnaas/endpoint_group.py b/openstackclient/network/v2/vpnaas/endpoint_group.py new file mode 100644 index 0000000000..45458abe8b --- /dev/null +++ b/openstackclient/network/v2/vpnaas/endpoint_group.py @@ -0,0 +1,244 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from osc_lib.cli import identity as identity_utils +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +_attr_map = [ + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('type', 'Type', column_util.LIST_BOTH), + ('endpoints', 'Endpoints', column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), +] + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'type': 'Type', + 'endpoints': 'Endpoints', + 'description': 'Description', + 'project_id': 'Project', +} + + +class CreateEndpointGroup(command.ShowOne): + _description = _("Create an endpoint group") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the endpoint group'), + ) + parser.add_argument( + 'name', metavar='', help=_('Name for the endpoint group') + ) + parser.add_argument( + '--type', + required=True, + choices=['subnet', 'cidr'], + help=_( + 'Type of endpoints in group (e.g. subnet, cidr, network, ' + 'router). Currently only subnet and cidr are supported.' + ), + ) + parser.add_argument( + '--value', + action='append', + dest='endpoints', + required=True, + help=_( + 'Endpoint(s) for the group. Must all be of the same type. ' + '(--value) option can be repeated' + ), + ) + identity_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs: dict[str, Any] = {} + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + if parsed_args.description: + attrs['description'] = parsed_args.description + + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + attrs['type'] = parsed_args.type + if parsed_args.type == 'subnet': + _subnet_ids = [ + client.find_subnet(endpoint, ignore_missing=False)['id'] + for endpoint in parsed_args.endpoints + ] + attrs['endpoints'] = _subnet_ids + else: + attrs['endpoints'] = parsed_args.endpoints + obj = client.create_vpn_endpoint_group(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteEndpointGroup(command.Command): + _description = _("Delete endpoint group(s)") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'endpoint_group', + metavar='', + nargs='+', + help=_('Endpoint group(s) to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 + for endpoint in parsed_args.endpoint_group: + try: + endpoint_id = client.find_vpn_endpoint_group( + endpoint, ignore_missing=False + ).id + client.delete_vpn_endpoint_group(endpoint_id) + except Exception as e: + result += 1 + print( + f"Failed to delete endpoint group with " + f"name or ID {endpoint}: {e}" + ) + + if result > 0: + total = len(parsed_args.endpoint_group) + msg = _( + "%(result)s of %(total)s endpoint group failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListEndpointGroup(command.Lister): + _description = _("List endpoint groups that belong to a given project") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + obj = client.vpn_endpoint_groups() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long + ) + return (headers, (utils.get_dict_properties(s, columns) for s in obj)) + + +class SetEndpointGroup(command.Command): + _description = _("Set endpoint group properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--description', + metavar='', + help=_('Description for the endpoint group'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('Set a name for the endpoint group'), + ) + parser.add_argument( + 'endpoint_group', + metavar='', + help=_('Endpoint group to set (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + attrs = {} + if parsed_args.description: + attrs['description'] = parsed_args.description + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + endpoint_id = client.find_vpn_endpoint_group( + parsed_args.endpoint_group, ignore_missing=False + )['id'] + try: + client.update_vpn_endpoint_group(endpoint_id, **attrs) + except Exception as e: + msg = _( + "Failed to set endpoint group %(endpoint_group)s: %(e)s" + ) % {'endpoint_group': parsed_args.endpoint_group, 'e': e} + raise exceptions.CommandError(msg) + + +class ShowEndpointGroup(command.ShowOne): + _description = _("Display endpoint group details") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'endpoint_group', + metavar='', + help=_('Endpoint group to display (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + obj = client.find_vpn_endpoint_group( + parsed_args.endpoint_group, ignore_missing=False + ) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns) + return (display_columns, data) diff --git a/openstackclient/network/v2/vpnaas/ikepolicy.py b/openstackclient/network/v2/vpnaas/ikepolicy.py new file mode 100644 index 0000000000..516a6beea2 --- /dev/null +++ b/openstackclient/network/v2/vpnaas/ikepolicy.py @@ -0,0 +1,351 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from osc_lib.cli import identity as identity_utils +from osc_lib.cli import parseractions +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network.v2.vpnaas import utils as vpn_utils + + +_attr_map = [ + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('auth_algorithm', 'Authentication Algorithm', column_util.LIST_BOTH), + ('encryption_algorithm', 'Encryption Algorithm', column_util.LIST_BOTH), + ('ike_version', 'IKE Version', column_util.LIST_BOTH), + ('pfs', 'Perfect Forward Secrecy (PFS)', column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ( + 'phase1_negotiation_mode', + 'Phase1 Negotiation Mode', + column_util.LIST_LONG_ONLY, + ), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('lifetime', 'Lifetime', column_util.LIST_LONG_ONLY), +] + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'auth_algorithm': 'Authentication Algorithm', + 'encryption_algorithm': 'Encryption Algorithm', + 'ike_version': 'IKE Version', + 'pfs': 'Perfect Forward Secrecy (PFS)', + 'phase1_negotiation_mode': 'Phase1 Negotiation Mode', + 'lifetime': 'Lifetime', + 'description': 'Description', + 'project_id': 'Project', +} + +_auth_algorithms = [ + 'sha1', + 'sha256', + 'sha384', + 'sha512', + 'aes-xcbc', + 'aes-cmac', +] + +_encryption_algorithms = [ + '3des', + 'aes-128', + 'aes-192', + 'aes-256', + 'aes-128-ccm-8', + 'aes-192-ccm-8', + 'aes-256-ccm-8', + 'aes-128-ccm-12', + 'aes-192-ccm-12', + 'aes-256-ccm-12', + 'aes-128-ccm-16', + 'aes-192-ccm-16', + 'aes-256-ccm-16', + 'aes-128-gcm-8', + 'aes-192-gcm-8', + 'aes-256-gcm-8', + 'aes-128-gcm-12', + 'aes-192-gcm-12', + 'aes-256-gcm-12', + 'aes-128-gcm-16', + 'aes-192-gcm-16', + 'aes-256-gcm-16', + 'aes-128-ctr', + 'aes-192-ctr', + 'aes-256-ctr', +] + +_pfs_groups = [ + 'group2', + 'group5', + 'group14', + 'group15', + 'group16', + 'group17', + 'group18', + 'group19', + 'group20', + 'group21', + 'group22', + 'group23', + 'group24', + 'group25', + 'group26', + 'group27', + 'group28', + 'group29', + 'group30', + 'group31', +] + + +def _convert_to_lowercase(string: str) -> str: + return string.lower() + + +def _get_common_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: + parser.add_argument( + '--description', + metavar='', + help=_('Description of the IKE policy'), + ) + parser.add_argument( + '--auth-algorithm', + choices=_auth_algorithms, + type=_convert_to_lowercase, + help=_('Authentication algorithm'), + ) + parser.add_argument( + '--encryption-algorithm', + choices=_encryption_algorithms, + type=_convert_to_lowercase, + help=_('Encryption algorithm'), + ) + parser.add_argument( + '--phase1-negotiation-mode', + choices=['main', 'aggressive'], + type=_convert_to_lowercase, + help=_('IKE Phase1 negotiation mode'), + ) + parser.add_argument( + '--ike-version', + choices=['v1', 'v2'], + type=_convert_to_lowercase, + help=_('IKE version for the policy'), + ) + parser.add_argument( + '--pfs', + choices=_pfs_groups, + type=_convert_to_lowercase, + help=_('Perfect Forward Secrecy'), + ) + parser.add_argument( + '--lifetime', + metavar='units=,value=', + action=parseractions.MultiKeyValueAction, + optional_keys=['units', 'value'], + help=_( + "IKE lifetime attributes. " + "'units'-seconds, default:seconds. " + "'value'-non negative integer, default:3600." + ), + ) + return parser + + +def _get_common_attrs(parsed_args: argparse.Namespace) -> dict[str, Any]: + attrs: dict[str, Any] = {} + if parsed_args.description: + attrs['description'] = parsed_args.description + if parsed_args.auth_algorithm: + attrs['auth_algorithm'] = parsed_args.auth_algorithm + if parsed_args.encryption_algorithm: + attrs['encryption_algorithm'] = parsed_args.encryption_algorithm + if parsed_args.phase1_negotiation_mode: + attrs['phase1_negotiation_mode'] = parsed_args.phase1_negotiation_mode + if parsed_args.ike_version: + attrs['ike_version'] = parsed_args.ike_version + if parsed_args.pfs: + attrs['pfs'] = parsed_args.pfs + if parsed_args.lifetime: + vpn_utils.validate_lifetime_dict(parsed_args.lifetime) + attrs['lifetime'] = parsed_args.lifetime + return attrs + + +class CreateIKEPolicy(command.ShowOne): + _description = _("Create an IKE policy") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + 'name', metavar='', help=_('Name of the IKE policy') + ) + identity_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs = _get_common_attrs(parsed_args) + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + obj = client.create_vpn_ike_policy(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id', 'units', 'value'] + ) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteIKEPolicy(command.Command): + _description = _("Delete IKE policy (policies)") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'ikepolicy', + metavar='', + nargs='+', + help=_('IKE policy to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 + for ike in parsed_args.ikepolicy: + try: + ike_id = client.find_vpn_ike_policy(ike, ignore_missing=False)[ + 'id' + ] + client.delete_vpn_ike_policy(ike_id) + except Exception as e: + result += 1 + print( + f"Failed to delete IKE policy with name or ID {ike}: {e}" + ) + + if result > 0: + total = len(parsed_args.ikepolicy) + msg = _("%(result)s of %(total)s IKE policy failed to delete.") % { + 'result': result, + 'total': total, + } + raise exceptions.CommandError(msg) + + +class ListIKEPolicy(command.Lister): + _description = _("List IKE policies that belong to a given project") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + help=_("List additional fields in output"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + obj = client.vpn_ike_policies() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long + ) + return (headers, (utils.get_dict_properties(s, columns) for s in obj)) + + +class SetIKEPolicy(command.Command): + _description = _("Set IKE policy properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + '--name', metavar='', help=_('Name of the IKE policy') + ) + parser.add_argument( + 'ikepolicy', + metavar='', + help=_('IKE policy to set (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + attrs = _get_common_attrs(parsed_args) + if parsed_args.name: + attrs['name'] = parsed_args.name + ike_id = client.find_vpn_ike_policy( + parsed_args.ikepolicy, ignore_missing=False + )['id'] + try: + client.update_vpn_ike_policy(ike_id, **attrs) + except Exception as e: + msg = _("Failed to set IKE policy '%(ike)s': %(e)s") % { + 'ike': parsed_args.ikepolicy, + 'e': e, + } + raise exceptions.CommandError(msg) + + +class ShowIKEPolicy(command.ShowOne): + _description = _("Display IKE policy details") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'ikepolicy', + metavar='', + help=_('IKE policy to display (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + obj = client.find_vpn_ike_policy( + parsed_args.ikepolicy, ignore_missing=False + ) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id', 'units', 'value'] + ) + data = utils.get_dict_properties(obj, columns) + return (display_columns, data) diff --git a/openstackclient/network/v2/vpnaas/ipsec_site_connection.py b/openstackclient/network/v2/vpnaas/ipsec_site_connection.py new file mode 100644 index 0000000000..6b1b942a1e --- /dev/null +++ b/openstackclient/network/v2/vpnaas/ipsec_site_connection.py @@ -0,0 +1,450 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack.network import v2 as network_v2 +from osc_lib.cli import format_columns +from osc_lib.cli import identity as identity_utils +from osc_lib.cli import parseractions +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network.v2.vpnaas import utils as vpn_utils + +_formatters = {'peer_cidrs': format_columns.ListColumn} + +_attr_map = [ + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('peer_address', 'Peer Address', column_util.LIST_BOTH), + ('auth_mode', 'Authentication Algorithm', column_util.LIST_BOTH), + ('status', 'Status', column_util.LIST_BOTH), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('peer_cidrs', 'Peer CIDRs', column_util.LIST_LONG_ONLY), + ('vpnservice_id', 'VPN Service', column_util.LIST_LONG_ONLY), + ('ipsecpolicy_id', 'IPSec Policy', column_util.LIST_LONG_ONLY), + ('ikepolicy_id', 'IKE Policy', column_util.LIST_LONG_ONLY), + ('mtu', 'MTU', column_util.LIST_LONG_ONLY), + ('initiator', 'Initiator', column_util.LIST_LONG_ONLY), + ('is_admin_state_up', 'State', column_util.LIST_LONG_ONLY), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('psk', 'Pre-shared Key', column_util.LIST_LONG_ONLY), + ('route_mode', 'Route Mode', column_util.LIST_LONG_ONLY), + ('local_id', 'Local ID', column_util.LIST_LONG_ONLY), + ('peer_id', 'Peer ID', column_util.LIST_LONG_ONLY), + ( + 'local_ep_group_id', + 'Local Endpoint Group ID', + column_util.LIST_LONG_ONLY, + ), + ('peer_ep_group_id', 'Peer Endpoint Group ID', column_util.LIST_LONG_ONLY), + ('dpd', 'DPD', column_util.LIST_LONG_ONLY), +] + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'peer_address': 'Peer Address', + 'auth_mode': 'Authentication Algorithm', + 'status': 'Status', + 'peer_cidrs': 'Peer CIDRs', + 'vpnservice_id': 'VPN Service', + 'ipsecpolicy_id': 'IPSec Policy', + 'ikepolicy_id': 'IKE Policy', + 'mtu': 'MTU', + 'initiator': 'Initiator', + 'is_admin_state_up': 'State', + 'psk': 'Pre-shared Key', + 'route_mode': 'Route Mode', + 'local_id': 'Local ID', + 'peer_id': 'Peer ID', + 'local_ep_group_id': 'Local Endpoint Group ID', + 'peer_ep_group_id': 'Peer Endpoint Group ID', + 'description': 'Description', + 'project_id': 'Project', + 'dpd': 'DPD', +} + + +def _convert_to_lowercase(string: str) -> str: + return string.lower() + + +def _get_common_parser( + parser: argparse.ArgumentParser, is_create: bool = True +) -> argparse.ArgumentParser: + parser.add_argument( + '--description', + metavar='', + help=_('Description for the connection'), + ) + parser.add_argument( + '--dpd', + metavar="action=,interval=,timeout=", + action=parseractions.MultiKeyValueAction, + optional_keys=['action', 'interval', 'timeout'], + help=_( + "IPSec Connection Dead Peer Detection attributes. " + "'action'-hold,clear,disabled,restart,restart-by-peer. " + "'interval' and 'timeout' are non negative integers. " + "'interval' should be less than 'timeout' value. " + "'action', default:hold 'interval', default:30, " + "'timeout', default:120." + ), + ) + parser.add_argument('--mtu', help=_('MTU size for the connection')) + parser.add_argument( + '--initiator', + choices=['bi-directional', 'response-only'], + type=_convert_to_lowercase, + help=_('Initiator state'), + ) + peer_group = parser.add_mutually_exclusive_group() + peer_group.add_argument( + '--peer-cidr', + dest='peer_cidrs', + help=_( + 'Remote subnet(s) in CIDR format. ' + 'Cannot be specified when using endpoint groups. Only ' + 'applicable, if subnet provided for VPN service.' + ), + ) + peer_group.add_argument( + '--local-endpoint-group', + help=_( + 'Local endpoint group (name or ID) with subnet(s) ' + 'for IPsec connection' + ), + ) + parser.add_argument( + '--peer-endpoint-group', + help=_( + 'Peer endpoint group (name or ID) with CIDR(s) for ' + 'IPSec connection' + ), + ) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', action='store_true', help=_("Enable IPSec site connection") + ) + admin_group.add_argument( + '--disable', + action='store_true', + help=_("Disable IPSec site connection"), + ) + parser.add_argument( + '--local-id', + help=_( + 'An ID to be used instead of the external IP ' + 'address for a virtual router' + ), + ) + return parser + + +def _get_common_attrs( + network_client: network_v2.Proxy, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} + if parsed_args.description: + attrs['description'] = str(parsed_args.description) + if parsed_args.mtu: + attrs['mtu'] = parsed_args.mtu + if parsed_args.enable: + attrs['admin_state_up'] = True + if parsed_args.disable: + attrs['admin_state_up'] = False + if parsed_args.initiator: + attrs['initiator'] = parsed_args.initiator + if parsed_args.dpd: + vpn_utils.validate_dpd_dict(parsed_args.dpd) + attrs['dpd'] = parsed_args.dpd + if parsed_args.local_endpoint_group: + _local_epg = network_client.find_vpn_endpoint_group( + parsed_args.local_endpoint_group, ignore_missing=False + ).id + attrs['local_ep_group_id'] = _local_epg + if parsed_args.peer_endpoint_group: + _peer_epg = network_client.find_vpn_endpoint_group( + parsed_args.peer_endpoint_group, ignore_missing=False + ).id + attrs['peer_ep_group_id'] = _peer_epg + if parsed_args.peer_cidrs: + attrs['peer_cidrs'] = parsed_args.peer_cidrs + if parsed_args.local_id: + attrs['local_id'] = parsed_args.local_id + return attrs + + +class CreateIPsecSiteConnection(command.ShowOne): + _description = _("Create an IPsec site connection") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + '--peer-id', + required=True, + help=_( + 'Peer router identity for authentication. Can be ' + 'IPv4/IPv6 address, e-mail address, key id, or FQDN' + ), + ) + parser.add_argument( + '--peer-address', + required=True, + help=_('Peer gateway public IPv4/IPv6 address or FQDN'), + ) + parser.add_argument( + '--psk', required=True, help=_('Pre-shared key string.') + ) + parser.add_argument( + '--vpnservice', + metavar='VPNSERVICE', + required=True, + help=_( + 'VPN service instance associated with this ' + 'connection (name or ID)' + ), + ) + parser.add_argument( + '--ikepolicy', + metavar='IKEPOLICY', + required=True, + help=_('IKE policy associated with this connection (name or ID)'), + ) + parser.add_argument( + '--ipsecpolicy', + metavar='IPSECPOLICY', + required=True, + help=_( + 'IPsec policy associated with this connection (name or ID)' + ), + ) + parser.add_argument( + 'name', + metavar='', + help=_('Set friendly name for the connection'), + ) + identity_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs = _get_common_attrs(client, parsed_args) + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + if parsed_args.vpnservice: + _vpnservice_id = client.find_vpn_service( + parsed_args.vpnservice, ignore_missing=False + )['id'] + attrs['vpnservice_id'] = _vpnservice_id + if parsed_args.ikepolicy: + _ikepolicy_id = client.find_vpn_ike_policy( + parsed_args.ikepolicy, ignore_missing=False + )['id'] + attrs['ikepolicy_id'] = _ikepolicy_id + if parsed_args.ipsecpolicy: + _ipsecpolicy_id = client.find_vpn_ipsec_policy( + parsed_args.ipsecpolicy, ignore_missing=False + )['id'] + attrs['ipsecpolicy_id'] = _ipsecpolicy_id + if parsed_args.peer_id: + attrs['peer_id'] = parsed_args.peer_id + if parsed_args.peer_address: + attrs['peer_address'] = parsed_args.peer_address + if parsed_args.psk: + attrs['psk'] = parsed_args.psk + if parsed_args.name: + attrs['name'] = parsed_args.name + if bool(parsed_args.local_endpoint_group) != bool( + parsed_args.peer_endpoint_group + ): + message = _("You must specify both local and peer endpoint groups") + raise exceptions.CommandError(message) + if not parsed_args.peer_cidrs and not parsed_args.local_endpoint_group: + message = _("You must specify endpoint groups or peer CIDR(s)") + raise exceptions.CommandError(message) + obj = client.create_vpn_ipsec_site_connection(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, + _attr_map_dict, + ['location', 'tenant_id', 'action', 'timeout', 'interval'], + ) + data = utils.get_dict_properties(obj, columns, formatters=_formatters) + return display_columns, data + + +class DeleteIPsecSiteConnection(command.Command): + _description = _("Delete IPsec site connection(s)") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'ipsec_site_connection', + metavar='', + nargs='+', + help=_('IPsec site connection to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 + for ipsec_conn in parsed_args.ipsec_site_connection: + try: + ipsec_con_id = client.find_vpn_ipsec_site_connection( + ipsec_conn, ignore_missing=False + )['id'] + client.delete_vpn_ipsec_site_connection(ipsec_con_id) + except Exception as e: + result += 1 + print( + f"Failed to delete IPsec site connection with " + f"name or ID {ipsec_conn}: {e}" + ) + + if result > 0: + total = len(parsed_args.ipsec_site_connection) + msg = _( + "%(result)s of %(total)s IPsec site connection failed " + "to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListIPsecSiteConnection(command.Lister): + _description = _( + "List IPsec site connections that belong to a given project" + ) + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + obj = client.vpn_ipsec_site_connections() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long + ) + return ( + headers, + ( + utils.get_dict_properties(s, columns, formatters=_formatters) + for s in obj + ), + ) + + +class SetIPsecSiteConnection(command.Command): + _description = _("Set IPsec site connection properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + '--peer-id', + help=_( + 'Peer router identity for authentication. Can be ' + 'IPv4/IPv6 address, e-mail address, key id, or FQDN' + ), + ) + parser.add_argument( + '--peer-address', + help=_('Peer gateway public IPv4/IPv6 address or FQDN'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('Set friendly name for the connection'), + ) + parser.add_argument( + 'ipsec_site_connection', + metavar='', + help=_('IPsec site connection to set (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + attrs = _get_common_attrs(client, parsed_args) + if parsed_args.peer_id: + attrs['peer_id'] = parsed_args.peer_id + if parsed_args.peer_address: + attrs['peer_address'] = parsed_args.peer_address + if parsed_args.name: + attrs['name'] = parsed_args.name + ipsec_conn_id = client.find_vpn_ipsec_site_connection( + parsed_args.ipsec_site_connection, ignore_missing=False + )['id'] + try: + client.update_vpn_ipsec_site_connection(ipsec_conn_id, **attrs) + except Exception as e: + msg = _( + "Failed to set IPsec site connection '%(ipsec_conn)s': %(e)s" + ) % {'ipsec_conn': parsed_args.ipsec_site_connection, 'e': e} + raise exceptions.CommandError(msg) + + +class ShowIPsecSiteConnection(command.ShowOne): + _description = _("Show information of a given IPsec site connection") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'ipsec_site_connection', + metavar='', + help=_('IPsec site connection to display (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + obj = client.find_vpn_ipsec_site_connection( + parsed_args.ipsec_site_connection, ignore_missing=False + ) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, + _attr_map_dict, + ['location', 'tenant_id', 'action', 'timeout', 'interval'], + ) + data = utils.get_dict_properties(obj, columns, formatters=_formatters) + return (display_columns, data) diff --git a/openstackclient/network/v2/vpnaas/ipsecpolicy.py b/openstackclient/network/v2/vpnaas/ipsecpolicy.py new file mode 100644 index 0000000000..042aa492fa --- /dev/null +++ b/openstackclient/network/v2/vpnaas/ipsecpolicy.py @@ -0,0 +1,361 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from osc_lib.cli import identity as identity_utils +from osc_lib.cli import parseractions +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common +from openstackclient.network.v2.vpnaas import utils as vpn_utils + + +_attr_map = [ + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('auth_algorithm', 'Authentication Algorithm', column_util.LIST_BOTH), + ('encapsulation_mode', 'Encapsulation Mode', column_util.LIST_BOTH), + ('transform_protocol', 'Transform Protocol', column_util.LIST_BOTH), + ('encryption_algorithm', 'Encryption Algorithm', column_util.LIST_BOTH), + ('pfs', 'Perfect Forward Secrecy (PFS)', column_util.LIST_LONG_ONLY), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('lifetime', 'Lifetime', column_util.LIST_LONG_ONLY), +] + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'auth_algorithm': 'Authentication Algorithm', + 'encapsulation_mode': 'Encapsulation Mode', + 'transform_protocol': 'Transform Protocol', + 'encryption_algorithm': 'Encryption Algorithm', + 'pfs': 'Perfect Forward Secrecy (PFS)', + 'lifetime': 'Lifetime', + 'description': 'Description', + 'project_id': 'Project', +} + +_auth_algorithms = [ + 'sha1', + 'sha256', + 'sha384', + 'sha512', + 'aes-xcbc', + 'aes-cmac', +] + +_encryption_algorithms = [ + '3des', + 'aes-128', + 'aes-192', + 'aes-256', + 'aes-128-ccm-8', + 'aes-192-ccm-8', + 'aes-256-ccm-8', + 'aes-128-ccm-12', + 'aes-192-ccm-12', + 'aes-256-ccm-12', + 'aes-128-ccm-16', + 'aes-192-ccm-16', + 'aes-256-ccm-16', + 'aes-128-gcm-8', + 'aes-192-gcm-8', + 'aes-256-gcm-8', + 'aes-128-gcm-12', + 'aes-192-gcm-12', + 'aes-256-gcm-12', + 'aes-128-gcm-16', + 'aes-192-gcm-16', + 'aes-256-gcm-16', + 'aes-128-ctr', + 'aes-192-ctr', + 'aes-256-ctr', +] + +_pfs_groups = [ + 'group2', + 'group5', + 'group14', + 'group15', + 'group16', + 'group17', + 'group18', + 'group19', + 'group20', + 'group21', + 'group22', + 'group23', + 'group24', + 'group25', + 'group26', + 'group27', + 'group28', + 'group29', + 'group30', + 'group31', +] + + +def _convert_to_lowercase(string: str) -> str: + return string.lower() + + +def _get_common_parser(parser: argparse.ArgumentParser) -> None: + parser.add_argument( + '--description', + metavar='', + help=_('Description of the IPsec policy'), + ) + parser.add_argument( + '--auth-algorithm', + choices=_auth_algorithms, + type=_convert_to_lowercase, + help=_('Authentication algorithm for IPsec policy'), + ) + parser.add_argument( + '--encapsulation-mode', + choices=['tunnel', 'transport'], + type=_convert_to_lowercase, + help=_('Encapsulation mode for IPsec policy'), + ) + parser.add_argument( + '--encryption-algorithm', + choices=_encryption_algorithms, + type=_convert_to_lowercase, + help=_('Encryption algorithm for IPsec policy'), + ) + parser.add_argument( + '--lifetime', + metavar='units=,value=', + action=parseractions.MultiKeyValueAction, + optional_keys=['units', 'value'], + help=_( + "IPsec lifetime attributes. " + "'units'-seconds, default:seconds. " + "'value'-non negative integer, default:3600." + ), + ) + parser.add_argument( + '--pfs', + choices=_pfs_groups, + type=_convert_to_lowercase, + help=_('Perfect Forward Secrecy for IPsec policy'), + ) + parser.add_argument( + '--transform-protocol', + type=_convert_to_lowercase, + choices=['esp', 'ah', 'ah-esp'], + help=_('Transform protocol for IPsec policy'), + ) + + +def _get_common_attrs(parsed_args: argparse.Namespace) -> dict[str, Any]: + attrs: dict[str, Any] = {} + if parsed_args.description: + attrs['description'] = str(parsed_args.description) + if parsed_args.auth_algorithm: + attrs['auth_algorithm'] = parsed_args.auth_algorithm + if parsed_args.encapsulation_mode: + attrs['encapsulation_mode'] = parsed_args.encapsulation_mode + if parsed_args.transform_protocol: + attrs['transform_protocol'] = parsed_args.transform_protocol + if parsed_args.encryption_algorithm: + attrs['encryption_algorithm'] = parsed_args.encryption_algorithm + if parsed_args.pfs: + attrs['pfs'] = parsed_args.pfs + if parsed_args.lifetime: + vpn_utils.validate_lifetime_dict(parsed_args.lifetime) + attrs['lifetime'] = parsed_args.lifetime + return attrs + + +class CreateIPsecPolicy(command.ShowOne): + _description = _("Create an IPsec policy") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + 'name', metavar='', help=_('Name of the IPsec policy') + ) + identity_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs = _get_common_attrs(parsed_args) + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + obj = client.create_vpn_ipsec_policy(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, + _attr_map_dict, + [ + 'location', + 'tenant_id', + 'phase1_negotiation_mode', + 'units', + 'value', + ], + ) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteIPsecPolicy(command.Command): + _description = _("Delete IPsec policy(policies)") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'ipsecpolicy', + metavar='', + nargs='+', + help=_('ipsec policy to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 + for ipsec in parsed_args.ipsecpolicy: + try: + ipsec_id = client.find_vpn_ipsec_policy( + ipsec, ignore_missing=False + )['id'] + client.delete_vpn_ipsec_policy(ipsec_id) + except Exception as e: + result += 1 + print( + f"Failed to delete IPsec policy with " + f"name or ID {ipsec}: {e}" + ) + + if result > 0: + total = len(parsed_args.ipsecpolicy) + msg = _( + "%(result)s of %(total)s IPsec policy failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListIPsecPolicy(command.Lister): + _description = _("List IPsec policies that belong to a given project") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + obj = client.vpn_ipsec_policies() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long + ) + return (headers, (utils.get_dict_properties(s, columns) for s in obj)) + + +class SetIPsecPolicy(command.Command): + _description = _("Set IPsec policy properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + '--name', metavar='', help=_('Name of the IPsec policy') + ) + parser.add_argument( + 'ipsecpolicy', + metavar='', + help=_('IPsec policy to set (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + attrs = _get_common_attrs(parsed_args) + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + ipsec_id = client.find_vpn_ipsec_policy( + parsed_args.ipsecpolicy, ignore_missing=False + )['id'] + try: + client.update_vpn_ipsec_policy(ipsec_id, **attrs) + except Exception as e: + msg = _("Failed to set IPsec policy '%(ipsec)s': %(e)s") % { + 'ipsec': parsed_args.ipsecpolicy, + 'e': e, + } + raise exceptions.CommandError(msg) + + +class ShowIPsecPolicy(command.ShowOne): + _description = _("Display IPsec policy details") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'ipsecpolicy', + metavar='', + help=_('IPsec policy to display (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + obj = client.find_vpn_ipsec_policy( + parsed_args.ipsecpolicy, ignore_missing=False + ) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, + _attr_map_dict, + [ + 'location', + 'tenant_id', + 'phase1_negotiation_mode', + 'units', + 'value', + ], + ) + data = utils.get_dict_properties(obj, columns) + return (display_columns, data) diff --git a/openstackclient/network/v2/vpnaas/utils.py b/openstackclient/network/v2/vpnaas/utils.py new file mode 100644 index 0000000000..5cc8a92f2f --- /dev/null +++ b/openstackclient/network/v2/vpnaas/utils.py @@ -0,0 +1,99 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""VPN Utilities and helper functions.""" + +from typing import Any + +from osc_lib import exceptions + +from openstackclient.i18n import _ + +DPD_SUPPORTED_ACTIONS = [ + 'hold', + 'clear', + 'restart', + 'restart-by-peer', + 'disabled', +] +DPD_SUPPORTED_KEYS = ['action', 'interval', 'timeout'] + +lifetime_keys = ['units', 'value'] +lifetime_units = ['seconds'] + + +def validate_dpd_dict(dpd_dict: dict[str, Any]) -> None: + for key, value in dpd_dict.items(): + if key not in DPD_SUPPORTED_KEYS: + message = _( + "DPD Dictionary KeyError: " + "Reason-Invalid DPD key : " + "'%(key)s' not in %(supported_key)s" + ) % {'key': key, 'supported_key': DPD_SUPPORTED_KEYS} + raise exceptions.CommandError(message) + if key == 'action' and value not in DPD_SUPPORTED_ACTIONS: + message = _( + "DPD Dictionary ValueError: " + "Reason-Invalid DPD action : " + "'%(key_value)s' not in %(supported_action)s" + ) % {'key_value': value, 'supported_action': DPD_SUPPORTED_ACTIONS} + raise exceptions.CommandError(message) + if key in ('interval', 'timeout'): + try: + if int(value) <= 0: + raise ValueError() + except ValueError: + message = _( + "DPD Dictionary ValueError: " + "Reason-Invalid positive integer value: " + "'%(key)s' = %(value)s" + ) % {'key': key, 'value': value} + raise exceptions.CommandError(message) + else: + dpd_dict[key] = int(value) + return + + +def validate_lifetime_dict(lifetime_dict: dict[str, Any]) -> None: + + for key, value in lifetime_dict.items(): + if key not in lifetime_keys: + message = _( + "Lifetime Dictionary KeyError: " + "Reason-Invalid unit key : " + "'%(key)s' not in %(supported_key)s" + ) % {'key': key, 'supported_key': lifetime_keys} + raise exceptions.CommandError(message) + if key == 'units' and value not in lifetime_units: + message = _( + "Lifetime Dictionary ValueError: " + "Reason-Invalid units : " + "'%(key_value)s' not in %(supported_units)s" + ) % {'key_value': key, 'supported_units': lifetime_units} + raise exceptions.CommandError(message) + if key == 'value': + try: + if int(value) < 60: + raise ValueError() + except ValueError: + message = _( + "Lifetime Dictionary ValueError: " + "Reason-Invalid value should be at least 60:" + "'%(key_value)s' = %(value)s" + ) % {'key_value': key, 'value': value} + raise exceptions.CommandError(message) + else: + lifetime_dict['value'] = int(value) + return diff --git a/openstackclient/network/v2/vpnaas/vpnservice.py b/openstackclient/network/v2/vpnaas/vpnservice.py new file mode 100644 index 0000000000..3af9b49224 --- /dev/null +++ b/openstackclient/network/v2/vpnaas/vpnservice.py @@ -0,0 +1,272 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack.network import v2 as network_v2 +from osc_lib.cli import identity as identity_utils +from osc_lib import exceptions +from osc_lib import utils +from osc_lib.utils import columns as column_util + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + + +_attr_map = [ + ('id', 'ID', column_util.LIST_BOTH), + ('name', 'Name', column_util.LIST_BOTH), + ('router_id', 'Router', column_util.LIST_BOTH), + ('subnet_id', 'Subnet', column_util.LIST_BOTH), + ('flavor_id', 'Flavor', column_util.LIST_BOTH), + ('is_admin_state_up', 'State', column_util.LIST_BOTH), + ('status', 'Status', column_util.LIST_BOTH), + ('description', 'Description', column_util.LIST_LONG_ONLY), + ('project_id', 'Project', column_util.LIST_LONG_ONLY), + ('external_v4_ip', 'Ext v4 IP', column_util.LIST_LONG_ONLY), + ('external_v6_ip', 'Ext v6 IP', column_util.LIST_LONG_ONLY), +] + +_attr_map_dict = { + 'id': 'ID', + 'name': 'Name', + 'router_id': 'Router', + 'subnet_id': 'Subnet', + 'flavor_id': 'Flavor', + 'is_admin_state_up': 'State', + 'status': 'Status', + 'description': 'Description', + 'project_id': 'Project', + 'external_v4_ip': 'Ext v4 IP', + 'external_v6_ip': 'Ext v6 IP', +} + + +def _get_common_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: + parser.add_argument( + '--description', + metavar='', + help=_('Description for the VPN service'), + ) + parser.add_argument( + '--subnet', + metavar='', + help=_('Local private subnet (name or ID)'), + ) + parser.add_argument( + '--flavor', + metavar='', + help=_('Flavor for the VPN service (name or ID)'), + ) + admin_group = parser.add_mutually_exclusive_group() + admin_group.add_argument( + '--enable', action='store_true', help=_("Enable VPN service") + ) + admin_group.add_argument( + '--disable', action='store_true', help=_("Disable VPN service") + ) + return parser + + +def _get_common_attrs( + client: network_v2.Proxy, parsed_args: argparse.Namespace +) -> dict[str, Any]: + attrs: dict[str, Any] = {} + if parsed_args.description: + attrs['description'] = str(parsed_args.description) + if parsed_args.subnet: + _subnet_id = client.find_subnet( + parsed_args.subnet, ignore_missing=False + ).id + attrs['subnet_id'] = _subnet_id + if parsed_args.flavor: + _flavor_id = client.find_flavor( + parsed_args.flavor, ignore_missing=False + ).id + attrs['flavor_id'] = _flavor_id + if parsed_args.enable: + attrs['admin_state_up'] = True + if parsed_args.disable: + attrs['admin_state_up'] = False + return attrs + + +class CreateVPNService(command.ShowOne): + _description = _("Create an VPN service") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + 'name', metavar='', help=_('Name for the VPN service') + ) + parser.add_argument( + '--router', + metavar='ROUTER', + required=True, + help=_('Router for the VPN service (name or ID)'), + ) + identity_utils.add_project_owner_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + attrs = _get_common_attrs(client, parsed_args) + if parsed_args.project is not None: + identity_client = self.app.client_manager.sdk_connection.identity + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + attrs['project_id'] = project_id + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + if parsed_args.router: + _router_id = client.find_router( + parsed_args.router, ignore_missing=False + ).id + attrs['router_id'] = _router_id + obj = client.create_vpn_service(**attrs) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns) + return display_columns, data + + +class DeleteVPNService(command.Command): + _description = _("Delete VPN service(s)") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'vpnservice', + metavar='', + nargs='+', + help=_('VPN service to delete (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + result = 0 + for vpn in parsed_args.vpnservice: + try: + vpn_id = client.find_vpn_service(vpn, ignore_missing=False).id + client.delete_vpn_service(vpn_id) + except Exception as e: + result += 1 + print( + f"Failed to delete VPN service with name or ID {vpn}: {e}" + ) + + if result > 0: + total = len(parsed_args.vpnservice) + msg = _( + "%(result)s of %(total)s vpn service failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListVPNService(command.Lister): + _description = _("List VPN services that belong to a given project") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_("List additional fields in output"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + client = self.app.client_manager.network + obj = client.vpn_services() + headers, columns = column_util.get_column_definitions( + _attr_map, long_listing=parsed_args.long + ) + return (headers, (utils.get_dict_properties(s, columns) for s in obj)) + + +class SetVPNSercice(command.Command): + _description = _("Set VPN service properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + _get_common_parser(parser) + parser.add_argument( + '--name', metavar='', help=_('Name for the VPN service') + ) + parser.add_argument( + 'vpnservice', + metavar='', + help=_('VPN service to modify (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + client = self.app.client_manager.network + attrs = _get_common_attrs(client, parsed_args) + if parsed_args.name: + attrs['name'] = str(parsed_args.name) + vpn_id = client.find_vpn_service( + parsed_args.vpnservice, ignore_missing=False + )['id'] + try: + client.update_vpn_service(vpn_id, **attrs) + except Exception as e: + msg = _("Failed to set vpn service '%(vpn)s': %(e)s") % { + 'vpn': parsed_args.vpnservice, + 'e': e, + } + raise exceptions.CommandError(msg) + + +class ShowVPNService(command.ShowOne): + _description = _("Display VPN service details") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'vpnservice', + metavar='', + help=_('VPN service to display (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + client = self.app.client_manager.network + obj = client.find_vpn_service( + parsed_args.vpnservice, ignore_missing=False + ) + display_columns, columns = utils.get_osc_show_columns_for_sdk_resource( + obj, _attr_map_dict, ['location', 'tenant_id'] + ) + data = utils.get_dict_properties(obj, columns) + return (display_columns, data) diff --git a/openstackclient/object/client.py b/openstackclient/object/client.py index 466f132ec2..f0742336cd 100644 --- a/openstackclient/object/client.py +++ b/openstackclient/object/client.py @@ -15,6 +15,9 @@ """Object client""" +import argparse +from typing import Any + from osc_lib import utils from openstackclient.api import object_store_v1 @@ -26,7 +29,7 @@ API_VERSIONS = ('1',) -def make_client(instance): +def make_client(instance: Any) -> object_store_v1.APIv1: """Returns an object-store API client.""" endpoint = instance.get_endpoint_for_service_type( @@ -43,7 +46,9 @@ def make_client(instance): return client -def build_option_parser(parser): +def build_option_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: """Hook to add global options""" parser.add_argument( '--os-object-api-version', diff --git a/openstackclient/object/v1/account.py b/openstackclient/object/v1/account.py index 686fdcc62c..26e2a6aedc 100644 --- a/openstackclient/object/v1/account.py +++ b/openstackclient/object/v1/account.py @@ -13,17 +13,21 @@ """Account v1 action implementations""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command +from openstackclient import command from openstackclient.i18n import _ class SetAccount(command.Command): _description = _("Set account properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--property", @@ -37,7 +41,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: self.app.client_manager.object_store.account_set( properties=parsed_args.property, ) @@ -46,19 +50,22 @@ def take_action(self, parsed_args): class ShowAccount(command.ShowOne): _description = _("Display account details") - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: data = self.app.client_manager.object_store.account_show() if 'properties' in data: data['properties'] = format_columns.DictColumn( data.pop('properties') ) - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class UnsetAccount(command.Command): _description = _("Unset account properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--property', @@ -73,7 +80,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: self.app.client_manager.object_store.account_unset( properties=parsed_args.property, ) diff --git a/openstackclient/object/v1/container.py b/openstackclient/object/v1/container.py index f4250dbba1..0333d3ee4e 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -15,13 +15,16 @@ """Container v1 action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ @@ -31,7 +34,7 @@ class CreateContainer(command.Lister): _description = _("Create new container") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--public', @@ -51,7 +54,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: results = [] for container in parsed_args.containers: if len(container) > 256: @@ -84,7 +89,7 @@ def take_action(self, parsed_args): class DeleteContainer(command.Command): _description = _("Delete container") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--recursive', @@ -101,7 +106,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: for container in parsed_args.containers: if parsed_args.recursive: objs = self.app.client_manager.object_store.object_list( @@ -120,14 +125,16 @@ def take_action(self, parsed_args): class ListContainer(command.Lister): _description = _("List containers") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--prefix", metavar="", help=_("Filter list using "), ) - pagination.add_marker_pagination_option_to_parser(parser) + pagination.add_marker_pagination_option_to_parser( + parser, include_max_items=False + ) parser.add_argument( "--end-marker", metavar="", @@ -147,7 +154,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: columns: tuple[str, ...] = ('Name',) if parsed_args.long: columns += ('Bytes', 'Count') @@ -182,7 +191,7 @@ def take_action(self, parsed_args): class SaveContainer(command.Command): _description = _("Save container contents locally") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'container', @@ -191,7 +200,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: self.app.client_manager.object_store.container_save( container=parsed_args.container, ) @@ -200,7 +209,7 @@ def take_action(self, parsed_args): class SetContainer(command.Command): _description = _("Set container properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'container', @@ -219,7 +228,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: self.app.client_manager.object_store.container_set( parsed_args.container, properties=parsed_args.property, @@ -229,7 +238,7 @@ def take_action(self, parsed_args): class ShowContainer(command.ShowOne): _description = _("Display container details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'container', @@ -238,20 +247,23 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: data = self.app.client_manager.object_store.container_show( container=parsed_args.container, ) if 'properties' in data: data['properties'] = format_columns.DictColumn(data['properties']) - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class UnsetContainer(command.Command): _description = _("Unset container properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'container', @@ -271,7 +283,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: self.app.client_manager.object_store.container_unset( parsed_args.container, properties=parsed_args.property, diff --git a/openstackclient/object/v1/object.py b/openstackclient/object/v1/object.py index cb8e4ffeb8..f3bc249a6b 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -15,14 +15,17 @@ """Object v1 action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ @@ -33,7 +36,7 @@ class CreateObject(command.Lister): _description = _("Upload object to container") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'container', @@ -56,7 +59,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: if parsed_args.name: if len(parsed_args.objects) > 1: msg = _( @@ -98,7 +103,7 @@ def take_action(self, parsed_args): class DeleteObject(command.Command): _description = _("Delete object from container") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'container', @@ -113,7 +118,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: for obj in parsed_args.objects: self.app.client_manager.object_store.object_delete( container=parsed_args.container, @@ -124,7 +129,7 @@ def take_action(self, parsed_args): class ListObject(command.Lister): _description = _("List objects") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "container", @@ -141,7 +146,9 @@ def get_parser(self, prog_name): metavar="", help=_("Roll up items with "), ) - pagination.add_marker_pagination_option_to_parser(parser) + pagination.add_marker_pagination_option_to_parser( + parser, include_max_items=False + ) parser.add_argument( "--end-marker", metavar="", @@ -161,7 +168,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: columns: tuple[str, ...] = ('Name',) if parsed_args.long: columns += ('Bytes', 'Hash', 'Content Type', 'Last Modified') @@ -200,7 +209,7 @@ def take_action(self, parsed_args): class SaveObject(command.Command): _description = _("Save object locally") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--file", @@ -222,7 +231,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: self.app.client_manager.object_store.object_save( container=parsed_args.container, object=parsed_args.object, @@ -233,7 +242,7 @@ def take_action(self, parsed_args): class SetObject(command.Command): _description = _("Set object properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'container', @@ -257,7 +266,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: self.app.client_manager.object_store.object_set( parsed_args.container, parsed_args.object, @@ -268,7 +277,7 @@ def take_action(self, parsed_args): class ShowObject(command.ShowOne): _description = _("Display object details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'container', @@ -282,7 +291,9 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: data = self.app.client_manager.object_store.object_show( container=parsed_args.container, object=parsed_args.object, @@ -290,13 +301,14 @@ def take_action(self, parsed_args): if 'properties' in data: data['properties'] = format_columns.DictColumn(data['properties']) - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class UnsetObject(command.Command): _description = _("Unset object properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'container', @@ -321,7 +333,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: self.app.client_manager.object_store.object_unset( parsed_args.container, parsed_args.object, diff --git a/openstackclient/py.typed b/openstackclient/py.typed new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/share/__init__.py b/openstackclient/share/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/share/client.py b/openstackclient/share/client.py new file mode 100644 index 0000000000..3475e6f4fa --- /dev/null +++ b/openstackclient/share/client.py @@ -0,0 +1,111 @@ +# Copyright 2012-2013 OpenStack Foundation +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import logging +from typing import TYPE_CHECKING, Any + +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient.i18n import _ + +if TYPE_CHECKING: + from manilaclient import client + +LOG = logging.getLogger(__name__) + +# global variables used when building the shell +DEFAULT_API_VERSION = '2' +API_VERSION_OPTION = 'os_share_api_version' +API_NAME = 'share' + +# Save the microversion if in use +_share_api_version = None + + +def make_client(instance: Any) -> 'client.Client': + """Returns a manilaclient.client.Client instance.""" + # Defer client imports until we actually need them + from manilaclient import client + + if _share_api_version is not None: + version = _share_api_version + else: + from manilaclient import api_versions + + check_version = instance._api_version[API_NAME] + version = api_versions.get_api_version(check_version) + + LOG.debug('Instantiating Shared File System client: %s', client.Client) + LOG.debug('Shared File System API version: %s', version) + + return client.Client( + version, + session=instance.session, + endpoint_type=instance.interface, + region_name=instance.region_name, + auth=instance.auth, + cacert=instance.cacert, + cert=instance.cert, + insecure=not instance.verify, + ) + + +def build_option_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: + """Hook to add global options""" + parser.add_argument( + '--os-share-api-version', + metavar='', + default=utils.env('OS_SHARE_API_VERSION'), + help=_( + "Shared File System API version, default=%s " + "(Env: OS_SHARE_API_VERSION)" + ) + % DEFAULT_API_VERSION, + ) + return parser + + +def check_api_version(check_version: str) -> bool: + # Defer client imports until we actually need them + from manilaclient import api_versions + + global _share_api_version + + # TODO(stephenfin): Other clients (novaclient, cinderclient, ...) do this + # normalization for us + if check_version.isdigit(): + check_version = f"{check_version}.0" + + _share_api_version = api_versions.get_api_version(check_version) + + # Bypass X.latest format microversion + if not _share_api_version.is_latest(): + if _share_api_version > api_versions.APIVersion("2.0"): + if not _share_api_version.matches( + api_versions.MIN_VERSION, + api_versions.MAX_VERSION, + ): + msg = _("versions supported by client: %(min)s - %(max)s") % { + "min": api_versions.MIN_VERSION, + "max": api_versions.MAX_VERSION, + } + raise exceptions.CommandError(msg) + + return True + + return False diff --git a/openstackclient/share/utils.py b/openstackclient/share/utils.py new file mode 100644 index 0000000000..47305d8592 --- /dev/null +++ b/openstackclient/share/utils.py @@ -0,0 +1,139 @@ +# Copyright 2019 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from collections.abc import Sequence +import logging +from typing import Any + +from manilaclient.common import constants +from osc_lib import exceptions + +from openstackclient.common import envvars +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + + +def extract_key_value_options( + pairs: dict[str, str] | None, +) -> dict[str, str]: + result_dict = {} + duplicate_options = [] + pairs = pairs or {} + + for attr, value in pairs.items(): + if attr not in result_dict: + result_dict[attr] = value + else: + duplicate_options.append(attr) + + if pairs and len(duplicate_options) > 0: + duplicate_str = ', '.join(duplicate_options) + msg = f"Following options were duplicated: {duplicate_str}" + raise exceptions.CommandError(msg) + + return result_dict + + +def format_properties(properties: dict[str, str]) -> str: + formatted_data = [] + + for item in properties: + formatted_data.append(f"{item} : {properties[item]}") + return "\n".join(formatted_data) + + +def extract_properties(properties: list[str]) -> dict[str, str]: + result_dict = {} + for item in properties: + try: + (key, value) = item.split('=', 1) + if key in result_dict: + raise exceptions.CommandError( + f"Argument '{key}' is specified twice." + ) + else: + result_dict[key] = value + except ValueError: + raise exceptions.CommandError( + "Parsing error, expected format 'key=value' for " + item + ) + return result_dict + + +def extract_extra_specs( + extra_specs: dict[str, Any], + specs_to_add: list[str], + bool_specs: Sequence[str] = constants.BOOL_SPECS, +) -> dict[str, Any]: + try: + for item in specs_to_add: + (key, value) = item.split('=', 1) + if key in extra_specs: + msg = f"Argument '{key}' value specified twice." + raise exceptions.CommandError(msg) + elif key in bool_specs: + if envvars.bool_from_str(value): + extra_specs[key] = value.capitalize() + else: + msg = ( + f"Argument '{key}' is of boolean " + f"type and has invalid value: {value!s}" + ) + raise exceptions.CommandError(msg) + else: + extra_specs[key] = value + except ValueError: + msg = _("Wrong format: specs should be key=value pairs.") + raise exceptions.CommandError(msg) + return extra_specs + + +def extract_group_specs( + extra_specs: dict[str, Any], specs_to_add: list[str] +) -> dict[str, Any]: + return extract_extra_specs( + extra_specs, specs_to_add, constants.GROUP_BOOL_SPECS + ) + + +def format_column_headers(columns: list[str]) -> list[str]: + column_headers = [] + for column in columns: + column_headers.append( + column.replace('_', ' ').title().replace('Id', 'ID') + ) + return column_headers + + +def format_share_group_type( + share_group_type: Any, formatter: str = 'table' +) -> dict[str, Any]: + printable_share_group_type = share_group_type._info + + is_public = printable_share_group_type.pop('is_public') + + printable_share_group_type['visibility'] = ( + 'public' if is_public else 'private' + ) + + if formatter == 'table': + printable_share_group_type['group_specs'] = format_properties( + share_group_type.group_specs + ) + printable_share_group_type['share_types'] = "\n".join( + printable_share_group_type['share_types'] + ) + + return printable_share_group_type diff --git a/openstackclient/share/v2/__init__.py b/openstackclient/share/v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/share/v2/messages.py b/openstackclient/share/v2/messages.py new file mode 100644 index 0000000000..75b11d827c --- /dev/null +++ b/openstackclient/share/v2/messages.py @@ -0,0 +1,226 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 collections.abc import Iterable, Sequence +import logging +from typing import Any + +from manilaclient import api_versions +from manilaclient.common.apiclient import utils as apiutils +from osc_lib import exceptions +from osc_lib import utils as oscutils + +from openstackclient import command +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + +MESSAGE_ATTRIBUTES = [ + 'id', + 'resource_type', + 'resource_id', + 'action_id', + 'user_message', + 'message_level', + 'detail_id', + 'created_at', + 'expires_at', + 'request_id', +] + + +class DeleteMessage(command.Command): + """Remove one or more messages.""" + + _description = _("Remove one or more messages") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'message', + metavar='', + nargs='+', + help=_('ID of the message(s).'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + failure_count = 0 + + for message in parsed_args.message: + try: + message_ref = apiutils.find_resource( + share_client.messages, message + ) + share_client.messages.delete(message_ref) + except Exception as e: + failure_count += 1 + LOG.error( + _("Delete for message %(message)s failed: %(e)s"), + {'message': message, 'e': e}, + ) + + if failure_count > 0: + raise exceptions.CommandError( + _("Unable to delete some or all of the specified messages.") + ) + + +class ListMessage(command.Lister): + """Lists all messages.""" + + _description = _("Lists all messages") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--resource-id', + metavar='', + default=None, + help=_('Filters results by a resource uuid. Default=None.'), + ) + parser.add_argument( + '--resource-type', + metavar='', + default=None, + help=_( + 'Filters results by a resource type. Default=None. ' + 'Example: "openstack message list --resource-type share"' + ), + ) + parser.add_argument( + '--action-id', + metavar='', + default=None, + help=_('Filters results by action id. Default=None.'), + ) + parser.add_argument( + '--detail-id', + metavar='', + default=None, + help=_('Filters results by detail id. Default=None.'), + ) + parser.add_argument( + '--request-id', + metavar='', + default=None, + help=_('Filters results by request id. Default=None.'), + ) + parser.add_argument( + '--message-level', + metavar='', + default=None, + help=_( + 'Filters results by the message level. Default=None. ' + 'Example: "openstack message list --message-level ERROR".' + ), + ) + parser.add_argument( + '--limit', + metavar='', + type=int, + default=None, + help=_('Maximum number of messages to return. (Default=None)'), + ) + parser.add_argument( + '--since', + metavar='', + default=None, + help=_( + 'Return only user messages created since given date. ' + 'The date format must be conforming to ISO8601. ' + 'Available only for microversion >= 2.52.' + ), + ) + parser.add_argument( + '--before', + metavar='', + default=None, + help=_( + 'Return only user messages created before given date. ' + 'The date format must be conforming to ISO8601. ' + 'Available only for microversion >= 2.52.' + ), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + share_client = self.app.client_manager.share + + search_opts = { + 'limit': parsed_args.limit, + 'request_id': parsed_args.request_id, + 'resource_type': parsed_args.resource_type, + 'resource_id': parsed_args.resource_id, + 'action_id': parsed_args.action_id, + 'detail_id': parsed_args.detail_id, + 'message_level': parsed_args.message_level, + } + + if share_client.api_version < api_versions.APIVersion("2.52"): + if getattr(parsed_args, 'since') or getattr(parsed_args, 'before'): + raise exceptions.CommandError( + _( + "Filtering messages by 'since' and 'before'" + " is possible only with Manila API version >=2.52" + ) + ) + else: + search_opts['created_since'] = parsed_args.since + search_opts['created_before'] = parsed_args.before + + messages = share_client.messages.list(search_opts=search_opts) + columns = [ + 'ID', + 'Resource Type', + 'Resource ID', + 'Action ID', + 'User Message', + 'Detail ID', + 'Created At', + ] + + return ( + columns, + (oscutils.get_item_properties(m, columns) for m in messages), + ) + + +class ShowMessage(command.ShowOne): + """Show details about a message.""" + + _description = _("Show details about a message") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'message', metavar='', help=_('ID of the message.') + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], tuple[Any, ...]]: + share_client = self.app.client_manager.share + + message = apiutils.find_resource( + share_client.messages, parsed_args.message + ) + + return ( + MESSAGE_ATTRIBUTES, + oscutils.get_dict_properties(message._info, MESSAGE_ATTRIBUTES), + ) diff --git a/openstackclient/share/v2/resource_locks.py b/openstackclient/share/v2/resource_locks.py new file mode 100644 index 0000000000..a6821c8f49 --- /dev/null +++ b/openstackclient/share/v2/resource_locks.py @@ -0,0 +1,485 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 collections.abc import Iterable, Sequence +import logging +from typing import Any +import uuid + +from manilaclient.common.apiclient import utils as apiutils +from manilaclient.common import constants +from openstackclient.identity import common as identity_common +from osc_lib import exceptions +from osc_lib import utils as osc_utils + +from openstackclient import command +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + +LOCK_DETAIL_ATTRIBUTES = [ + 'ID', + 'Resource Id', + 'Resource Type', + 'Resource Action', + 'Lock Context', + 'User Id', + 'Project Id', + 'Created At', + 'Updated At', + 'Lock Reason', +] + +LOCK_SUMMARY_ATTRIBUTES = [ + 'ID', + 'Resource Id', + 'Resource Type', + 'Resource Action', +] + +RESOURCE_TYPE_MANAGERS = { + 'share': 'shares', + 'access_rule': 'share_access_rules', +} + + +# TODO(stephenfin): Move this to osc_lib since it's useful elsewhere (e.g. +# glance) +def is_uuid_like(value: str) -> bool: + """Returns validation of a value as a UUID. + + :param val: Value to verify + :type val: string + :returns: bool + + .. versionchanged:: 1.1.1 + Support non-lowercase UUIDs. + """ + try: + formatted_value = ( + value.replace('urn:', '') + .replace('uuid:', '') + .strip('{}') + .replace('-', '') + .lower() + ) + return str(uuid.UUID(value)).replace('-', '') == formatted_value + except (TypeError, ValueError, AttributeError): + return False + + +class CreateResourceLock(command.ShowOne): + """Create a new resource lock.""" + + _description = _("Lock a resource action from occurring on a resource") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'resource', + metavar='', + help=_('Name or ID of resource to lock.'), + ) + parser.add_argument( + 'resource_type', + metavar='', + help=_('Type of the resource (e.g.: share, access).'), + ) + parser.add_argument( + '--resource-action', + '--resource_action', + metavar='', + default='delete', + help=_('Action to lock on the resource (default="delete")'), + ) + parser.add_argument( + '--lock-reason', + '--lock_reason', + '--reason', + metavar='', + help=_('Reason for the resource lock.'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], tuple[Any, ...]]: + share_client = self.app.client_manager.share + resource_type = parsed_args.resource_type + if resource_type not in RESOURCE_TYPE_MANAGERS: + raise exceptions.CommandError(_("Unsupported resource type")) + res_manager = RESOURCE_TYPE_MANAGERS[resource_type] + + resource = osc_utils.find_resource( + getattr(share_client, res_manager), parsed_args.resource + ) + resource_lock = share_client.resource_locks.create( + resource.id, + resource_type, + parsed_args.resource_action, + parsed_args.lock_reason, + ) + + resource_lock._info.pop('links', None) + + return self.dict2columns(resource_lock._info) + + +class DeleteResourceLock(command.Command): + """Remove one or more resource locks.""" + + _description = _("Remove one or more resource locks") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'lock', + metavar='', + nargs='+', + help=_('ID(s) of the lock(s).'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + failure_count = 0 + + for lock in parsed_args.lock: + try: + lock = apiutils.find_resource( + share_client.resource_locks, lock + ) + lock.delete() + except Exception as e: + failure_count += 1 + LOG.error( + _("Failed to delete %(lock)s: %(e)s"), + {'lock': lock, 'e': e}, + ) + + if failure_count > 0: + raise exceptions.CommandError( + _("Unable to delete some or all of the specified locks.") + ) + + +class ListResourceLock(command.Lister): + """Lists all resource locks.""" + + _description = _("Lists all resource locks") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--all-projects', + action='store_true', + help=_("Filter resource locks for all projects. (Admin only)."), + ) + parser.add_argument( + '--project', + default=None, + help=_( + "Filter resource locks for specific project by name or ID, " + "combine with --all-projects (Admin only)." + ), + ) + parser.add_argument( + '--user', + default=None, + help=_( + "Filter resource locks for specific user by name or ID, " + "combine with --all-projects to search across projects " + "(Admin only)." + ), + ) + parser.add_argument( + '--id', + metavar='', + default=None, + help=_('Filter resource locks by ID. Default=None.'), + ) + parser.add_argument( + '--resource', + '--resource-id', + '--resource_id', + default=None, + metavar='', + dest='resource', + help=_( + "Filter resource locks for a resource by ID, specify " + "--resource-type to look up by name." + ), + ) + parser.add_argument( + '--resource-type', + '--resource_type', + default=None, + metavar='', + help=_("Filter resource locks by type of resource."), + ) + parser.add_argument( + '--resource-action', + '--resource_action', + default=None, + metavar='', + help=_("Filter resource locks by resource action."), + ) + + parser.add_argument( + '--lock-context', + '--lock_context', + '--context', + default=None, + choices=['user', 'admin', 'service'], + metavar='', + help=_("Filter resource locks by context."), + ) + parser.add_argument( + '--since', + default=None, + metavar='', + help=_( + "Filter resource locks created since given date. " + "The date format must be conforming to ISO8601. " + ), + ) + parser.add_argument( + '--before', + default=None, + metavar='', + help=_( + "Filter resource locks created before given date. " + "The date format must be conforming to ISO8601. " + ), + ) + parser.add_argument( + '--limit', + metavar='', + type=int, + default=None, + help=_("Number of resource locks to list. (Default=None)"), + ) + parser.add_argument( + '--offset', + metavar="", + default=None, + help=_( + 'Starting position of resource lock records ' + 'in a paginated list.' + ), + ) + parser.add_argument( + '--sort-key', + '--sort_key', + metavar='', + type=str, + default=None, + choices=constants.RESOURCE_LOCK_SORT_KEY_VALUES, + help=( + f'Key to be sorted, available keys are ' + f'{constants.RESOURCE_LOCK_SORT_KEY_VALUES}. Default=None.' + ), + ) + parser.add_argument( + '--sort-dir', + '--sort_dir', + metavar='', + type=str, + default=None, + choices=constants.SORT_DIR_VALUES, + help=( + f'Sort direction, available values are ' + f'{constants.SORT_DIR_VALUES}. OPTIONAL: Default=None.' + ), + ) + parser.add_argument( + '--detailed', + dest='detailed', + metavar='<0|1>', + nargs='?', + type=int, + const=1, + default=0, + help=_("Show detailed information about filtered resource locks."), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + share_client = self.app.client_manager.share + identity_client = self.app.client_manager.identity + + columns = ( + LOCK_SUMMARY_ATTRIBUTES + if not parsed_args.detailed + else LOCK_DETAIL_ATTRIBUTES + ) + + project_id = None + user_id = None + + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + if parsed_args.user: + user_id = identity_common.find_user( + identity_client, parsed_args.user, parsed_args.user_domain + ).id + # set all_projects when using project option + all_projects = bool(parsed_args.project) or parsed_args.all_projects + + resource_id = parsed_args.resource + resource_type = parsed_args.resource_type + if resource_type is not None: + if resource_type not in RESOURCE_TYPE_MANAGERS: + raise exceptions.CommandError(_("Unsupported resource type")) + if resource_id is not None: + res_manager = RESOURCE_TYPE_MANAGERS[resource_type] + resource_id = osc_utils.find_resource( + getattr(share_client, res_manager), parsed_args.resource + ).id + elif resource_id and not is_uuid_like(resource_id): + raise exceptions.CommandError( + _("Provide resource ID or specify --resource-type.") + ) + + search_opts = { + 'all_projects': all_projects, + 'project_id': project_id, + 'user_id': user_id, + 'id': parsed_args.id, + 'resource_id': resource_id, + 'resource_type': parsed_args.resource_type, + 'resource_action': parsed_args.resource_action, + 'lock_context': parsed_args.lock_context, + 'created_before': parsed_args.before, + 'created_since': parsed_args.since, + 'limit': parsed_args.limit, + 'offset': parsed_args.offset, + } + + resource_locks = share_client.resource_locks.list( + search_opts=search_opts, + sort_key=parsed_args.sort_key, + sort_dir=parsed_args.sort_dir, + ) + + return ( + columns, + ( + osc_utils.get_item_properties(m, columns) + for m in resource_locks + ), + ) + + +class ShowResourceLock(command.ShowOne): + """Show details about a resource lock.""" + + _description = _("Show details about a resource lock") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'lock', metavar='', help=_('ID of resource lock to show.') + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], tuple[Any, ...]]: + share_client = self.app.client_manager.share + + resource_lock = apiutils.find_resource( + share_client.resource_locks, parsed_args.lock + ) + + return ( + LOCK_DETAIL_ATTRIBUTES, + osc_utils.get_dict_properties( + resource_lock._info, LOCK_DETAIL_ATTRIBUTES + ), + ) + + +class SetResourceLock(command.Command): + """Set resource lock properties.""" + + _description = _("Update resource lock properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'lock', metavar='', help=_('ID of lock to update.') + ) + parser.add_argument( + '--resource-action', + '--resource_action', + metavar='', + help=_('Resource action to set in the resource lock'), + ) + parser.add_argument( + '--lock-reason', + '--lock_reason', + '--reason', + dest='lock_reason', + help=_("Reason for the resource lock"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + + update_kwargs = {} + if parsed_args.resource_action is not None: + update_kwargs['resource_action'] = parsed_args.resource_action + if parsed_args.lock_reason is not None: + update_kwargs['lock_reason'] = parsed_args.lock_reason + if update_kwargs: + share_client.resource_locks.update( + parsed_args.lock, **update_kwargs + ) + + +class UnsetResourceLock(command.Command): + """Unsets a property on a resource lock.""" + + _description = _("Remove resource lock properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'lock', metavar='', help=_('ID of resource lock to update.') + ) + parser.add_argument( + '--lock-reason', + '--lock_reason', + '--reason', + dest='lock_reason', + action='store_true', + default=False, + help=_("Unset the lock reason. (Default=False)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + + if parsed_args.lock_reason: + share_client.resource_locks.update( + parsed_args.lock, lock_reason=None + ) diff --git a/openstackclient/share/v2/security_services.py b/openstackclient/share/v2/security_services.py new file mode 100644 index 0000000000..899478681c --- /dev/null +++ b/openstackclient/share/v2/security_services.py @@ -0,0 +1,659 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 collections.abc import Iterable, Sequence +import logging +from typing import Any + +from manilaclient import api_versions +from osc_lib.cli import parseractions +from osc_lib import exceptions +from osc_lib import utils as oscutils + +from openstackclient import command +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + + +class CreateShareSecurityService(command.ShowOne): + """Create security service used by project.""" + + _description = _("Create security service used by project.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'type', + metavar='', + default=None, + choices=['ldap', 'kerberos', 'active_directory'], + help=_( + "Security service type. Possible options are: " + "'ldap', 'kerberos', 'active_directory'." + ), + ) + parser.add_argument( + '--dns-ip', + metavar='', + default=None, + help=_( + "DNS IP address of the security service used " + "inside project's network." + ), + ) + parser.add_argument( + '--ou', + metavar='', + default=None, + help=_( + "Security service OU (Organizational Unit). " + "Available only for microversion >= 2.44." + ), + ) + parser.add_argument( + '--server', + metavar='', + default=None, + help=_("Security service IP address or hostname."), + ) + parser.add_argument( + '--domain', + metavar='', + default=None, + help=_("Security service domain."), + ) + parser.add_argument( + '--user', + metavar='', + default=None, + help=_("Security service user or group used by project."), + ) + parser.add_argument( + '--password', + metavar='', + default=None, + help=_("Password used by user."), + ) + parser.add_argument( + '--name', + metavar='', + default=None, + help=_("Security service name."), + ) + parser.add_argument( + '--description', + metavar='', + default=None, + help=_("Security service description."), + ) + parser.add_argument( + '--default-ad-site', + metavar='', + dest='default_ad_site', + default=None, + help=_( + "Default AD site. Available only for " + "microversion >= 2.76. Can be provided in the " + "place of '--server' but not along with it." + ), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], tuple[Any, ...]]: + share_client = self.app.client_manager.share + + kwargs = { + 'dns_ip': parsed_args.dns_ip, + 'server': parsed_args.server, + 'domain': parsed_args.domain, + 'user': parsed_args.user, + 'password': parsed_args.password, + 'name': parsed_args.name, + 'description': parsed_args.description, + } + + if share_client.api_version >= api_versions.APIVersion("2.44"): + kwargs['ou'] = parsed_args.ou + elif parsed_args.ou: + raise exceptions.CommandError( + "Defining a security service Organizational Unit is " + "available only for microversion >= 2.44" + ) + + if share_client.api_version >= api_versions.APIVersion("2.76"): + kwargs['default_ad_site'] = parsed_args.default_ad_site + elif parsed_args.default_ad_site: + raise exceptions.CommandError( + "Defining a security service Default AD site is " + "available only for microversion >= 2.76" + ) + + if parsed_args.type == 'active_directory': + server = parsed_args.server + default_ad_site = parsed_args.default_ad_site + if server and default_ad_site: + raise exceptions.CommandError( + "Cannot create security service because both " + "server and 'default_ad_site' were provided. " + "Specify either server or 'default_ad_site'." + ) + + security_service = share_client.security_services.create( + parsed_args.type, **kwargs + ) + + return self.dict2columns(security_service._info) + + +class DeleteShareSecurityService(command.Command): + """Delete one or more security services.""" + + _description = _("Delete one or more security services.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'security_service', + metavar='', + nargs="+", + help=_("Name or ID of the security service(s) to delete."), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + result = 0 + + for security_service in parsed_args.security_service: + try: + security_service_obj = oscutils.find_resource( + share_client.security_services, security_service + ) + share_client.security_services.delete(security_service_obj) + + except Exception as e: + result += 1 + LOG.error( + "Failed to delete security service with name or ID %s: %s", + security_service, + e, + ) + + if result > 0: + total = len(parsed_args.security_service) + msg = ( + f"{result} of {total} security services failed to be deleted." + ) + raise exceptions.CommandError(msg) + + +class ShowShareSecurityService(command.ShowOne): + """Show security service.""" + + _description = _("Show security service.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'security_service', + metavar='', + help=_("Security service name or ID to show."), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], tuple[Any, ...]]: + share_client = self.app.client_manager.share + + security_service = oscutils.find_resource( + share_client.security_services, parsed_args.security_service + ) + + data = security_service._info + if parsed_args.formatter == 'table': + if 'share_networks' in data.keys(): + data['share_networks'] = "\n".join(data['share_networks']) + + return self.dict2columns(data) + + +class SetShareSecurityService(command.Command): + """Set security service.""" + + _description = _("Set security service.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'security_service', + metavar='', + help=_("Security service name or ID."), + ) + parser.add_argument( + '--dns-ip', + metavar='', + default=None, + help=_("Set DNS IP address used inside project's network."), + ) + parser.add_argument( + '--ou', + metavar='', + default=None, + help=_( + "Set security service OU (Organizational Unit). " + "Available only for microversion >= 2.44." + ), + ) + parser.add_argument( + '--server', + metavar='', + default=None, + help=_("Set security service IP address or hostname."), + ) + parser.add_argument( + '--domain', + metavar='', + default=None, + help=_("Set security service domain."), + ) + parser.add_argument( + '--user', + metavar='', + default=None, + help=_("Set security service user or group used by project."), + ) + parser.add_argument( + '--password', + metavar='', + default=None, + help=_("Set password used by user."), + ) + parser.add_argument( + '--name', + metavar='', + default=None, + help=_("Set security service name."), + ) + parser.add_argument( + '--description', + metavar='', + default=None, + help=_("Set security service description."), + ) + parser.add_argument( + '--default-ad-site', + metavar='', + dest='default_ad_site', + default=None, + help=_( + "Default AD site. Available only for microversion >= 2.76." + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + + security_service = oscutils.find_resource( + share_client.security_services, parsed_args.security_service + ) + + kwargs = { + 'dns_ip': parsed_args.dns_ip, + 'server': parsed_args.server, + 'domain': parsed_args.domain, + 'user': parsed_args.user, + 'password': parsed_args.password, + 'name': parsed_args.name, + 'description': parsed_args.description, + } + + if share_client.api_version >= api_versions.APIVersion("2.44"): + kwargs['ou'] = parsed_args.ou + elif parsed_args.ou: + raise exceptions.CommandError( + _( + "Setting a security service Organizational Unit is " + "available only for microversion >= 2.44" + ) + ) + + if share_client.api_version >= api_versions.APIVersion("2.76"): + kwargs['default_ad_site'] = parsed_args.default_ad_site + elif parsed_args.default_ad_site: + raise exceptions.CommandError( + "Defining a security service Default AD site is " + "available only for microversion >= 2.76" + ) + + if security_service.type == 'active_directory': + server = parsed_args.server + default_ad_site = parsed_args.default_ad_site + if server and default_ad_site: + raise exceptions.CommandError( + "Cannot set security service because both " + "server and 'default_ad_site' were provided. " + "Specify either server or 'default_ad_site'." + ) + try: + security_service.update(**kwargs) + except Exception as e: + raise exceptions.CommandError( + f"One or more set operations failed: {e}" + ) + + +class UnsetShareSecurityService(command.Command): + """Unset security service.""" + + _description = _("Unset security service.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'security_service', + metavar='', + help=_("Security service name or ID."), + ) + parser.add_argument( + '--dns-ip', + action='store_true', + help=_("Unset DNS IP address used inside project's network."), + ) + parser.add_argument( + '--ou', + action='store_true', + help=_( + "Unset security service OU (Organizational Unit). " + "Available only for microversion >= 2.44." + ), + ) + parser.add_argument( + '--server', + action='store_true', + help=_("Unset security service IP address or hostname."), + ) + parser.add_argument( + '--domain', + action='store_true', + help=_("Unset security service domain."), + ) + parser.add_argument( + '--user', + action='store_true', + help=_("Unset security service user or group used by project."), + ) + parser.add_argument( + '--password', + action='store_true', + help=_("Unset password used by user."), + ) + parser.add_argument( + '--name', + action='store_true', + help=_("Unset security service name."), + ) + parser.add_argument( + '--description', + action='store_true', + help=_("Unset security service description."), + ) + parser.add_argument( + '--default-ad-site', + dest='default_ad_site', + action='store_true', + help=_( + "Default AD site. Available only for microversion >= 2.76." + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + + security_service = oscutils.find_resource( + share_client.security_services, parsed_args.security_service + ) + + kwargs: dict[str, str] = {} + args = [ + 'dns_ip', + 'server', + 'domain', + 'user', + 'password', + 'name', + 'description', + ] + for arg in args: + if getattr(parsed_args, arg): + # the SDK unsets a value if it is an empty string + kwargs[arg] = '' + + if ( + parsed_args.ou + and share_client.api_version >= api_versions.APIVersion("2.44") + ): + # the SDK unsets a value if it is an empty string + kwargs['ou'] = '' + + elif parsed_args.ou: + raise exceptions.CommandError( + _( + "Unsetting a security service Organizational Unit is " + "available only for microversion >= 2.44" + ) + ) + + if ( + parsed_args.default_ad_site + and share_client.api_version >= api_versions.APIVersion("2.76") + ): + # the SDK unsets a value if it is an empty string + kwargs['default_ad_site'] = '' + elif parsed_args.default_ad_site: + raise exceptions.CommandError( + _( + "Unsetting a security service Default AD site is " + "available only for microversion >= 2.76" + ) + ) + + try: + security_service.update(**kwargs) + except Exception as e: + raise exceptions.CommandError( + f"One or more unset operations failed: {e}" + ) + + +class ListShareSecurityService(command.Lister): + """List security services.""" + + _description = _("List security services.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--all-projects', + action='store_true', + help=_("Display information from all projects (Admin only)."), + ) + parser.add_argument( + '--share-network', + metavar='', + default=None, + help=_("Filter results by share network name or ID."), + ) + parser.add_argument( + '--status', + metavar='', + default=None, + help=_("Filter results by status."), + ) + parser.add_argument( + '--name', + metavar='', + default=None, + help=_("Filter results by security service name."), + ) + parser.add_argument( + '--type', + metavar='', + default=None, + help=_("Filter results by security service type."), + ) + parser.add_argument( + '--user', + metavar='', + default=None, + help=_( + "Filter results by security service user or group " + "used by project." + ), + ) + parser.add_argument( + '--dns-ip', + metavar='', + default=None, + help=_( + "Filter results by DNS IP address used inside " + "project's network." + ), + ) + parser.add_argument( + '--ou', + metavar='', + default=None, + help=_( + "Filter results by security service OU " + "(Organizational Unit). " + "Available only for microversion >= 2.44." + ), + ) + parser.add_argument( + '--default-ad-site', + metavar='', + dest='default_ad_site', + default=None, + help=_( + "Filter results by security service default_ad_site. " + "Available only for microversion >= 2.76." + ), + ) + parser.add_argument( + '--server', + metavar='', + default=None, + help=_( + "Filter results by security service IP address or hostname." + ), + ) + parser.add_argument( + '--domain', + metavar='', + default=None, + help=_("Filter results by security service domain."), + ) + parser.add_argument( + '--detail', + action='store_true', + help=_( + "Show detailed information about filtered security services." + ), + ) + parser.add_argument( + "--limit", + metavar="", + type=int, + default=None, + action=parseractions.NonNegativeAction, + help=_("Limit the number of security services returned"), + ) + parser.add_argument( + "--marker", + metavar="", + help=_("The last security service ID of the previous page"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + share_client = self.app.client_manager.share + + columns = ['ID', 'Name', 'Status', 'Type'] + + if parsed_args.all_projects: + columns.append('Project ID') + + if parsed_args.detail: + columns.append('Share Networks') + + search_opts = { + 'all_tenants': parsed_args.all_projects, + 'status': parsed_args.status, + 'name': parsed_args.name, + 'type': parsed_args.type, + 'user': parsed_args.user, + 'dns_ip': parsed_args.dns_ip, + 'server': parsed_args.server, + 'domain': parsed_args.domain, + 'offset': parsed_args.marker, + 'limit': parsed_args.limit, + } + + if ( + parsed_args.ou + and share_client.api_version >= api_versions.APIVersion("2.44") + ): + search_opts['ou'] = parsed_args.ou + + elif parsed_args.ou: + raise exceptions.CommandError( + _( + "Filtering results by security service Organizational " + "Unit is available only for microversion >= 2.44" + ) + ) + + if ( + parsed_args.default_ad_site + and share_client.api_version >= api_versions.APIVersion("2.76") + ): + search_opts['default_ad_site'] = parsed_args.default_ad_site + elif parsed_args.default_ad_site: + raise exceptions.CommandError( + _( + "Filtering results by security service Default AD site is " + "available only for microversion >= 2.76" + ) + ) + + if parsed_args.share_network: + search_opts['share_network_id'] = oscutils.find_resource( + share_client.share_networks, parsed_args.share_network + ).id + + data = share_client.security_services.list( + search_opts=search_opts, detailed=parsed_args.detail + ) + + return ( + columns, + (oscutils.get_item_properties(s, columns) for s in data), + ) diff --git a/openstackclient/share/v2/services.py b/openstackclient/share/v2/services.py new file mode 100644 index 0000000000..e217e31ba0 --- /dev/null +++ b/openstackclient/share/v2/services.py @@ -0,0 +1,214 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 collections.abc import Iterable, Sequence +from typing import Any + +from manilaclient import api_versions +from osc_lib import exceptions +from osc_lib import utils as osc_utils + +from openstackclient import command +from openstackclient.i18n import _ + + +class SetShareService(command.Command): + """Enable/disable share service (Admin only).""" + + _description = _("Enable/Disable share service (Admin only).") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'host', + metavar='', + help=_("Host name as 'example_host@example_backend'."), + ) + parser.add_argument( + 'binary', + metavar='', + help=_( + "Service binary, could be 'manila-share', " + "'manila-scheduler' or 'manila-data'" + ), + ) + enable_group = parser.add_mutually_exclusive_group() + enable_group.add_argument( + '--enable', + action='store_true', + help=_('Enable share service'), + ) + enable_group.add_argument( + '--disable', + action='store_true', + help=_('Disable share service'), + ) + parser.add_argument( + "--disable-reason", + metavar="", + help=_( + "Reason for disabling the service " + "(should be used with --disable option)" + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + if parsed_args.disable_reason and not parsed_args.disable: + msg = _( + "Cannot specify option --disable-reason without " + "--disable specified." + ) + raise exceptions.CommandError(msg) + + share_client = self.app.client_manager.share + + if parsed_args.enable: + try: + share_client.services.enable( + parsed_args.host, parsed_args.binary + ) + except Exception as e: + msg = _("Failed to enable service: %(e)s") + raise exceptions.CommandError(msg % {'e': e}) + + if parsed_args.disable: + if parsed_args.disable_reason: + if share_client.api_version < api_versions.APIVersion("2.83"): + raise exceptions.CommandError( + "Service disable reason can be specified only with " + "manila API version >= 2.83" + ) + try: + if parsed_args.disable_reason: + share_client.services.disable( + parsed_args.host, + parsed_args.binary, + disable_reason=parsed_args.disable_reason, + ) + else: + share_client.services.disable( + parsed_args.host, parsed_args.binary + ) + except Exception as e: + msg = _("Failed to disable service: %(e)s") + raise exceptions.CommandError(msg % {'e': e}) + + +class ListShareService(command.Lister): + """List share services (Admin only).""" + + _description = _("List share services (Admin only).") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "--host", + metavar="", + default=None, + help=_("Filter services by name of the host."), + ) + parser.add_argument( + "--binary", + metavar="", + default=None, + help=_("Filter services by the name of the service."), + ) + parser.add_argument( + "--status", + metavar="", + default=None, + help=_("Filter results by status."), + ) + parser.add_argument( + "--state", + metavar="", + default=None, + choices=['up', 'down'], + help=_("Filter results by state."), + ) + parser.add_argument( + "--zone", + metavar="", + default=None, + help=_("Filter services by their availability zone."), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + share_client = self.app.client_manager.share + + search_opts = { + 'host': parsed_args.host, + 'binary': parsed_args.binary, + 'status': parsed_args.status, + 'state': parsed_args.state, + 'zone': parsed_args.zone, + } + + services = share_client.services.list(search_opts=search_opts) + + columns = [ + 'ID', + 'Binary', + 'Host', + 'Zone', + 'Status', + 'State', + 'Updated At', + ] + if share_client.api_version >= api_versions.APIVersion("2.83"): + columns.append('Disabled Reason') + if share_client.api_version >= api_versions.APIVersion("2.86"): + columns.append('Ensuring') + + data = ( + osc_utils.get_dict_properties(service._info, columns) + for service in services + ) + + return (columns, data) + + +class EnsureShareService(command.Command): + """Run ensure shares in a back end (Admin only).""" + + _description = _("Run ensure shares in a back end (Admin only).") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'host', + metavar='', + help=_( + "Host to run ensure shares. 'example_host@example_backend'." + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + + if share_client.api_version < api_versions.APIVersion("2.86"): + raise exceptions.CommandError( + "Ensure shares API is only available in " + "manila API version >= 2.86" + ) + + try: + share_client.services.ensure_shares(parsed_args.host) + except Exception as e: + msg = _("Failed to ensure shares: %(e)s") + raise exceptions.CommandError(msg % {'e': e}) diff --git a/openstackclient/share/v2/share_group_snapshots.py b/openstackclient/share/v2/share_group_snapshots.py new file mode 100644 index 0000000000..d561964393 --- /dev/null +++ b/openstackclient/share/v2/share_group_snapshots.py @@ -0,0 +1,484 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 collections.abc import Iterable, Sequence +import logging +from typing import Any + +from osc_lib.cli import parseractions +from osc_lib import exceptions +from osc_lib import utils as osc_utils + +from openstackclient import command +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + + +class CreateShareGroupSnapshot(command.ShowOne): + """Create a share group snapshot.""" + + _description = _("Create a share group snapshot of the given share group") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "share_group", + metavar="", + help=_("Name or ID of the share group."), + ) + parser.add_argument( + "--name", + metavar="", + default=None, + help=_("Optional share group snapshot name. (Default=None)"), + ) + parser.add_argument( + "--description", + metavar="", + default=None, + help=_( + "Optional share group snapshot description. (Default=None)" + ), + ) + parser.add_argument( + '--wait', + action='store_true', + default=False, + help=_('Wait for share group snapshot creation'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], tuple[Any, ...]]: + share_client = self.app.client_manager.share + + share_group = osc_utils.find_resource( + share_client.share_groups, parsed_args.share_group + ) + + share_group_snapshot = share_client.share_group_snapshots.create( + share_group, + name=parsed_args.name, + description=parsed_args.description, + ) + if parsed_args.wait: + if not osc_utils.wait_for_status( + status_f=share_client.share_group_snapshots.get, + res_id=share_group_snapshot.id, + success_status=['available'], + ): + LOG.error(_("ERROR: Share group snapshot is in error state.")) + + share_group_snapshot = osc_utils.find_resource( + share_client.share_group_snapshots, share_group_snapshot.id + ) + + data = share_group_snapshot._info + data.pop('links', None) + data.pop('members', None) + + return self.dict2columns(data) + + +class DeleteShareGroupSnapshot(command.Command): + """Delete one or more share group snapshots.""" + + _description = _("Delete one or more share group snapshot") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "share_group_snapshot", + metavar="", + nargs="+", + help=_("Name or ID of the group snapshot(s) to delete"), + ) + parser.add_argument( + "--force", + action='store_true', + default=False, + help=_( + "Attempt to force delete the share group snapshot(s) " + "(Default=False) (Admin only)." + ), + ) + parser.add_argument( + "--wait", + action='store_true', + default=False, + help=_("Wait for share group snapshot deletion"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + result = 0 + + for share_group_snapshot in parsed_args.share_group_snapshot: + try: + share_group_snapshot_obj = osc_utils.find_resource( + share_client.share_group_snapshots, share_group_snapshot + ) + + share_client.share_group_snapshots.delete( + share_group_snapshot_obj, force=parsed_args.force + ) + + if parsed_args.wait: + if not osc_utils.wait_for_delete( + manager=share_client.share_group_snapshots, + res_id=share_group_snapshot_obj.id, + ): + result += 1 + + except Exception as e: + result += 1 + LOG.error( + 'Failed to delete a share group snapshot with ' + 'name or ID %s: %s', + share_group_snapshot, + e, + ) + + if result > 0: + total = len(parsed_args.share_group_snapshot) + msg = ( + f'{result} of {total} share group snapshots failed to delete.' + ) + raise exceptions.CommandError(msg) + + +class ShowShareGroupSnapshot(command.ShowOne): + """Display a share group snapshot""" + + _description = _("Show details about a share group snapshot") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "share_group_snapshot", + metavar="", + help=_("Name or ID of the share group snapshot to display"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], tuple[Any, ...]]: + share_client = self.app.client_manager.share + + share_group_snapshot = osc_utils.find_resource( + share_client.share_group_snapshots, + parsed_args.share_group_snapshot, + ) + + data = share_group_snapshot._info + data.pop('links', None) + data.pop('members', None) + + return self.dict2columns(data) + + +class SetShareGroupSnapshot(command.Command): + """Set share group snapshot properties.""" + + _description = _("Set share group snapshot properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "share_group_snapshot", + metavar="", + help=_('Name or ID of the snapshot to set a property for'), + ) + parser.add_argument( + "--name", + metavar="", + default=None, + help=_("Set a name to the snapshot."), + ) + parser.add_argument( + "--description", + metavar="", + default=None, + help=_("Set a description to the snapshot."), + ) + parser.add_argument( + "--status", + metavar="", + choices=[ + 'available', + 'error', + 'creating', + 'deleting', + 'error_deleting', + ], + help=_( + "Explicitly set the state of a share group snapshot" + "(Admin only). " + "Options include : available, error, creating, " + "deleting, error_deleting." + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + result = 0 + + share_group_snapshot = osc_utils.find_resource( + share_client.share_group_snapshots, + parsed_args.share_group_snapshot, + ) + + kwargs = {} + + if parsed_args.name is not None: + kwargs['name'] = parsed_args.name + if parsed_args.description is not None: + kwargs['description'] = parsed_args.description + + if kwargs: + try: + share_client.share_group_snapshots.update( + share_group_snapshot, **kwargs + ) + except Exception as e: + result += 1 + LOG.error( + 'Failed to set name or description for ' + 'share group snapshot with ID %s: %s', + share_group_snapshot.id, + e, + ) + + if parsed_args.status: + try: + share_client.share_group_snapshots.reset_state( + share_group_snapshot, parsed_args.status + ) + except Exception as e: + result += 1 + LOG.error( + 'Failed to set status for share group snapshot with ' + 'ID %s: %s', + share_group_snapshot.id, + e, + ) + + if result > 0: + raise exceptions.CommandError( + _("One or more of the set operations failed") + ) + + +class UnsetShareGroupSnapshot(command.Command): + """Unset a share group snapshot property.""" + + _description = _("Unset a share group snapshot property") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "share_group_snapshot", + metavar="", + help=_("Name or ID of the group snapshot to unset a property of"), + ) + parser.add_argument( + "--name", + action='store_true', + help=_("Unset share group snapshot name."), + ) + parser.add_argument( + "--description", + action='store_true', + help=_("Unset share group snapshot description."), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + + share_group_snapshot = osc_utils.find_resource( + share_client.share_group_snapshots, + parsed_args.share_group_snapshot, + ) + + kwargs = {} + if parsed_args.name: + # the SDK unsets name if it is an empty string + kwargs['name'] = '' + if parsed_args.description: + # the SDK unsets description if it is an empty string + kwargs['description'] = '' + if kwargs: + try: + share_client.share_group_snapshots.update( + share_group_snapshot, **kwargs + ) + except Exception as e: + raise exceptions.CommandError( + 'Failed to unset name or description for ' + f'share group snapshot : {e}' + ) + + +class ListShareGroupSnapshot(command.Lister): + """List share group snapshots.""" + + _description = _("List share group snapshots") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "--all-projects", + action='store_true', + default=False, + help=_("Display information from all projects (Admin only)."), + ) + parser.add_argument( + "--name", + metavar="", + default=None, + help=_("Filter results by name."), + ) + parser.add_argument( + "--status", + metavar="", + default=None, + help=_("Filter results by status."), + ) + parser.add_argument( + "--share-group", + metavar="", + default=None, + help=_("Filter results by share group name or ID."), + ) + parser.add_argument( + "--limit", + metavar="", + type=int, + default=None, + action=parseractions.NonNegativeAction, + help=_("Limit the number of share groups returned"), + ) + parser.add_argument( + "--marker", + metavar="", + help=_("The last share group snapshot ID of the previous page"), + ) + parser.add_argument( + '--sort', + metavar="[:]", + default='name:asc', + help=_( + "Sort output by selected keys and directions(asc or desc) " + "(default: name:asc), multiple keys and directions can be " + "specified separated by comma" + ), + ) + parser.add_argument( + "--detailed", + action="store_true", + help=_("Show detailed information about share group snapshot. "), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + share_client = self.app.client_manager.share + + share_group_id = None + if parsed_args.share_group: + share_group_id = osc_utils.find_resource( + share_client.share_groups, parsed_args.share_group + ).id + + columns = [ + 'ID', + 'Name', + 'Status', + 'Description', + ] + + search_opts = { + 'all_tenants': parsed_args.all_projects, + 'name': parsed_args.name, + 'status': parsed_args.status, + 'share_group_id': share_group_id, + 'limit': parsed_args.limit, + 'offset': parsed_args.marker, + } + + if parsed_args.detailed: + columns.extend( + [ + 'Created At', + 'Share Group ID', + ] + ) + + if parsed_args.all_projects: + columns.append('Project ID') + + share_group_snapshots = share_client.share_group_snapshots.list( + search_opts=search_opts + ) + + share_group_snapshots = osc_utils.sort_items( + share_group_snapshots, parsed_args.sort, str + ) + + data = ( + osc_utils.get_dict_properties(share_group_snapshot._info, columns) + for share_group_snapshot in share_group_snapshots + ) + + return (columns, data) + + +class ListShareGroupSnapshotMembers(command.Lister): + """List members for share group snapshot.""" + + _description = _("List members of share group snapshot") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "share_group_snapshot", + metavar="", + help=_("Name or ID of the group snapshot to list members for"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + share_client = self.app.client_manager.share + + columns = ['Share ID', 'Size'] + + share_group_snapshot = osc_utils.find_resource( + share_client.share_group_snapshots, + parsed_args.share_group_snapshot, + ) + + data = ( + osc_utils.get_dict_properties(member, columns) + for member in share_group_snapshot._info.get('members', []) + ) + + return (columns, data) diff --git a/openstackclient/share/v2/share_group_type_access.py b/openstackclient/share/v2/share_group_type_access.py new file mode 100644 index 0000000000..96c0c0b522 --- /dev/null +++ b/openstackclient/share/v2/share_group_type_access.py @@ -0,0 +1,192 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import argparse +import logging +from collections.abc import Iterable, Sequence +from typing import Any + +from manilaclient.common.apiclient import utils as apiutils +from osc_lib import exceptions +from osc_lib import utils as oscutils + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + +LOG = logging.getLogger(__name__) + + +class ShareGroupTypeAccessAllow(command.Command): + """Allow a project to access a share group type.""" + + _description = _( + "Allow a project to access a share group type (Admin only)." + ) + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'share_group_type', + metavar="", + help=_("Share group type name or ID to allow access to."), + ) + parser.add_argument( + 'projects', + metavar="", + nargs="+", + help=_("Project Name or ID to add share group type access for."), + ) + identity_common.add_project_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + identity_client = self.app.client_manager.identity + result = 0 + + share_group_type = apiutils.find_resource( + share_client.share_group_types, parsed_args.share_group_type + ) + + for project in parsed_args.projects: + try: + project_obj = identity_common.find_project( + identity_client, project, parsed_args.project_domain + ) + + share_client.share_group_type_access.add_project_access( + share_group_type, project_obj.id + ) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to allow access for project '%(project)s' " + "to share group type with name or ID " + "'%(share_group_type)s': %(e)s" + ), + { + 'project': project, + 'share_group_type': share_group_type, + 'e': e, + }, + ) + + if result > 0: + total = len(parsed_args.projects) + msg = _( + "Failed to allow access to %(result)s of %(total)s projects" + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListShareGroupTypeAccess(command.Lister): + """Get access list for share group type.""" + + _description = _("Get access list for share group type (Admin only).") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'share_group_type', + metavar="", + help=_("Filter results by share group type name or ID."), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + share_client = self.app.client_manager.share + + share_group_type = apiutils.find_resource( + share_client.share_group_types, parsed_args.share_group_type + ) + + if share_group_type._info.get('is_public'): + raise exceptions.CommandError( + 'Forbidden to get access list for public share group type.' + ) + + data = share_client.share_group_type_access.list(share_group_type) + + columns = ['Project ID'] + values = (oscutils.get_item_properties(s, columns) for s in data) + + return (columns, values) + + +class ShareGroupTypeAccessDeny(command.Command): + """Deny a project to access a share group type.""" + + _description = _( + "Deny a project to access a share group type (Admin only)." + ) + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'share_group_type', + metavar="", + help=_("Share group type name or ID to deny access from"), + ) + parser.add_argument( + 'projects', + metavar="", + nargs="+", + help=_( + "Project Name(s) or ID(s) to deny share group type access for." + ), + ) + identity_common.add_project_domain_option_to_parser(parser) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + identity_client = self.app.client_manager.identity + result = 0 + + share_group_type = apiutils.find_resource( + share_client.share_group_types, parsed_args.share_group_type + ) + + for project in parsed_args.projects: + try: + project_obj = identity_common.find_project( + identity_client, project, parsed_args.project_domain + ) + + share_client.share_group_type_access.remove_project_access( + share_group_type, project_obj.id + ) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to deny access for project '%(project)s' " + "to share group type with name or ID " + "'%(share_group_type)s': %(e)s" + ), + { + 'project': project, + 'share_group_type': share_group_type, + 'e': e, + }, + ) + + if result > 0: + total = len(parsed_args.projects) + msg = _( + "Failed to deny access to %(result)s of %(total)s projects" + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) diff --git a/openstackclient/share/v2/share_group_types.py b/openstackclient/share/v2/share_group_types.py new file mode 100644 index 0000000000..5cfa9bc824 --- /dev/null +++ b/openstackclient/share/v2/share_group_types.py @@ -0,0 +1,385 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 logging +from collections.abc import Iterable, Sequence +from typing import Any + +from manilaclient.common.apiclient import utils as apiutils +from osc_lib import exceptions +from osc_lib import utils as oscutils + +from openstackclient import command +from openstackclient.common import envvars +from openstackclient.i18n import _ +from openstackclient.share import utils + +LOG = logging.getLogger(__name__) + +ATTRIBUTES = [ + 'id', + 'name', + 'share_types', + 'visibility', + 'is_default', + 'group_specs', +] + + +class CreateShareGroupType(command.ShowOne): + """Create new share group type.""" + + _description = _("Create new share group type") + + log = logging.getLogger(__name__ + ".CreateShareGroupType") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'name', + metavar="", + default=None, + help=_('Share group type name'), + ) + parser.add_argument( + "share_types", + metavar="", + nargs="+", + default=None, + help=_( + "List of share type names or IDs. Example:" + " my-share-type-1 my-share-type-2" + ), + ) + parser.add_argument( + "--group-specs", + type=str, + nargs='*', + metavar='', + default=None, + help=_( + "Share Group type extra specs by key and value." + " OPTIONAL: Default=None. Example:" + " --group-specs consistent_snapshot_support=host." + ), + ) + parser.add_argument( + '--public', + metavar="", + default=True, + help=_('Make type accessible to the public (default true).'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], tuple[Any, ...]]: + share_client = self.app.client_manager.share + + kwargs = {'name': parsed_args.name} + + share_types_list = [] + for share_type in parsed_args.share_types: + try: + share_type_obj = apiutils.find_resource( + share_client.share_types, share_type + ) + + share_types_list.append(share_type_obj.name) + except Exception as e: + msg = _( + "Failed to find the share type with name or ID " + "'%(share_type)s': %(e)s" + ) + raise exceptions.CommandError( + msg % {'share_type': share_type, 'e': e} + ) + + kwargs['share_types'] = share_types_list + + if parsed_args.public: + kwargs['is_public'] = envvars.bool_from_str(parsed_args.public) + + group_specs: dict[str, Any] = {} + if parsed_args.group_specs: + for item in parsed_args.group_specs: + group_specs = utils.extract_group_specs(group_specs, [item]) + + kwargs['group_specs'] = group_specs + + share_group_type = share_client.share_group_types.create(**kwargs) + + formatter = parsed_args.formatter + + formatted_group_type = utils.format_share_group_type( + share_group_type, formatter + ) + + return ( + ATTRIBUTES, + oscutils.get_dict_properties(formatted_group_type, ATTRIBUTES), + ) + + +class DeleteShareGroupType(command.Command): + """Delete a share group type.""" + + _description = _("Delete a share group type") + + log = logging.getLogger(__name__ + ".DeleteShareGroupType") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'share_group_types', + metavar="", + nargs="+", + help=_("Name or ID of the share group type(s) to delete"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + result = 0 + + for share_group_type in parsed_args.share_group_types: + try: + share_group_type_obj = apiutils.find_resource( + share_client.share_group_types, share_group_type + ) + + share_client.share_group_types.delete(share_group_type_obj) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete share group type with " + "name or ID '%(share_group_type)s': %(e)s" + ), + {'share_group_type': share_group_type, 'e': e}, + ) + + if result > 0: + total = len(parsed_args.share_group_types) + msg = _( + "%(result)s of %(total)s share group types failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListShareGroupType(command.Lister): + """List Share Group Types.""" + + _description = _("List share types") + + log = logging.getLogger(__name__ + ".ListShareGroupType") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--all', + action='store_true', + default=False, + help=_( + 'Display all share group types whether public or private. ' + 'Default=False. (Admin only)' + ), + ) + parser.add_argument( + '--group-specs', + type=str, + nargs='*', + metavar='', + default=None, + help=_('Filter share group types with group specs (key=value).'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + share_client = self.app.client_manager.share + + search_opts = {} + if parsed_args.group_specs: + search_opts = { + 'group_specs': utils.extract_group_specs( + extra_specs={}, specs_to_add=parsed_args.group_specs + ) + } + + formatter = parsed_args.formatter + + share_group_types = share_client.share_group_types.list( + search_opts=search_opts, show_all=parsed_args.all + ) + + formatted_types = [] + for share_group_type in share_group_types: + formatted_types.append( + utils.format_share_group_type(share_group_type, formatter) + ) + + column_headers = utils.format_column_headers(ATTRIBUTES) + values = ( + oscutils.get_dict_properties(sgt, ATTRIBUTES) + for sgt in formatted_types + ) + + return (column_headers, values) + + +class ShowShareGroupType(command.ShowOne): + """Show Share Group Types.""" + + _description = _("Show share group types") + + log = logging.getLogger(__name__ + ".ShowShareGroupType") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'share_group_type', + metavar="", + help=_("Name or ID of the share group type to show"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], tuple[Any, ...]]: + share_client = self.app.client_manager.share + + share_group_type = apiutils.find_resource( + share_client.share_group_types, parsed_args.share_group_type + ) + + share_group_type_obj = share_client.share_group_types.get( + share_group_type + ) + + formatter = parsed_args.formatter + + formatted_group_type = utils.format_share_group_type( + share_group_type_obj, formatter + ) + + return ( + ATTRIBUTES, + oscutils.get_dict_properties(formatted_group_type, ATTRIBUTES), + ) + + +class SetShareGroupType(command.Command): + """Set share type properties.""" + + _description = _("Set share group type properties") + + log = logging.getLogger(__name__ + ".SetShareGroupType") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'share_group_type', + metavar="", + help=_("Name or ID of the share group type to modify"), + ) + parser.add_argument( + "--group-specs", + type=str, + nargs='*', + metavar='', + default=None, + help=_( + "Extra specs key and value of share group type that will be" + " used for share type creation. OPTIONAL: Default=None." + " Example: --group-specs consistent-snapshot-support=True" + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + + try: + share_group_type_obj = apiutils.find_resource( + share_client.share_group_types, parsed_args.share_group_type + ) + except Exception as e: + msg = _( + "Failed to find the share group type with " + "name or ID '%(share_group_type)s': %(e)s" + ) + raise exceptions.CommandError( + msg + % {'share_group_type': parsed_args.share_group_type, 'e': e} + ) + + if parsed_args.group_specs: + group_specs = utils.extract_group_specs( + extra_specs={}, specs_to_add=parsed_args.group_specs + ) + try: + share_group_type_obj.set_keys(group_specs) + except Exception as e: + raise exceptions.CommandError( + f"Failed to set share group type key: {e}" + ) + + +class UnsetShareGroupType(command.Command): + """Unset share group type extra specs.""" + + _description = _("Unset share group type extra specs") + + log = logging.getLogger(__name__ + ".UnsetShareGroupType") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'share_group_type', + metavar="", + help=_("Name or ID of the share grouptype to modify"), + ) + parser.add_argument( + 'group_specs', + metavar='', + nargs='+', + help=_('Remove group specs from this share group type'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + + try: + share_group_type_obj = apiutils.find_resource( + share_client.share_group_types, parsed_args.share_group_type + ) + except Exception as e: + msg = _( + "Failed to find the share group type with " + "name or ID '%(share_group_type)s': %(e)s" + ) + raise exceptions.CommandError( + msg + % {'share_group_type': parsed_args.share_group_type, 'e': e} + ) + + if parsed_args.group_specs: + try: + share_group_type_obj.unset_keys(parsed_args.group_specs) + except Exception as e: + raise exceptions.CommandError( + f"Failed to remove share type group extra spec: {e}" + ) diff --git a/openstackclient/share/v2/share_groups.py b/openstackclient/share/v2/share_groups.py new file mode 100644 index 0000000000..4ee54e2498 --- /dev/null +++ b/openstackclient/share/v2/share_groups.py @@ -0,0 +1,593 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 logging +from collections.abc import Iterable, Sequence +from typing import Any + +from manilaclient import api_versions +from osc_lib.cli import parseractions +from osc_lib import exceptions +from osc_lib import utils as osc_utils + +from openstackclient import command +from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common + +LOG = logging.getLogger(__name__) + + +class CreateShareGroup(command.ShowOne): + """Create new share group.""" + + _description = _("Create new share group") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--name', + metavar="", + default=None, + help=_('Share group name'), + ) + parser.add_argument( + "--description", + metavar="", + default=None, + help=_("Share group description."), + ) + parser.add_argument( + "--share-types", + metavar="", + nargs="+", + default=[], + help=_("Name or ID of share type(s)."), + ) + parser.add_argument( + "--share-group-type", + metavar="", + default=None, + help=_( + "Share group type name or ID of the share group to be created." + ), + ) + parser.add_argument( + "--share-network", + metavar="", + default=False, + help=_("Specify share network name or id"), + ) + parser.add_argument( + "--source-share-group-snapshot", + metavar="", + default=False, + help=_( + "Share group snapshot name or ID to create " + "the share group from." + ), + ) + parser.add_argument( + "--availability-zone", + metavar='', + default=None, + help=_( + "Optional availability zone in which group should be created" + ), + ) + parser.add_argument( + "--wait", + action='store_true', + default=False, + help=_('Wait for share group creation'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], tuple[Any, ...]]: + share_client = self.app.client_manager.share + + share_types = [] + for share_type in parsed_args.share_types: + share_types.append( + osc_utils.find_resource( + share_client.share_types, + share_type, + ) + ) + share_group_type = None + if parsed_args.share_group_type: + share_group_type = osc_utils.find_resource( + share_client.share_group_types, parsed_args.share_group_type + ).id + + share_network = None + if parsed_args.share_network: + share_network = osc_utils.find_resource( + share_client.share_networks, parsed_args.share_network + ).id + + source_share_group_snapshot = None + if parsed_args.source_share_group_snapshot: + source_share_group_snapshot = osc_utils.find_resource( + share_client.share_group_snapshots, + parsed_args.source_share_group_snapshot, + ).id + + body = { + 'name': parsed_args.name, + 'description': parsed_args.description, + 'share_types': share_types, + 'share_group_type': share_group_type, + 'share_network': share_network, + 'source_share_group_snapshot': source_share_group_snapshot, + 'availability_zone': parsed_args.availability_zone, + } + + share_group = share_client.share_groups.create(**body) + + if parsed_args.wait: + if not osc_utils.wait_for_status( + status_f=share_client.share_groups.get, + res_id=share_group.id, + success_status=['available'], + ): + LOG.error(_("ERROR: Share group is in error state.")) + + share_group = osc_utils.find_resource( + share_client.share_groups, share_group.id + ) + + printable_share_group = share_group._info + printable_share_group.pop('links', None) + + if printable_share_group.get('share_types'): + if parsed_args.formatter == 'table': + printable_share_group['share_types'] = "\n".join( + printable_share_group['share_types'] + ) + + return self.dict2columns(printable_share_group) + + +class DeleteShareGroup(command.Command): + """Delete one or more share groups.""" + + _description = _("Delete one or more share groups") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "share_group", + metavar="", + nargs="+", + help=_("Name or ID of the share group(s) to delete"), + ) + parser.add_argument( + "--force", + action='store_true', + default=False, + help=_( + "Attempt to force delete the share group (Default=False) " + "(Admin only)." + ), + ) + parser.add_argument( + "--wait", + action='store_true', + default=False, + help=_("Wait for share group to delete"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + result = 0 + + for share_group in parsed_args.share_group: + try: + share_group_obj = osc_utils.find_resource( + share_client.share_groups, share_group + ) + + share_client.share_groups.delete( + share_group_obj, force=parsed_args.force + ) + + if parsed_args.wait: + if not osc_utils.wait_for_delete( + manager=share_client.share_groups, + res_id=share_group_obj.id, + ): + result += 1 + + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete share group with " + "name or ID '%(share_group)s': %(e)s" + ), + {'share_group': share_group, 'e': e}, + ) + + if result > 0: + total = len(parsed_args.share_group) + msg = _( + "%(result)s of %(total)s share groups failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListShareGroup(command.Lister): + """List share groups.""" + + _description = _("List share groups") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "--all-projects", + action='store_true', + default=False, + help=_("Display share groups from all projects (Admin only)."), + ) + parser.add_argument( + "--name", + metavar="", + default=None, + help=_("Filter results by name."), + ) + parser.add_argument( + "--description", + metavar="", + default=None, + help=_( + "Filter results by description. Available " + "only for microversion >= 2.36." + ), + ) + parser.add_argument( + "--status", + metavar="", + default=None, + help=_("Filter results by status."), + ) + parser.add_argument( + "--share-server", + metavar="", + default=None, + help=_("Filter results by share server ID."), + ) + parser.add_argument( + "--share-group-type", + metavar="", + default=None, + help=_( + "Filter results by a share group type ID " + "or name that was used for share group " + "creation. " + ), + ) + parser.add_argument( + "--snapshot", + metavar="", + default=None, + help=_( + "Filter results by share group snapshot " + "name or ID that was used to create the " + "share group. " + ), + ) + parser.add_argument( + "--host", + metavar="", + default=None, + help=_("Filter results by host."), + ) + parser.add_argument( + "--share-network", + metavar="", + default=None, + help=_("Filter results by share-network name or ID. "), + ) + parser.add_argument( + "--project", + metavar="", + default=None, + help=_( + "Filter results by project name or ID. Useful with " + "set key '--all-projects'. " + ), + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + "--limit", + metavar="", + type=int, + default=None, + action=parseractions.NonNegativeAction, + help=_("Limit the number of share groups returned"), + ) + parser.add_argument( + "--marker", + metavar="", + help=_("The last share group ID of the previous page"), + ) + parser.add_argument( + '--sort', + metavar="[:]", + default='name:asc', + help=_( + "Sort output by selected keys and directions(asc or desc) " + "(default: name:asc), multiple keys and directions can be " + "specified separated by comma" + ), + ) + parser.add_argument( + "--name~", + metavar="", + default=None, + help=_( + "Filter results matching a share group " + "name pattern. Available only for " + "microversion >= 2.36. " + ), + ) + parser.add_argument( + "--description~", + metavar="", + default=None, + help=_( + "Filter results matching a share group " + "description pattern. Available only for " + "microversion >= 2.36. " + ), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + share_client = self.app.client_manager.share + identity_client = self.app.client_manager.identity + + share_server_id = None + if parsed_args.share_server: + share_server_id = osc_utils.find_resource( + share_client.share_servers, parsed_args.share_server + ).id + + share_group_type = None + if parsed_args.share_group_type: + share_group_type = osc_utils.find_resource( + share_client.share_group_types, parsed_args.share_group_type + ).id + + snapshot = None + if parsed_args.snapshot: + snapshot = osc_utils.find_resource( + share_client.share_snapshots, parsed_args.snapshot + ).id + + share_network = None + if parsed_args.share_network: + share_network = osc_utils.find_resource( + share_client.share_networks, parsed_args.share_network + ).id + + project_id = None + if parsed_args.project: + project_id = identity_common.find_project( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ).id + + columns = [ + 'ID', + 'Name', + 'Status', + 'Description', + ] + + search_opts = { + 'all_tenants': parsed_args.all_projects, + 'name': parsed_args.name, + 'status': parsed_args.status, + 'share_server_id': share_server_id, + 'share_group_type': share_group_type, + 'snapshot': snapshot, + 'host': parsed_args.host, + 'share_network': share_network, + 'project_id': project_id, + 'limit': parsed_args.limit, + 'offset': parsed_args.marker, + } + + if share_client.api_version >= api_versions.APIVersion("2.36"): + search_opts['name~'] = getattr(parsed_args, 'name~') + search_opts['description~'] = getattr(parsed_args, 'description~') + search_opts['description'] = parsed_args.description + elif ( + parsed_args.description + or getattr(parsed_args, 'name~') + or getattr(parsed_args, 'description~') + ): + raise exceptions.CommandError( + "Pattern based filtering (name~, description~ and description)" + " is only available with manila API version >= 2.36" + ) + + if parsed_args.all_projects: + columns.append('Project ID') + share_groups = share_client.share_groups.list(search_opts=search_opts) + + data = ( + osc_utils.get_dict_properties(share_group._info, columns) + for share_group in share_groups + ) + + return (columns, data) + + +class ShowShareGroup(command.ShowOne): + """Show share group.""" + + _description = _("Show details about a share group") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "share_group", + metavar="", + help=_("Name or ID of the share group."), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], tuple[Any, ...]]: + share_client = self.app.client_manager.share + + share_group = osc_utils.find_resource( + share_client.share_groups, parsed_args.share_group + ) + + printable_share_group = share_group._info + printable_share_group.pop('links', None) + + if printable_share_group.get('share_types'): + if parsed_args.formatter == 'table': + printable_share_group['share_types'] = "\n".join( + printable_share_group['share_types'] + ) + + return self.dict2columns(printable_share_group) + + +class SetShareGroup(command.Command): + """Set share group.""" + + _description = _("Explicitly set share group status") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'share_group', + metavar="", + help=_('Name or ID of the share group to update.'), + ) + parser.add_argument( + '--name', + metavar="", + default=None, + help=_('New name for the share group. (Default=None)'), + ) + parser.add_argument( + '--description', + metavar='', + default=None, + help=_('Share group description. (Default=None)'), + ) + parser.add_argument( + '--status', + metavar='', + default=None, + help=_( + 'Explicitly update the status of a share group (Admin ' + 'only). Examples include: available, error, creating, ' + 'deleting, error_deleting.' + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + result = 0 + + share_group = osc_utils.find_resource( + share_client.share_groups, parsed_args.share_group + ) + + kwargs = {} + if parsed_args.name is not None: + kwargs['name'] = parsed_args.name + if parsed_args.description is not None: + kwargs['description'] = parsed_args.description + if kwargs: + try: + share_client.share_groups.update(share_group.id, **kwargs) + except Exception as e: + LOG.error( + _("Failed to update share group name or description: %s"), + e, + ) + result += 1 + if parsed_args.status: + try: + share_group.reset_state(parsed_args.status) + except Exception as e: + LOG.error(_("Failed to set status for the share group: %s"), e) + result += 1 + + if result > 0: + raise exceptions.CommandError( + _("One or more of the set operations failed") + ) + + +class UnsetShareGroup(command.Command): + """Unset a share group property.""" + + _description = _("Unset a share group property") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'share_group', + metavar="", + help=_("Name or ID of the share group to set a property for."), + ) + parser.add_argument( + "--name", + action='store_true', + help=_("Unset share group name."), + ) + parser.add_argument( + "--description", + action='store_true', + help=_("Unset share group description."), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + share_client = self.app.client_manager.share + + share_group = osc_utils.find_resource( + share_client.share_groups, parsed_args.share_group + ) + + kwargs: dict[str, None] = {} + if parsed_args.name: + kwargs['name'] = None + if parsed_args.description: + kwargs['description'] = None + if kwargs: + try: + share_client.share_groups.update(share_group, **kwargs) + except Exception as e: + msg = _( + "Failed to unset share_group name or description: %(e)s" + ) + raise exceptions.CommandError(msg % {'e': e}) diff --git a/openstackclient/shell.py b/openstackclient/shell.py index 6bbbb5f7b6..6ff82961b5 100644 --- a/openstackclient/shell.py +++ b/openstackclient/shell.py @@ -16,7 +16,9 @@ """Command-line interface to the OpenStack APIs""" +import argparse import sys +from typing import Any import warnings from osc_lib.api import auth @@ -26,16 +28,34 @@ import openstackclient from openstackclient.common import clientmanager - DEFAULT_DOMAIN = 'default' +# list of modules that were originally out-of-tree and are now in +# core OSC +IGNORED_MODULES = ( + 'neutron_taas.taas_client.osc', + 'neutronclient.osc.v2.fwaas', + 'neutronclient.osc.v2.taas', + 'neutronclient.osc.v2.networking_bgpvpn', + 'neutronclient.osc.v2.dynamic_routing', + 'neutronclient.osc.v2.vpnaas', + # FIXME(stephenfin): this is ignored temporarily while we complete + # implementation. This should be dropped once completed. + 'openstackclient.share.v2', +) class OpenStackShell(shell.OpenStackShell): - def __init__(self): + client_manager: clientmanager.ClientManager + + def __init__(self) -> None: + command_manager = commandmanager.CommandManager( + 'openstack.cli', ignored_modules=IGNORED_MODULES + ) + super().__init__( description=__doc__.strip(), version=openstackclient.__version__, - command_manager=commandmanager.CommandManager('openstack.cli'), + command_manager=command_manager, deferred_help=True, ) @@ -48,13 +68,20 @@ def __init__(self): # about them warnings.filterwarnings('ignore', module='openstack') - def build_option_parser(self, description, version): - parser = super().build_option_parser(description, version) + def build_option_parser( + self, + description: str | None, + version: str | None, + argparse_kwargs: dict[str, Any] | None = None, + ) -> argparse.ArgumentParser: + parser = super().build_option_parser( + description, version, argparse_kwargs + ) parser = clientmanager.build_plugin_option_parser(parser) parser = auth.build_auth_plugins_option_parser(parser) return parser - def _final_defaults(self): + def _final_defaults(self) -> None: super()._final_defaults() # Set the default plugin to admin_token if endpoint and token are given @@ -64,11 +91,8 @@ def _final_defaults(self): else: self._auth_type = 'password' - def _load_plugins(self): - """Load plugins via stevedore - - osc-lib has no opinion on what plugins should be loaded - """ + def _load_plugins(self) -> None: + """Load plugins via stevedore.""" # Loop through extensions to get API versions for mod in clientmanager.PLUGIN_MODULES: default_version = getattr(mod, 'DEFAULT_API_VERSION', None) @@ -118,16 +142,43 @@ def _load_plugins(self): }, ) - # Command groups deal only with major versions - version = '.v' + version_opt.replace('.', '_').split('_')[0] - cmd_group = 'openstack.' + api.replace('-', '_') + version + # Build our command group which we expect to look like: + # + # openstack..vN + # + # Note that command groups deal only with major versions + cmd_group = '.'.join( + [ + 'openstack', + api.replace('-', '_'), + 'v' + version_opt.replace('.', '_').split('_')[0], + ] + ) self.command_manager.add_command_group(cmd_group) self.log.debug( '%(name)s API version %(version)s, cmd group %(group)s', {'name': api, 'version': version_opt, 'group': cmd_group}, ) - def _load_commands(self): + mod_extensions = getattr(mod, 'API_EXTENSIONS', None) + if not mod_extensions: + continue + + for extension in mod_extensions: + extension_cmd_group = '.'.join([cmd_group, extension]) + self.command_manager.add_command_group(extension_cmd_group) + self.log.debug( + '%(name)s API version %(version)s ' + '(%(extension)s extension), cmd group %(group)s', + { + 'name': api, + 'version': version_opt, + 'extension': extension, + 'group': cmd_group, + }, + ) + + def _load_commands(self) -> None: """Load commands via cliff/stevedore osc-lib has no opinion on what commands should be loaded @@ -147,7 +198,7 @@ def _load_commands(self): # } self.command_manager.add_command_group('openstack.extension') - def initialize_app(self, argv): + def initialize_app(self, argv: list[str]) -> None: super().initialize_app(argv) # Re-create the client_manager with our subclass @@ -158,7 +209,7 @@ def initialize_app(self, argv): ) -def main(argv=None): +def main(argv: list[str] | None = None) -> int: if argv is None: argv = sys.argv[1:] diff --git a/openstackclient/tests/functional/base.py b/openstackclient/tests/functional/base.py index 96c9accf6b..24d064b934 100644 --- a/openstackclient/tests/functional/base.py +++ b/openstackclient/tests/functional/base.py @@ -10,11 +10,13 @@ # License for the specific language governing permissions and limitations # under the License. +from collections.abc import Iterable, Mapping, Sequence import json import logging import os import shlex import subprocess +from typing import Any, Literal, cast, overload from tempest.lib.cli import output_parser from tempest.lib import exceptions @@ -24,7 +26,7 @@ LOG = logging.getLogger(__name__) -def execute(cmd, *, fail_ok=False): +def execute(cmd: str, *, fail_ok: bool = False) -> str: """Executes specified command for the given action.""" LOG.debug('Executing: %s', cmd) cmdlist = shlex.split(cmd) @@ -53,15 +55,37 @@ def execute(cmd, *, fail_ok=False): class TestCase(testtools.TestCase): + @overload @classmethod def openstack( cls, - cmd, + cmd: str, *, - cloud=ADMIN_CLOUD, - fail_ok=False, - parse_output=False, - ): + cloud: str | None = ADMIN_CLOUD, + fail_ok: bool = False, + parse_output: Literal[False] = False, + ) -> str: ... + + @overload + @classmethod + def openstack( + cls, + cmd: str, + *, + cloud: str | None = ADMIN_CLOUD, + fail_ok: bool = False, + parse_output: Literal[True] = ..., + ) -> Any: ... + + @classmethod + def openstack( + cls, + cmd: str, + *, + cloud: str | None = ADMIN_CLOUD, + fail_ok: bool = False, + parse_output: bool = False, + ) -> str | Any: """Executes openstackclient command for the given action :param cmd: A string representation of the command to execute. @@ -92,7 +116,7 @@ def openstack( format_args.append('-f json') output = execute( - ' '.join(['openstack'] + auth_args + [cmd] + format_args), + ' '.join(['openstack', *auth_args, cmd, *format_args]), fail_ok=fail_ok, ) @@ -106,7 +130,9 @@ def openstack( return output @classmethod - def is_service_enabled(cls, service, version=None): + def is_service_enabled( + cls, service: str, version: str | None = None + ) -> bool: """Ask client cloud if service is available :param service: The service name or type. This should be either an @@ -126,7 +152,9 @@ def is_service_enabled(cls, service, version=None): return bool(ret) @classmethod - def is_extension_enabled(cls, alias, *, service='network'): + def is_extension_enabled( + cls, alias: str, *, service: str = 'network' + ) -> bool: """Ask client cloud if extension is enabled""" extensions = cls.openstack( f'extension list --{service}', @@ -135,38 +163,44 @@ def is_extension_enabled(cls, alias, *, service='network'): return alias in [x['Alias'] for x in extensions] @classmethod - def get_openstack_configuration_value(cls, configuration): + def get_openstack_configuration_value(cls, configuration: str) -> str: opts = cls.get_opts([configuration]) return cls.openstack('configuration show ' + opts) @classmethod - def get_opts(cls, fields, output_format='value'): + def get_opts(cls, fields: list[str], output_format: str = 'value') -> str: return ' -f {} {}'.format( output_format, ' '.join(['-c ' + it for it in fields]) ) @classmethod - def assertOutput(cls, expected, actual): + def assertOutput(cls, expected: str, actual: str) -> None: if expected != actual: raise Exception(expected + ' != ' + actual) @classmethod - def assertInOutput(cls, expected, actual): + def assertInOutput(cls, expected: str, actual: str) -> None: if expected not in actual: raise Exception(expected + ' not in ' + actual) @classmethod - def assertsOutputNotNone(cls, observed): + def assertsOutputNotNone(cls, observed: Any) -> None: if observed is None: raise Exception('No output observed') - def assert_table_structure(self, items, field_names): + def assert_table_structure( + self, items: Iterable[Mapping[str, Any]], field_names: Sequence[str] + ) -> None: """Verify that all items have keys listed in field_names.""" for item in items: for field in field_names: self.assertIn(field, item) - def assert_show_fields(self, show_output, field_names): + def assert_show_fields( + self, + show_output: Iterable[Mapping[str, Any]], + field_names: Sequence[str], + ) -> None: """Verify that all items have keys listed in field_names.""" # field_names = ['name', 'description'] @@ -186,17 +220,18 @@ def parse_show_as_object(self, raw_output): o.update(item) return o - def parse_show(self, raw_output): + def parse_show(self, raw_output: str) -> list[dict[str, Any]]: """Return list of dicts with item values parsed from cli output.""" items = [] table_ = output_parser.table(raw_output) for row in table_['values']: item = {} - item[row[0]] = row[1] + item[str(row[0])] = row[1] items.append(item) return items - def parse_listing(self, raw_output): + def parse_listing(self, raw_output: str) -> list[dict[str, Any]]: """Return list of dicts with basic item parsed from cli output.""" - return output_parser.listing(raw_output) + # need to add hints to tempest + return cast(list[dict[str, Any]], output_parser.listing(raw_output)) diff --git a/openstackclient/tests/functional/common/test_extension.py b/openstackclient/tests/functional/common/test_extension.py index c65f52db51..1a8af9bc89 100644 --- a/openstackclient/tests/functional/common/test_extension.py +++ b/openstackclient/tests/functional/common/test_extension.py @@ -13,6 +13,8 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar + from tempest.lib import exceptions as tempest_exc from openstackclient.tests.functional import base @@ -21,6 +23,8 @@ class ExtensionTests(base.TestCase): """Functional tests for extension""" + haz_network: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/common/test_quota.py b/openstackclient/tests/functional/common/test_quota.py index 373b178c15..189d04bcc4 100644 --- a/openstackclient/tests/functional/common/test_quota.py +++ b/openstackclient/tests/functional/common/test_quota.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from tempest.lib.common.utils import data_utils @@ -25,7 +26,8 @@ class QuotaTests(base.TestCase): test runs as these may run in parallel and otherwise step on each other. """ - PROJECT_NAME: str + haz_network: ClassVar[bool] + PROJECT_NAME: ClassVar[str] @classmethod def setUpClass(cls): @@ -251,7 +253,10 @@ def test_quota_show_usage_option(self): self.assertEqual(sorted(expected_headers), sorted(row_headers)) resources.append(row['Resource']) for header in expected_headers[1:]: - self.assertIsInstance(row[header], int) + if row['Resource'] == 'key-pairs' and header == 'In Use': + self.assertEqual(row[header], "N/A") + else: + self.assertIsInstance(row[header], int) # Ensure that returned quota has network quota... self.assertIn("networks", resources) # ...and compute quota diff --git a/openstackclient/tests/functional/compute/v2/common.py b/openstackclient/tests/functional/compute/v2/common.py index 5892ee3a0b..70e8c95d28 100644 --- a/openstackclient/tests/functional/compute/v2/common.py +++ b/openstackclient/tests/functional/compute/v2/common.py @@ -12,6 +12,7 @@ # import time +from typing import cast import uuid from tempest.lib import exceptions @@ -35,18 +36,18 @@ def setUp(self): @classmethod def get_flavor(cls) -> str: + valid_flavors = ['m1.tiny', 'cirros256'] # NOTE(rtheis): Get cirros256 or m1.tiny flavors since functional # tests may create other flavors. flavors = cls.openstack("flavor list", parse_output=True) - server_flavor = None for flavor in flavors: if flavor['Name'] in ['m1.tiny', 'cirros256']: - server_flavor = flavor['Name'] - break - - assert server_flavor is not None + return cast(str, flavor['Name']) - return server_flavor + raise Exception( + f'Failed to find a suitable flavor. Required one of: ' + f'{", ".join(valid_flavors)}' + ) @classmethod def get_image(cls) -> str: @@ -54,18 +55,17 @@ def get_image(cls) -> str: # create other images. Image may be named '-uec' or # '-disk'. images = cls.openstack("image list", parse_output=True) - server_image = None for image in images: if image['Name'].startswith('cirros-') and ( image['Name'].endswith('-uec') or image['Name'].endswith('-disk') ): - server_image = image['Name'] - break + return cast(str, image['Name']) - assert server_image is not None - - return server_image + raise Exception( + 'Failed to find a suitable image. Required one matching one of ' + 'the following patterns: cirros-*-uec, cirros-*-disk' + ) @classmethod def get_network(cls) -> str: @@ -78,7 +78,8 @@ def get_network(cls) -> str: ) except exceptions.CommandFailed: return '' - return '--nic net-id=' + cmd_output['id'] + + return '='.join(('--nic net-id', cmd_output['id'])) def server_create(self, name=None, cleanup=True): """Create server, with cleanup""" diff --git a/openstackclient/tests/functional/compute/v2/test_flavor.py b/openstackclient/tests/functional/compute/v2/test_flavor.py index 4a0ff4883c..7f4cc43e71 100644 --- a/openstackclient/tests/functional/compute/v2/test_flavor.py +++ b/openstackclient/tests/functional/compute/v2/test_flavor.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional import base @@ -19,6 +20,7 @@ class FlavorTests(base.TestCase): """Functional tests for flavor.""" PROJECT_NAME = uuid.uuid4().hex + PROJECT_ID: ClassVar[str] @classmethod def setUpClass(cls): @@ -28,7 +30,7 @@ def setUpClass(cls): "project create --enable " + cls.PROJECT_NAME, parse_output=True, ) - cls.project_id = cmd_output["id"] + cls.PROJECT_ID = cmd_output["id"] @classmethod def tearDownClass(cls): diff --git a/openstackclient/tests/functional/compute/v2/test_server.py b/openstackclient/tests/functional/compute/v2/test_server.py index 6afa2c7c0e..c00d9dc555 100644 --- a/openstackclient/tests/functional/compute/v2/test_server.py +++ b/openstackclient/tests/functional/compute/v2/test_server.py @@ -13,6 +13,7 @@ import itertools import json import time +from typing import ClassVar import uuid from tempest.lib import exceptions @@ -25,6 +26,8 @@ class ServerTests(common.ComputeTestCase): """Functional tests for openstack server commands""" + haz_network: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/identity/v2/common.py b/openstackclient/tests/functional/identity/v2/common.py index dd2e271933..c17c713eb6 100644 --- a/openstackclient/tests/functional/identity/v2/common.py +++ b/openstackclient/tests/functional/identity/v2/common.py @@ -11,6 +11,7 @@ # under the License. import os +from typing import ClassVar import unittest import fixtures @@ -57,19 +58,22 @@ class IdentityTests(base.TestCase): CATALOG_LIST_HEADERS = ['Name', 'Type', 'Endpoints'] ENDPOINT_LIST_HEADERS = ['ID', 'Region', 'Service Name', 'Service Type'] + PROJECT_NAME: ClassVar[str] + PROJECT_DESCRIPTION: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() # create dummy project - cls.project_name = data_utils.rand_name('TestProject') - cls.project_description = data_utils.rand_name('description') + cls.PROJECT_NAME = data_utils.rand_name('TestProject') + cls.PROJECT_DESCRIPTION = data_utils.rand_name('description') try: cls.openstack( '--os-identity-api-version 2 ' 'project create ' - f'--description {cls.project_description} ' + f'--description {cls.PROJECT_DESCRIPTION} ' '--enable ' - f'{cls.project_name}' + f'{cls.PROJECT_NAME}' ) except tempest_exceptions.CommandFailed: # Good chance this is due to Identity v2 admin not being enabled @@ -83,7 +87,7 @@ def tearDownClass(cls): try: cls.openstack( '--os-identity-api-version 2 ' - f'project delete {cls.project_name}' + f'project delete {cls.PROJECT_NAME}' ) finally: super().tearDownClass() @@ -125,7 +129,7 @@ def _create_dummy_user(self, add_clean_up=True): email = data_utils.rand_name() + '@example.com' raw_output = self.openstack( 'user create ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--password {password} ' f'--email {email} ' '--enable ' diff --git a/openstackclient/tests/functional/identity/v2/test_role.py b/openstackclient/tests/functional/identity/v2/test_role.py index ec6134012f..58a04f5e2a 100644 --- a/openstackclient/tests/functional/identity/v2/test_role.py +++ b/openstackclient/tests/functional/identity/v2/test_role.py @@ -39,14 +39,14 @@ def test_role_add(self): username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}', ) @@ -58,13 +58,13 @@ def test_role_remove(self): username = self._create_dummy_user() add_raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}' ) del_raw_output = self.openstack( 'role remove ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}' ) diff --git a/openstackclient/tests/functional/identity/v3/common.py b/openstackclient/tests/functional/identity/v3/common.py index 9f21374ff5..9e66574bae 100644 --- a/openstackclient/tests/functional/identity/v3/common.py +++ b/openstackclient/tests/functional/identity/v3/common.py @@ -10,7 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. +import json import os +import tempfile +from typing import ClassVar import fixtures from tempest.lib.common.utils import data_utils @@ -74,6 +77,10 @@ class IdentityTests(base.TestCase): ] ENDPOINT_LIST_PROJECT_HEADERS = ['ID', 'Name'] + MAPPING_FIELDS = ['id', 'rules', 'schema_version'] + + MAPPING_LIST_HEADERS = ['ID', 'schema_version'] + IDENTITY_PROVIDER_FIELDS = [ 'description', 'enabled', @@ -147,30 +154,50 @@ class IdentityTests(base.TestCase): 'Region ID', ] + POLICY_FIELDS = [ + 'id', + 'rules', + 'type', + ] + POLICY_LIST_HEADERS = [ + 'ID', + 'Type', + ] + POLICY_LIST_LONG_HEADERS = [ + 'ID', + 'Type', + 'Rules', + ] + + DOMAIN_NAME: ClassVar[str] + DOMAIN_DESCRIPTION: ClassVar[str] + PROJECT_NAME: ClassVar[str] + PROJECT_DESCRIPTION: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() # create dummy domain - cls.domain_name = data_utils.rand_name('TestDomain') - cls.domain_description = data_utils.rand_name('description') + cls.DOMAIN_NAME = data_utils.rand_name('TestDomain') + cls.DOMAIN_DESCRIPTION = data_utils.rand_name('description') cls.openstack( '--os-identity-api-version 3 ' 'domain create ' - f'--description {cls.domain_description} ' + f'--description {cls.DOMAIN_DESCRIPTION} ' '--enable ' - f'{cls.domain_name}' + f'{cls.DOMAIN_NAME}' ) # create dummy project - cls.project_name = data_utils.rand_name('TestProject') - cls.project_description = data_utils.rand_name('description') + cls.PROJECT_NAME = data_utils.rand_name('TestProject') + cls.PROJECT_DESCRIPTION = data_utils.rand_name('description') cls.openstack( '--os-identity-api-version 3 ' 'project create ' - f'--domain {cls.domain_name} ' - f'--description {cls.project_description} ' + f'--domain {cls.DOMAIN_NAME} ' + f'--description {cls.PROJECT_DESCRIPTION} ' '--enable ' - f'{cls.project_name}' + f'{cls.PROJECT_NAME}' ) @classmethod @@ -179,15 +206,15 @@ def tearDownClass(cls): # delete dummy project cls.openstack( '--os-identity-api-version 3 ' - f'project delete {cls.project_name}' + f'project delete {cls.PROJECT_NAME}' ) # disable and delete dummy domain cls.openstack( '--os-identity-api-version 3 ' - f'domain set --disable {cls.domain_name}' + f'domain set --disable {cls.DOMAIN_NAME}' ) cls.openstack( - f'--os-identity-api-version 3 domain delete {cls.domain_name}' + f'--os-identity-api-version 3 domain delete {cls.DOMAIN_NAME}' ) finally: super().tearDownClass() @@ -213,9 +240,9 @@ def _create_dummy_user(self, add_clean_up=True): description = data_utils.rand_name('description') raw_output = self.openstack( 'user create ' - f'--domain {self.domain_name} ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--password {password} ' f'--email {email} ' f'--description {description} ' @@ -262,14 +289,14 @@ def _create_dummy_group(self, add_clean_up=True): description = data_utils.rand_name('description') raw_output = self.openstack( 'group create ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--description {description} ' f'{group_name}' ) if add_clean_up: self.addCleanup( self.openstack, - f'group delete --domain {self.domain_name} {group_name}', + f'group delete --domain {self.DOMAIN_NAME} {group_name}', ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.GROUP_FIELDS) @@ -295,14 +322,14 @@ def _create_dummy_project(self, add_clean_up=True): project_description = data_utils.rand_name('description') self.openstack( 'project create ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--description {project_description} ' f'--enable {project_name}' ) if add_clean_up: self.addCleanup( self.openstack, - f'project delete --domain {self.domain_name} {project_name}', + f'project delete --domain {self.DOMAIN_NAME} {project_name}', ) return project_name @@ -365,6 +392,34 @@ def _create_dummy_endpoint(self, interface='public', add_clean_up=True): self.assert_show_fields(items, self.ENDPOINT_FIELDS) return endpoint['id'] + def _create_dummy_mapping(self, add_clean_up=True): + mapping = data_utils.rand_name('Mapping') + # Create rules file + with tempfile.NamedTemporaryFile(mode='w+') as f: + RULES = [ + { + "local": [{"group": {"id": "85a868"}}], + "remote": [ + {"type": "orgPersonType", "any_one_of": ["Employee"]}, + {"type": "sn", "any_one_of": ["Young"]}, + ], + } + ] + f.write(json.dumps(RULES)) + f.flush() + raw_output = self.openstack( + f'mapping create {mapping} --rules {f.name} --schema-version 1.0' + ) + + if add_clean_up: + self.addCleanup( + self.openstack, + f'mapping delete {mapping}', + ) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.MAPPING_FIELDS) + return mapping + def _create_dummy_idp(self, add_clean_up=True): identity_provider = data_utils.rand_name('IdentityProvider') description = data_utils.rand_name('description') @@ -480,3 +535,30 @@ def _create_dummy_limit(self, add_clean_up=True): self.assert_show_fields(items, self.LIMIT_FIELDS) return limit_id + + def _create_dummy_policy(self, add_clean_up=True): + # Create rules file + with tempfile.NamedTemporaryFile(mode='w+') as f: + RULES = [ + { + "local": [{"group": {"id": "85a868"}}], + "remote": [ + {"type": "orgPersonType", "any_one_of": ["Employee"]}, + {"type": "sn", "any_one_of": ["Young"]}, + ], + } + ] + f.write(json.dumps(RULES)) + f.flush() + raw_output = self.openstack(f'policy create {f.name}') + items = self.parse_show(raw_output) + policy_id = self._extract_value_from_items('id', items) + + if add_clean_up: + self.addCleanup( + self.openstack, + f'policy delete {policy_id}', + ) + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.POLICY_FIELDS) + return policy_id diff --git a/openstackclient/tests/functional/identity/v3/test_access_rule.py b/openstackclient/tests/functional/identity/v3/test_access_rule.py index c8436c834d..4f7225801f 100644 --- a/openstackclient/tests/functional/identity/v3/test_access_rule.py +++ b/openstackclient/tests/functional/identity/v3/test_access_rule.py @@ -62,7 +62,7 @@ def setUp(self): items = self.parse_show_as_object(raw_output) self.access_rule_ids = [ - x['id'] for x in ast.literal_eval(items['access_rules']) + x['id'] for x in ast.literal_eval(items['Access Rules']) ] self.addCleanup( self.openstack, diff --git a/openstackclient/tests/functional/identity/v3/test_application_credential.py b/openstackclient/tests/functional/identity/v3/test_application_credential.py index a9c2286d57..129d82a61d 100644 --- a/openstackclient/tests/functional/identity/v3/test_application_credential.py +++ b/openstackclient/tests/functional/identity/v3/test_application_credential.py @@ -21,13 +21,13 @@ class ApplicationCredentialTests(common.IdentityTests): APPLICATION_CREDENTIAL_FIELDS = [ - 'id', - 'name', - 'project_id', - 'description', - 'roles', - 'expires_at', - 'unrestricted', + 'ID', + 'Name', + 'Project ID', + 'Description', + 'Roles', + 'Expires At', + 'Unrestricted', ] APPLICATION_CREDENTIAL_LIST_HEADERS = [ 'ID', @@ -107,7 +107,7 @@ def test_application_credential_create_with_options(self): secret = data_utils.rand_name('secret') description = data_utils.rand_name('description') tomorrow = ( - datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None) + datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + datetime.timedelta(days=1) ).strftime('%Y-%m-%dT%H:%M:%S%z') role1, role2 = self._create_role_assignments() diff --git a/openstackclient/tests/functional/identity/v3/test_catalog.py b/openstackclient/tests/functional/identity/v3/test_catalog.py index f4089b7eb8..429f94ac36 100644 --- a/openstackclient/tests/functional/identity/v3/test_catalog.py +++ b/openstackclient/tests/functional/identity/v3/test_catalog.py @@ -10,35 +10,54 @@ # License for the specific language governing permissions and limitations # under the License. + from openstackclient.tests.functional.identity.v3 import common class CatalogTests(common.IdentityTests): - def test_catalog_list(self): + """Functional tests for catalog commands""" + + def test_catalog(self): + """Test catalog list and show functionality""" + # Create a test service for isolated testing + _dummy_service_name = self._create_dummy_service(add_clean_up=True) + + # list catalogs raw_output = self.openstack('catalog list') items = self.parse_listing(raw_output) self.assert_table_structure(items, ['Name', 'Type', 'Endpoints']) - def test_catalog_show(self): - """test catalog show command - - The output example: - +-----------+----------------------------------------+ - | Field | Value | - +-----------+----------------------------------------+ - | endpoints | test1 | - | | public: http://localhost:5000/v2.0 | - | | test1 | - | | internal: http://localhost:5000/v2.0 | - | | test1 | - | | admin: http://localhost:35357/v2.0 | - | | | - | id | e1e68b5ba21a43a39ff1cf58e736c3aa | - | name | keystone | - | type | identity | - +-----------+----------------------------------------+ - """ - raw_output = self.openstack('catalog show {}'.format('identity')) + # Verify created service appears in catalog + service_names = [ + item.get('Name') for item in items if item.get('Name') + ] + self.assertIn( + _dummy_service_name, + service_names, + "Created dummy service should be present in catalog", + ) + + # show service (by name) + raw_output = self.openstack(f'catalog show {_dummy_service_name}') items = self.parse_show(raw_output) - # items may have multiple endpoint urls with empty key - self.assert_show_fields(items, ['endpoints', 'name', 'type', '', 'id']) + self.assert_show_fields(items, ['endpoints', 'name', 'type', 'id']) + + # Extract the type from the dummy service + _dummy_service_type = next( + (item['type'] for item in items if 'type' in item), None + ) + + # show service (by type) + raw_output = self.openstack(f'catalog show {_dummy_service_type}') + items = self.parse_show(raw_output) + self.assert_show_fields(items, ['endpoints', 'name', 'type', 'id']) + + # show service (non-existent) + result = self.openstack( + 'catalog show nonexistent-service-xyz', fail_ok=True + ) + self.assertEqual( + '', + result.strip(), + "Non-existent service should return empty result", + ) diff --git a/openstackclient/tests/functional/identity/v3/test_group.py b/openstackclient/tests/functional/identity/v3/test_group.py index a2e41d813a..0292d866bf 100644 --- a/openstackclient/tests/functional/identity/v3/test_group.py +++ b/openstackclient/tests/functional/identity/v3/test_group.py @@ -28,7 +28,7 @@ def test_group_list(self): def test_group_list_with_domain(self): group_name = self._create_dummy_group() - raw_output = self.openstack(f'group list --domain {self.domain_name}') + raw_output = self.openstack(f'group list --domain {self.DOMAIN_NAME}') items = self.parse_listing(raw_output) self.assert_table_structure(items, common.BASIC_LIST_HEADERS) self.assertIn(group_name, raw_output) @@ -36,14 +36,14 @@ def test_group_list_with_domain(self): def test_group_delete(self): group_name = self._create_dummy_group(add_clean_up=False) raw_output = self.openstack( - f'group delete --domain {self.domain_name} {group_name}' + f'group delete --domain {self.DOMAIN_NAME} {group_name}' ) self.assertEqual(0, len(raw_output)) def test_group_show(self): group_name = self._create_dummy_group() raw_output = self.openstack( - f'group show --domain {self.domain_name} {group_name}' + f'group show --domain {self.DOMAIN_NAME} {group_name}' ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.GROUP_FIELDS) @@ -53,20 +53,20 @@ def test_group_set(self): new_group_name = data_utils.rand_name('NewTestGroup') raw_output = self.openstack( 'group set ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--name {new_group_name} ' f'{group_name}' ) self.assertEqual(0, len(raw_output)) raw_output = self.openstack( - f'group show --domain {self.domain_name} {new_group_name}' + f'group show --domain {self.DOMAIN_NAME} {new_group_name}' ) group = self.parse_show_as_object(raw_output) self.assertEqual(new_group_name, group['name']) # reset group name to make sure it will be cleaned up raw_output = self.openstack( 'group set ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--name {group_name} ' f'{new_group_name}' ) @@ -77,15 +77,15 @@ def test_group_add_user(self): username = self._create_dummy_user() raw_output = self.openstack( 'group add user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}' ) self.addCleanup( self.openstack, 'group remove user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}', ) self.assertOutput('', raw_output) @@ -95,22 +95,22 @@ def test_group_contains_user(self): username = self._create_dummy_user() raw_output = self.openstack( 'group add user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}' ) self.addCleanup( self.openstack, 'group remove user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}', ) self.assertOutput('', raw_output) raw_output = self.openstack( 'group contains user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}' ) self.assertEqual( @@ -123,14 +123,14 @@ def test_group_remove_user(self): username = self._create_dummy_user() add_raw_output = self.openstack( 'group add user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}' ) remove_raw_output = self.openstack( 'group remove user ' - f'--group-domain {self.domain_name} ' - f'--user-domain {self.domain_name} ' + f'--group-domain {self.DOMAIN_NAME} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{group_name} {username}' ) self.assertOutput('', add_raw_output) diff --git a/openstackclient/tests/functional/identity/v3/test_idp.py b/openstackclient/tests/functional/identity/v3/test_idp.py index c9ef01d0f1..047b8b9ad8 100644 --- a/openstackclient/tests/functional/identity/v3/test_idp.py +++ b/openstackclient/tests/functional/identity/v3/test_idp.py @@ -56,7 +56,9 @@ def test_idp_set(self): raw_output = self.openstack( f'identity provider set ' f'{identity_provider} ' - f'--remote-id {new_remoteid}' + f'--authorization-ttl 7 ' + f'--remote-id {new_remoteid} ' + f'--remote-id {new_remoteid + "2nd"}' ) self.assertEqual(0, len(raw_output)) raw_output = self.openstack( diff --git a/openstackclient/tests/functional/identity/v3/test_limit.py b/openstackclient/tests/functional/identity/v3/test_limit.py index 8c0bbcd6a9..497d33a9c3 100644 --- a/openstackclient/tests/functional/identity/v3/test_limit.py +++ b/openstackclient/tests/functional/identity/v3/test_limit.py @@ -100,6 +100,53 @@ def test_limit_create_with_project_name(self): self.assert_show_fields(items, self.LIMIT_FIELDS) registered_limit_id = self._create_dummy_registered_limit() + def test_limit_create_with_project_domain(self): + registered_limit_id = self._create_dummy_registered_limit() + raw_output = self.openstack( + f'registered limit show {registered_limit_id}', + cloud=SYSTEM_CLOUD, + ) + items = self.parse_show(raw_output) + service_id = self._extract_value_from_items('service_id', items) + resource_name = self._extract_value_from_items('resource_name', items) + + raw_output = self.openstack(f'service show {service_id}') + items = self.parse_show(raw_output) + service_name = self._extract_value_from_items('name', items) + + project_name = self._create_dummy_project() + raw_output = self.openstack( + f'project show {project_name}', + cloud=SYSTEM_CLOUD, + ) + items = self.parse_show(raw_output) + domain_id = self._extract_value_from_items('domain_id', items) + + params = { + 'project_name': project_name, + 'project_domain': domain_id, + 'service_name': service_name, + 'resource_name': resource_name, + 'resource_limit': 15, + } + raw_output = self.openstack( + 'limit create' + ' --project {project_name}' + ' --project-domain {project_domain}' + ' --service {service_name}' + ' --resource-limit {resource_limit}' + ' {resource_name}'.format(**params), + cloud=SYSTEM_CLOUD, + ) + items = self.parse_show(raw_output) + limit_id = self._extract_value_from_items('id', items) + self.addCleanup( + self.openstack, f'limit delete {limit_id}', cloud=SYSTEM_CLOUD + ) + + self.assert_show_fields(items, self.LIMIT_FIELDS) + registered_limit_id = self._create_dummy_registered_limit() + def test_limit_create_with_service_id(self): self._create_dummy_limit() diff --git a/openstackclient/tests/functional/identity/v3/test_mapping.py b/openstackclient/tests/functional/identity/v3/test_mapping.py new file mode 100644 index 0000000000..f95f6de23b --- /dev/null +++ b/openstackclient/tests/functional/identity/v3/test_mapping.py @@ -0,0 +1,81 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import tempfile + +from openstackclient.tests.functional.identity.v3 import common + + +class MappingTests(common.IdentityTests): + def test_mapping_create(self): + self._create_dummy_mapping() + + def test_mapping_delete(self): + mapping = self._create_dummy_mapping(add_clean_up=False) + raw_output = self.openstack(f'mapping delete {mapping}') + self.assertEqual(0, len(raw_output)) + + def test_mapping_multi_delete(self): + mapping_1 = self._create_dummy_mapping(add_clean_up=False) + mapping_2 = self._create_dummy_mapping(add_clean_up=False) + raw_output = self.openstack(f'mapping delete {mapping_1} {mapping_2}') + self.assertEqual(0, len(raw_output)) + + def test_mapping_show(self): + mapping = self._create_dummy_mapping(add_clean_up=True) + raw_output = self.openstack(f'mapping show {mapping}') + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.MAPPING_FIELDS) + + def test_mapping_list(self): + self._create_dummy_mapping(add_clean_up=True) + raw_output = self.openstack('mapping list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.MAPPING_LIST_HEADERS) + + def test_mapping_set(self): + mapping = self._create_dummy_mapping(add_clean_up=True) + new_schema_version = '2.0' + with tempfile.NamedTemporaryFile(mode='w+') as f: + NEW_RULES = [ + { + "local": [{"group": {"id": "85a868"}}], + "remote": [ + {"type": "orgPersonType", "any_one_of": ["Employee"]}, + {"type": "sn", "any_one_of": ["Young"]}, + ], + }, + { + "local": [ + {"group": {"id": "0cd5e9"}}, + {"user": {"name": "0cd5e9"}}, + ], + "remote": [ + {"type": "UserName"}, + { + "type": "orgPersonType", + "not_any_of": ["Contractor", "SubContractor"], + }, + {"type": "LastName", "any_one_of": ["Bo"]}, + ], + }, + ] + f.write(json.dumps(NEW_RULES)) + f.flush() + raw_output = self.openstack( + f'mapping set {mapping} --rules {f.name} --schema-version {new_schema_version}' + ) + self.assertEqual(0, len(raw_output)) + raw_output = self.openstack(f'mapping show {mapping}') + updated_value = self.parse_show_as_object(raw_output) + self.assertEqual('2.0', updated_value['schema_version']) diff --git a/openstackclient/tests/functional/identity/v3/test_policy.py b/openstackclient/tests/functional/identity/v3/test_policy.py new file mode 100644 index 0000000000..34d35c50fb --- /dev/null +++ b/openstackclient/tests/functional/identity/v3/test_policy.py @@ -0,0 +1,86 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import json +import tempfile + +from openstackclient.tests.functional.identity.v3 import common + + +class PolicyTests(common.IdentityTests): + def test_policy_create(self): + self._create_dummy_policy() + + def test_policy_delete(self): + policy = self._create_dummy_policy(add_clean_up=False) + raw_output = self.openstack(f'policy delete {policy}') + self.assertEqual(0, len(raw_output)) + + def test_policy_multi_delete(self): + policy_1 = self._create_dummy_policy(add_clean_up=False) + policy_2 = self._create_dummy_policy(add_clean_up=False) + raw_output = self.openstack(f'policy delete {policy_1} {policy_2}') + self.assertEqual(0, len(raw_output)) + + def test_policy_show(self): + policy = self._create_dummy_policy(add_clean_up=True) + raw_output = self.openstack(f'policy show {policy}') + items = self.parse_show(raw_output) + self.assert_show_fields(items, self.POLICY_FIELDS) + + def test_policy_list(self): + self._create_dummy_policy(add_clean_up=True) + raw_output = self.openstack('policy list') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.POLICY_LIST_HEADERS) + + def test_policy_list_long(self): + self._create_dummy_policy(add_clean_up=True) + raw_output = self.openstack('policy list --long') + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.POLICY_LIST_LONG_HEADERS) + + def test_policy_set(self): + policy = self._create_dummy_policy(add_clean_up=True) + with tempfile.NamedTemporaryFile(mode='w+') as f: + NEW_RULES = [ + { + "local": [{"group": {"id": "85a868"}}], + "remote": [ + {"type": "orgPersonType", "any_one_of": ["Employee"]}, + {"type": "sn", "any_one_of": ["Young"]}, + ], + }, + { + "local": [ + {"group": {"id": "0cd5e9"}}, + {"user": {"name": "0cd5e9"}}, + ], + "remote": [ + {"type": "UserName"}, + { + "type": "orgPersonType", + "not_any_of": ["Contractor", "SubContractor"], + }, + {"type": "LastName", "any_one_of": ["Bo"]}, + ], + }, + ] + f.write(json.dumps(NEW_RULES)) + f.flush() + raw_output = self.openstack( + f'policy set {policy} --rules {f.name} --type text/json' + ) + self.assertEqual(0, len(raw_output)) + raw_output = self.openstack(f'policy show {policy}') + updated_value = self.parse_show_as_object(raw_output) + self.assertEqual('text/json', updated_value['type']) diff --git a/openstackclient/tests/functional/identity/v3/test_project.py b/openstackclient/tests/functional/identity/v3/test_project.py index 7a66c18518..8394377e4c 100644 --- a/openstackclient/tests/functional/identity/v3/test_project.py +++ b/openstackclient/tests/functional/identity/v3/test_project.py @@ -21,7 +21,7 @@ def test_project_create(self): description = data_utils.rand_name('description') raw_output = self.openstack( 'project create ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--description {description} ' '--enable ' '--property k1=v1 ' @@ -30,7 +30,7 @@ def test_project_create(self): ) self.addCleanup( self.openstack, - f'project delete --domain {self.domain_name} {project_name}', + f'project delete --domain {self.DOMAIN_NAME} {project_name}', ) items = self.parse_show(raw_output) show_fields = list(self.PROJECT_FIELDS) @@ -43,7 +43,7 @@ def test_project_create(self): def test_project_delete(self): project_name = self._create_dummy_project(add_clean_up=False) raw_output = self.openstack( - f'project delete --domain {self.domain_name} {project_name}' + f'project delete --domain {self.DOMAIN_NAME} {project_name}' ) self.assertEqual(0, len(raw_output)) @@ -55,7 +55,7 @@ def test_project_list(self): def test_project_list_with_domain(self): project_name = self._create_dummy_project() raw_output = self.openstack( - f'project list --domain {self.domain_name}' + f'project list --domain {self.DOMAIN_NAME}' ) items = self.parse_listing(raw_output) self.assert_table_structure(items, common.BASIC_LIST_HEADERS) @@ -75,7 +75,7 @@ def test_project_set(self): self.assertEqual(0, len(raw_output)) # check project details raw_output = self.openstack( - f'project show --domain {self.domain_name} {new_project_name}' + f'project show --domain {self.DOMAIN_NAME} {new_project_name}' ) items = self.parse_show(raw_output) fields = list(self.PROJECT_FIELDS) @@ -92,7 +92,7 @@ def test_project_set(self): def test_project_show(self): raw_output = self.openstack( - f'project show --domain {self.domain_name} {self.project_name}' + f'project show --domain {self.DOMAIN_NAME} {self.PROJECT_NAME}' ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.PROJECT_FIELDS) @@ -101,10 +101,10 @@ def test_project_show_with_parents_children(self): output = self.openstack( 'project show ' '--parents --children ' - f'--domain {self.domain_name} ' - f'{self.project_name}', + f'--domain {self.DOMAIN_NAME} ' + f'{self.PROJECT_NAME}', parse_output=True, ) - for attr_name in self.PROJECT_FIELDS + ['parents', 'subtree']: + for attr_name in [*self.PROJECT_FIELDS, 'parents', 'subtree']: self.assertIn(attr_name, output) - self.assertEqual(self.project_name, output.get('name')) + self.assertEqual(self.PROJECT_NAME, output.get('name')) diff --git a/openstackclient/tests/functional/identity/v3/test_role.py b/openstackclient/tests/functional/identity/v3/test_role.py index 3237c0bfb4..f4189aeef0 100644 --- a/openstackclient/tests/functional/identity/v3/test_role.py +++ b/openstackclient/tests/functional/identity/v3/test_role.py @@ -76,19 +76,19 @@ def test_role_add(self): username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{role_name}', ) self.assertEqual(0, len(raw_output)) @@ -98,20 +98,20 @@ def test_role_add_inherited(self): username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' '--inherited ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' '--inherited ' f'{role_name}', ) @@ -122,18 +122,18 @@ def test_role_remove(self): username = self._create_dummy_user() add_raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{role_name}' ) remove_raw_output = self.openstack( 'role remove ' - f'--project {self.project_name} ' - f'--project-domain {self.domain_name} ' + f'--project {self.PROJECT_NAME} ' + f'--project-domain {self.DOMAIN_NAME} ' f'--user {username} ' - f'--user-domain {self.domain_name} ' + f'--user-domain {self.DOMAIN_NAME} ' f'{role_name}' ) self.assertEqual(0, len(add_raw_output)) diff --git a/openstackclient/tests/functional/identity/v3/test_role_assignment.py b/openstackclient/tests/functional/identity/v3/test_role_assignment.py index 76e33c286e..373f7a8ef9 100644 --- a/openstackclient/tests/functional/identity/v3/test_role_assignment.py +++ b/openstackclient/tests/functional/identity/v3/test_role_assignment.py @@ -62,52 +62,226 @@ def test_role_assignment_list_group(self): items = self.parse_listing(raw_output) self.assert_table_structure(items, self.ROLE_ASSIGNMENT_LIST_HEADERS) + def test_role_assignment_list_group_domain(self): + domain_name_A = self._create_dummy_domain() + domain_name_B = self._create_dummy_domain() + role_name = self._create_dummy_role() + group_name = 'group_name' + self.openstack(f'group create --domain {domain_name_A} {group_name}') + self.addCleanup( + self.openstack, + f'group delete --domain {domain_name_A} {group_name}', + ) + self.openstack(f'group create --domain {domain_name_B} {group_name}') + self.addCleanup( + self.openstack, + f'group delete --domain {domain_name_B} {group_name}', + ) + raw_output = self.openstack( + 'role add ' + f'--project {self.PROJECT_NAME} ' + f'--group {group_name} --group-domain {domain_name_A} ' + f'{role_name}' + ) + self.addCleanup( + self.openstack, + 'role remove ' + f'--project {self.PROJECT_NAME} ' + f'--group {group_name} --group-domain {domain_name_A} ' + f'{role_name}', + ) + self.assertEqual('', raw_output.strip()) + raw_output = self.openstack( + f'role assignment list ' + f'--group {group_name} --group-domain {domain_name_A} ' + ) + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.ROLE_ASSIGNMENT_LIST_HEADERS) + raw_output = self.openstack( + f'role assignment list ' + f'--group {group_name} --group-domain {domain_name_B} ' + ) + self.assertEqual('', raw_output.strip()) + def test_role_assignment_list_domain(self): role_name = self._create_dummy_role() username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--user {username} ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--domain {self.domain_name} ' + f'--domain {self.DOMAIN_NAME} ' f'--user {username} ' f'{role_name}', ) self.assertEqual(0, len(raw_output)) raw_output = self.openstack( - f'role assignment list --domain {self.domain_name} ' + f'role assignment list --domain {self.DOMAIN_NAME} ' + ) + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.ROLE_ASSIGNMENT_LIST_HEADERS) + + def test_role_assignment_list_user_domain(self): + domain_name_A = self._create_dummy_domain() + domain_name_B = self._create_dummy_domain() + role_name = self._create_dummy_role() + username = 'username' + self.openstack(f'user create --domain {domain_name_A} {username}') + self.addCleanup( + self.openstack, f'user delete --domain {domain_name_A} {username}' + ) + self.openstack(f'user create --domain {domain_name_B} {username}') + self.addCleanup( + self.openstack, f'user delete --domain {domain_name_B} {username}' + ) + raw_output = self.openstack( + 'role add ' + f'--project {self.PROJECT_NAME} ' + f'--user {username} --user-domain {domain_name_A} ' + f'{role_name}' + ) + self.addCleanup( + self.openstack, + 'role remove ' + f'--project {self.PROJECT_NAME} ' + f'--user {username} --user-domain {domain_name_A} ' + f'{role_name}', + ) + self.assertEqual('', raw_output.strip()) + raw_output = self.openstack( + f'role assignment list ' + f'--user {username} --user-domain {domain_name_A} ' + ) + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.ROLE_ASSIGNMENT_LIST_HEADERS) + raw_output = self.openstack( + f'role assignment list ' + f'--user {username} --user-domain {domain_name_B} ' + ) + self.assertEqual('', raw_output.strip()) + + def test_role_assignment_list_role_domain(self): + domain_name_A = self._create_dummy_domain() + domain_name_B = self._create_dummy_domain() + role_name = 'role_name' + username = 'username' + self.openstack(f'role create --domain {domain_name_A} {role_name}') + self.addCleanup( + self.openstack, f'role delete --domain {domain_name_A} {role_name}' + ) + self.openstack(f'role create --domain {domain_name_B} {role_name}') + self.addCleanup( + self.openstack, f'role delete --domain {domain_name_B} {role_name}' + ) + self.openstack(f'user create --domain {domain_name_A} {username}') + self.addCleanup( + self.openstack, f'user delete --domain {domain_name_A} {username}' + ) + raw_output = self.openstack( + 'role add ' + f'--user {username} --domain {domain_name_A} ' + f'--role-domain {domain_name_A} ' + f'{role_name}' + ) + self.addCleanup( + self.openstack, + 'role remove ' + f'--user {username} --domain {domain_name_A} ' + f'--role-domain {domain_name_A} ' + f'{role_name}', + ) + self.assertEqual('', raw_output.strip()) + raw_output = self.openstack( + f'role assignment list ' + f'--role {role_name} --role-domain {domain_name_A}' ) items = self.parse_listing(raw_output) self.assert_table_structure(items, self.ROLE_ASSIGNMENT_LIST_HEADERS) + raw_output = self.openstack( + f'role assignment list ' + f'--role {role_name} --role-domain {domain_name_B}' + ) + items = self.parse_listing(raw_output) + self.assertEqual('', raw_output.strip()) def test_role_assignment_list_project(self): role_name = self._create_dummy_role() username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}' ) self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' f'{role_name}', ) self.assertEqual(0, len(raw_output)) raw_output = self.openstack( - f'role assignment list --project {self.project_name} ' + f'role assignment list --project {self.PROJECT_NAME} ' ) items = self.parse_listing(raw_output) self.assert_table_structure(items, self.ROLE_ASSIGNMENT_LIST_HEADERS) + def test_role_assignment_list_project_domain(self): + domain_name_A = self._create_dummy_domain() + domain_name_B = self._create_dummy_domain() + role_name = self._create_dummy_role() + project_name = 'project_name' + username = 'username' + self.openstack( + f'project create --domain {domain_name_A} {project_name}' + ) + self.addCleanup( + self.openstack, + f'project delete --domain {domain_name_A} {project_name}', + ) + self.openstack( + f'project create --domain {domain_name_B} {project_name}' + ) + self.addCleanup( + self.openstack, + f'project delete --domain {domain_name_B} {project_name}', + ) + self.openstack(f'user create --domain {domain_name_A} {username}') + self.addCleanup( + self.openstack, f'user delete --domain {domain_name_A} {username}' + ) + raw_output = self.openstack( + 'role add ' + f'--project {project_name} --project-domain {domain_name_A} ' + f'--user {username} --user-domain {domain_name_A} ' + f'{role_name}' + ) + self.addCleanup( + self.openstack, + 'role remove ' + f'--project {project_name} --project-domain {domain_name_A} ' + f'--user {username} --user-domain {domain_name_A} ' + f'{role_name}', + ) + self.assertEqual('', raw_output.strip()) + raw_output = self.openstack( + f'role assignment list ' + f'--project {project_name} --project-domain {domain_name_A} ' + ) + items = self.parse_listing(raw_output) + self.assert_table_structure(items, self.ROLE_ASSIGNMENT_LIST_HEADERS) + raw_output = self.openstack( + f'role assignment list ' + f'--project {project_name} --project-domain {domain_name_B} ' + ) + self.assertEqual('', raw_output.strip()) + def test_role_assignment_list_effective(self): raw_output = self.openstack('role assignment list --effective') items = self.parse_listing(raw_output) @@ -128,7 +302,7 @@ def test_role_assignment_list_inherited(self): username = self._create_dummy_user() raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' '--inherited ' f'{role_name}' @@ -136,7 +310,7 @@ def test_role_assignment_list_inherited(self): self.addCleanup( self.openstack, 'role remove ' - f'--project {self.project_name} ' + f'--project {self.PROJECT_NAME} ' f'--user {username} ' '--inherited ' f'{role_name}', diff --git a/openstackclient/tests/functional/identity/v3/test_user.py b/openstackclient/tests/functional/identity/v3/test_user.py index dd56293e63..a5e7f1adbe 100644 --- a/openstackclient/tests/functional/identity/v3/test_user.py +++ b/openstackclient/tests/functional/identity/v3/test_user.py @@ -22,7 +22,7 @@ def test_user_create(self): def test_user_delete(self): username = self._create_dummy_user(add_clean_up=False) raw_output = self.openstack( - f'user delete --domain {self.domain_name} {username}' + f'user delete --domain {self.DOMAIN_NAME} {username}' ) self.assertEqual(0, len(raw_output)) @@ -34,7 +34,7 @@ def test_user_list(self): def test_user_set(self): username = self._create_dummy_user() raw_output = self.openstack( - f'user show --domain {self.domain_name} {username}' + f'user show --domain {self.DOMAIN_NAME} {username}' ) user = self.parse_show_as_object(raw_output) new_username = data_utils.rand_name('NewTestUser') @@ -46,7 +46,7 @@ def test_user_set(self): ) self.assertEqual(0, len(raw_output)) raw_output = self.openstack( - f'user show --domain {self.domain_name} {new_username}' + f'user show --domain {self.DOMAIN_NAME} {new_username}' ) updated_user = self.parse_show_as_object(raw_output) self.assertEqual(user['id'], updated_user['id']) @@ -57,7 +57,7 @@ def test_user_set_default_project_id(self): project_name = self._create_dummy_project() # get original user details raw_output = self.openstack( - f'user show --domain {self.domain_name} {username}' + f'user show --domain {self.DOMAIN_NAME} {username}' ) user = self.parse_show_as_object(raw_output) # update user @@ -67,19 +67,19 @@ def test_user_set_default_project_id(self): '--project-domain {project_domain} ' '{id}'.format( project=project_name, - project_domain=self.domain_name, + project_domain=self.DOMAIN_NAME, id=user['id'], ) ) self.assertEqual(0, len(raw_output)) # get updated user details raw_output = self.openstack( - f'user show --domain {self.domain_name} {username}' + f'user show --domain {self.DOMAIN_NAME} {username}' ) updated_user = self.parse_show_as_object(raw_output) # get project details raw_output = self.openstack( - f'project show --domain {self.domain_name} {project_name}' + f'project show --domain {self.DOMAIN_NAME} {project_name}' ) project = self.parse_show_as_object(raw_output) # check updated user details @@ -89,7 +89,7 @@ def test_user_set_default_project_id(self): def test_user_show(self): username = self._create_dummy_user() raw_output = self.openstack( - f'user show --domain {self.domain_name} {username}' + f'user show --domain {self.DOMAIN_NAME} {username}' ) items = self.parse_show(raw_output) self.assert_show_fields(items, self.USER_FIELDS) diff --git a/openstackclient/tests/functional/image/base.py b/openstackclient/tests/functional/image/base.py index d948f81551..174c47c637 100644 --- a/openstackclient/tests/functional/image/base.py +++ b/openstackclient/tests/functional/image/base.py @@ -16,9 +16,4 @@ class BaseImageTests(base.TestCase): """Functional tests for Image commands""" - @classmethod - def setUpClass(cls): - super().setUpClass() - # TODO(dtroyer): maybe do image API discovery here to determine - # what is available, it isn't in the service catalog - cls.haz_v1_api = False + ... diff --git a/openstackclient/tests/functional/image/v1/test_image.py b/openstackclient/tests/functional/image/v1/test_image.py deleted file mode 100644 index c4118babba..0000000000 --- a/openstackclient/tests/functional/image/v1/test_image.py +++ /dev/null @@ -1,97 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import uuid - -import fixtures - -from openstackclient.tests.functional.image import base - - -class ImageTests(base.BaseImageTests): - """Functional tests for Image commands""" - - def setUp(self): - super().setUp() - - if not self.haz_v1_api: - self.skipTest('No Image v1 API present') - - ver_fixture = fixtures.EnvironmentVariable('OS_IMAGE_API_VERSION', '1') - self.useFixture(ver_fixture) - - self.name = uuid.uuid4().hex - output = self.openstack( - 'image create ' + self.name, - parse_output=True, - ) - self.image_id = output["id"] - self.assertOutput(self.name, output['name']) - - def tearDown(self): - try: - self.openstack('image delete ' + self.image_id) - finally: - super().tearDown() - - def test_image_list(self): - output = self.openstack('image list') - self.assertIn(self.name, [img['Name'] for img in output]) - - def test_image_attributes(self): - """Test set, unset, show on attributes, tags and properties""" - - # Test explicit attributes - self.openstack( - 'image set ' - + '--min-disk 4 ' - + '--min-ram 5 ' - + '--disk-format qcow2 ' - + '--public ' - + self.name - ) - output = self.openstack( - 'image show ' + self.name, - parse_output=True, - ) - self.assertEqual( - 4, - output["min_disk"], - ) - self.assertEqual( - 5, - output["min_ram"], - ) - self.assertEqual( - 'qcow2', - output['disk_format'], - ) - self.assertTrue( - output["is_public"], - ) - - # Test properties - self.openstack( - 'image set ' - + '--property a=b ' - + '--property c=d ' - + '--public ' - + self.name - ) - output = self.openstack( - 'image show ' + self.name, - parse_output=True, - ) - self.assertEqual( - {'a': 'b', 'c': 'd'}, - output["properties"], - ) diff --git a/openstackclient/tests/functional/image/v2/test_cache.py b/openstackclient/tests/functional/image/v2/test_cache.py new file mode 100644 index 0000000000..58245e5ca8 --- /dev/null +++ b/openstackclient/tests/functional/image/v2/test_cache.py @@ -0,0 +1,54 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from openstackclient.tests.functional.image import base + + +class CacheTests(base.BaseImageTests): + """Functional tests for Cache commands""" + + def test_cached_image(self): + """Test cached image operations including queue and clear""" + # Create test image + name = uuid.uuid4().hex + output = self.openstack( + f'image create {name}', + parse_output=True, + ) + image_id = output["id"] + self.assertOutput(name, output['name']) + + # Register cleanup for created image + self.addCleanup( + self.openstack, 'cached image delete ' + image_id, fail_ok=True + ) + self.addCleanup(self.openstack, 'image delete ' + image_id) + + # Queue image for caching + self.openstack('cached image queue ' + image_id) + + # Verify queuing worked + cache_output = self.openstack('cached image list', parse_output=True) + self.assertIsInstance(cache_output, list) + image_ids = [img['ID'] for img in cache_output] + self.assertIn(image_id, image_ids) + + # Clear cached images + self.openstack('cached image clear') + + # Verify clearing worked + output = self.openstack('cached image list', parse_output=True) + if output: + image_ids = [img['ID'] for img in output] + self.assertNotIn(image_id, image_ids) diff --git a/openstackclient/tests/functional/image/v2/test_image.py b/openstackclient/tests/functional/image/v2/test_image.py index 4d51737c31..828488c42c 100644 --- a/openstackclient/tests/functional/image/v2/test_image.py +++ b/openstackclient/tests/functional/image/v2/test_image.py @@ -218,17 +218,39 @@ def test_image_members(self): 'image remove project ' + self.name + ' ' + my_project_id ) - # else: - # # Test not shared - # self.assertRaises( - # image_exceptions.HTTPForbidden, - # self.openstack, - # 'image add project ' + - # self.name + ' ' + - # my_project_id - # ) - # self.openstack( - # 'image set ' + - # '--share ' + - # self.name - # ) + def test_image_hidden(self): + # Test image is shown in list + output = self.openstack( + 'image list', + parse_output=True, + ) + self.assertIn( + self.name, + [img['Name'] for img in output], + ) + + # Hide the image and test image not show in the list + self.openstack('image set ' + '--hidden ' + self.name) + output = self.openstack( + 'image list', + parse_output=True, + ) + self.assertNotIn(self.name, [img['Name'] for img in output]) + + # Test image show in the list with flag + output = self.openstack( + 'image list', + parse_output=True, + ) + self.assertNotIn(self.name, [img['Name'] for img in output]) + + # Unhide the image and test image is again visible in regular list + self.openstack('image set ' + '--unhidden ' + self.name) + output = self.openstack( + 'image list', + parse_output=True, + ) + self.assertIn( + self.name, + [img['Name'] for img in output], + ) diff --git a/openstackclient/tests/functional/image/v2/test_metadef_objects.py b/openstackclient/tests/functional/image/v2/test_metadef_objects.py new file mode 100644 index 0000000000..5216c933fd --- /dev/null +++ b/openstackclient/tests/functional/image/v2/test_metadef_objects.py @@ -0,0 +1,69 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from openstackclient.tests.functional import base + + +class MetadefObjectTests(base.TestCase): + def setUp(self): + super().setUp() + self.obj_name = self.getUniqueString('metadef-obj') + self.ns_name = self.getUniqueString('metadef-ns') + self.openstack(f"image metadef namespace create {self.ns_name}") + self.addCleanup( + lambda: self.openstack( + f"image metadef namespace delete {self.ns_name}" + ) + ) + + def test_metadef_objects(self): + # CREATE + created = self.openstack( + ( + "image metadef object create " + f"--namespace {self.ns_name} " + f"{self.obj_name}" + ), + parse_output=True, + ) + self.addCleanup( + lambda: self.openstack( + f"image metadef object delete {self.ns_name} {self.obj_name}" + ) + ) + self.assertEqual(self.obj_name, created["name"]) + self.assertEqual(self.ns_name, created["namespace_name"]) + + # UPDATE + new_name = f"{self.obj_name}-updated" + self.openstack( + "image metadef object update " + f"{self.ns_name} {self.obj_name} " + f"--name {new_name}" + ) + self.obj_name = new_name + + # READ (get) + shown = self.openstack( + f"image metadef object show {self.ns_name} {self.obj_name}", + parse_output=True, + ) + self.assertEqual(self.obj_name, shown["name"]) + self.assertEqual(self.ns_name, shown["namespace_name"]) + + # READ (list) + rows = self.openstack( + f"image metadef object list {self.ns_name}", + parse_output=True, + ) + names = {row["name"] for row in rows} + self.assertIn(self.obj_name, names) diff --git a/openstackclient/tests/functional/image/v2/test_metadef_resource_type.py b/openstackclient/tests/functional/image/v2/test_metadef_resource_type.py new file mode 100644 index 0000000000..ab8dc13ef0 --- /dev/null +++ b/openstackclient/tests/functional/image/v2/test_metadef_resource_type.py @@ -0,0 +1,55 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from openstackclient.tests.functional.image import base + + +class ImageMetadefResourceTypeTests(base.BaseImageTests): + """Functional tests for image metadef resource type commands.""" + + def setUp(self): + super().setUp() + + # Create unique namespace name using UUID + self.namespace_name = 'test-mdef-ns-' + uuid.uuid4().hex + self.resource_type_name = 'test-mdef-rt-' + uuid.uuid4().hex + + # Create namespace + self.openstack('image metadef namespace create ' + self.namespace_name) + self.addCleanup( + self.openstack, + 'image metadef namespace delete ' + self.namespace_name, + ) + + def test_metadef_resource_type(self): + """Test image metadef resource type commands""" + + self.openstack( + 'image metadef resource type association create ' + f'{self.namespace_name} {self.resource_type_name}', + ) + self.addCleanup( + self.openstack, + 'image metadef resource type association delete ' + f'{self.namespace_name} {self.resource_type_name}', + ) + + output = self.openstack( + 'image metadef resource type list', + parse_output=True, + ) + + self.assertIn( + self.resource_type_name, [item['Name'] for item in output] + ) diff --git a/openstackclient/tests/functional/network/v2/common.py b/openstackclient/tests/functional/network/v2/common.py index 248758a843..00c0b57e3d 100644 --- a/openstackclient/tests/functional/network/v2/common.py +++ b/openstackclient/tests/functional/network/v2/common.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional import base @@ -18,6 +19,8 @@ class NetworkTests(base.TestCase): """Functional tests for Network commands""" + haz_network: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() @@ -33,7 +36,7 @@ def setUp(self): class NetworkTagTests(NetworkTests): """Functional tests with tag operation""" - base_command: str + base_command: ClassVar[str] def test_tag_operation(self): # Get project IDs @@ -84,7 +87,7 @@ def _list_tag_check(self, project_id, expected): parse_output=True, ) for name, tags in expected: - net = [n for n in cmd_output if n['Name'] == name][0] + net = next(n for n in cmd_output if n['Name'] == name) self.assertEqual(set(tags), set(net['Tags'])) def _create_resource_for_tag_test(self, name, args): diff --git a/openstackclient/tests/functional/network/v2/test_floating_ip.py b/openstackclient/tests/functional/network/v2/test_floating_ip.py index a1b11a44a3..c2ce00d481 100644 --- a/openstackclient/tests/functional/network/v2/test_floating_ip.py +++ b/openstackclient/tests/functional/network/v2/test_floating_ip.py @@ -11,6 +11,7 @@ # under the License. import random +from typing import ClassVar import uuid from openstackclient.tests.functional.network.v2 import common @@ -19,6 +20,11 @@ class FloatingIpTests(common.NetworkTests): """Functional tests for floating ip""" + EXTERNAL_NETWORK_NAME: ClassVar[str] + EXTERNAL_NETWORK_ID: ClassVar[str] + PRIVATE_NETWORK_NAME: ClassVar[str] + PRIVATE_NETWORK_ID: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() @@ -32,14 +38,14 @@ def setUpClass(cls): 'network create ' + '--external ' + cls.EXTERNAL_NETWORK_NAME, parse_output=True, ) - cls.external_network_id = json_output["id"] + cls.EXTERNAL_NETWORK_ID = json_output["id"] # Create a private network for the port json_output = cls.openstack( 'network create ' + cls.PRIVATE_NETWORK_NAME, parse_output=True, ) - cls.private_network_id = json_output["id"] + cls.PRIVATE_NETWORK_ID = json_output["id"] @classmethod def tearDownClass(cls): @@ -59,8 +65,8 @@ def setUp(self): super().setUp() # Verify setup - self.assertIsNotNone(self.external_network_id) - self.assertIsNotNone(self.private_network_id) + self.assertIsNotNone(self.EXTERNAL_NETWORK_ID) + self.assertIsNotNone(self.PRIVATE_NETWORK_ID) def _create_subnet(self, network_name, subnet_name): subnet_id = None diff --git a/openstackclient/tests/functional/network/v2/test_ip_availability.py b/openstackclient/tests/functional/network/v2/test_ip_availability.py index 1cdbd487a5..be1df9108a 100644 --- a/openstackclient/tests/functional/network/v2/test_ip_availability.py +++ b/openstackclient/tests/functional/network/v2/test_ip_availability.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional.network.v2 import common @@ -18,6 +19,9 @@ class IPAvailabilityTests(common.NetworkTests): """Functional tests for IP availability""" + NAME: ClassVar[str] + NETWORK_NAME: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/network/v2/test_network_meter_rule.py b/openstackclient/tests/functional/network/v2/test_network_meter_rule.py index c80643e31d..268794298c 100644 --- a/openstackclient/tests/functional/network/v2/test_network_meter_rule.py +++ b/openstackclient/tests/functional/network/v2/test_network_meter_rule.py @@ -13,6 +13,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import unittest import uuid @@ -22,8 +23,8 @@ class TestMeterRule(common.NetworkTests): """Functional tests for meter rule""" - METER_ID: str - METER_RULE_ID: str + METER_ID: ClassVar[str] + METER_NAME: ClassVar[str] @classmethod def setUpClass(cls): diff --git a/openstackclient/tests/functional/network/v2/test_network_segment.py b/openstackclient/tests/functional/network/v2/test_network_segment.py index 03f5daf743..df26bcc8d7 100644 --- a/openstackclient/tests/functional/network/v2/test_network_segment.py +++ b/openstackclient/tests/functional/network/v2/test_network_segment.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional.network.v2 import common @@ -18,6 +19,10 @@ class NetworkSegmentTests(common.NetworkTests): """Functional tests for network segment""" + NETWORK_NAME: ClassVar[str] + NETWORK_ID: ClassVar[str] + PHYSICAL_NETWORK_NAME: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/network/v2/test_subnet.py b/openstackclient/tests/functional/network/v2/test_subnet.py index 2ec987e9b8..6af7c94541 100644 --- a/openstackclient/tests/functional/network/v2/test_subnet.py +++ b/openstackclient/tests/functional/network/v2/test_subnet.py @@ -11,6 +11,7 @@ # under the License. import random +from typing import ClassVar import uuid from openstackclient.tests.functional.network.v2 import common @@ -21,19 +22,22 @@ class SubnetTests(common.NetworkTagTests): base_command = 'subnet' + NETWORK_NAME: ClassVar[str] + NETWORK_ID: ClassVar[str] + @classmethod def setUpClass(cls): super().setUpClass() - if cls.haz_network: - cls.NETWORK_NAME = uuid.uuid4().hex - # Create a network for the all subnet tests - cmd_output = cls.openstack( - 'network create ' + cls.NETWORK_NAME, - parse_output=True, - ) - # Get network_id for assertEqual - cls.NETWORK_ID = cmd_output["id"] + cls.NETWORK_NAME = uuid.uuid4().hex + + # Create a network for the all subnet tests + cmd_output = cls.openstack( + 'network create ' + cls.NETWORK_NAME, + parse_output=True, + ) + # Get network_id for assertEqual + cls.NETWORK_ID = cmd_output["id"] @classmethod def tearDownClass(cls): diff --git a/openstackclient/tests/functional/object/v1/common.py b/openstackclient/tests/functional/object/v1/common.py index 036731da50..f3cc9aea93 100644 --- a/openstackclient/tests/functional/object/v1/common.py +++ b/openstackclient/tests/functional/object/v1/common.py @@ -10,12 +10,16 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar + from openstackclient.tests.functional import base class ObjectStoreTests(base.TestCase): """Functional tests for Object Store commands""" + haz_object_store: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/volume/v2/common.py b/openstackclient/tests/functional/volume/v2/common.py index f15d4d961f..ed55030d27 100644 --- a/openstackclient/tests/functional/volume/v2/common.py +++ b/openstackclient/tests/functional/volume/v2/common.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar + import fixtures from openstackclient.tests.functional.volume import base @@ -18,6 +20,8 @@ class BaseVolumeTests(base.BaseVolumeTests): """Base class for Volume functional tests.""" + haz_volume_v2: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/volume/v2/test_volume.py b/openstackclient/tests/functional/volume/v2/test_volume.py index 4667858751..1940fac9fd 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume.py +++ b/openstackclient/tests/functional/volume/v2/test_volume.py @@ -124,7 +124,7 @@ def test_volume_set_and_unset(self): cmd_output["properties"], ) self.assertEqual( - 'false', + False, cmd_output["bootable"], ) self.wait_for_status("volume", name, "available") @@ -171,7 +171,7 @@ def test_volume_set_and_unset(self): cmd_output["volume_image_metadata"], ) self.assertEqual( - 'true', + True, cmd_output["bootable"], ) @@ -241,33 +241,3 @@ def test_volume_snapshot(self): # Deleting snapshot may take time. If volume snapshot still exists when # a parent volume delete is requested, the volume deletion will fail. self.wait_for_delete('volume snapshot', snapshot_name) - - def test_volume_list_backward_compatibility(self): - """Test backward compatibility of list command""" - name1 = uuid.uuid4().hex - cmd_output = self.openstack( - 'volume create ' + '--size 1 ' + name1, - parse_output=True, - ) - self.addCleanup(self.openstack, 'volume delete ' + name1) - self.assertEqual( - 1, - cmd_output["size"], - ) - self.wait_for_status("volume", name1, "available") - - # Test list -c "Display Name" - cmd_output = self.openstack( - 'volume list ' + '-c "Display Name"', - parse_output=True, - ) - for each_volume in cmd_output: - self.assertIn('Display Name', each_volume) - - # Test list -c "Name" - cmd_output = self.openstack( - 'volume list ' + '-c "Name"', - parse_output=True, - ) - for each_volume in cmd_output: - self.assertIn('Name', each_volume) diff --git a/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py b/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py index e5daded1bd..a5302033b6 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py +++ b/openstackclient/tests/functional/volume/v2/test_volume_snapshot.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional.volume.v2 import common @@ -18,24 +19,27 @@ class VolumeSnapshotTests(common.BaseVolumeTests): """Functional tests for volume snapshot.""" - VOLLY = uuid.uuid4().hex + VOLUME_NAME = uuid.uuid4().hex + VOLUME_ID: ClassVar[str] @classmethod def setUpClass(cls): super().setUpClass() # create a volume for all tests to create snapshot cmd_output = cls.openstack( - 'volume create ' + '--size 1 ' + cls.VOLLY, + 'volume create ' + '--size 1 ' + cls.VOLUME_NAME, parse_output=True, ) - cls.wait_for_status('volume', cls.VOLLY, 'available') + cls.wait_for_status('volume', cls.VOLUME_NAME, 'available') cls.VOLUME_ID = cmd_output['id'] @classmethod def tearDownClass(cls): try: - cls.wait_for_status('volume', cls.VOLLY, 'available') - raw_output = cls.openstack('volume delete --force ' + cls.VOLLY) + cls.wait_for_status('volume', cls.VOLUME_NAME, 'available') + raw_output = cls.openstack( + 'volume delete --force ' + cls.VOLUME_NAME + ) cls.assertOutput('', raw_output) finally: super().tearDownClass() @@ -44,7 +48,10 @@ def test_volume_snapshot_delete(self): """Test create, delete multiple""" name1 = uuid.uuid4().hex cmd_output = self.openstack( - 'volume snapshot create ' + name1 + ' --volume ' + self.VOLLY, + 'volume snapshot create ' + + name1 + + ' --volume ' + + self.VOLUME_NAME, parse_output=True, ) self.assertEqual( @@ -54,7 +61,10 @@ def test_volume_snapshot_delete(self): name2 = uuid.uuid4().hex cmd_output = self.openstack( - 'volume snapshot create ' + name2 + ' --volume ' + self.VOLLY, + 'volume snapshot create ' + + name2 + + ' --volume ' + + self.VOLUME_NAME, parse_output=True, ) self.assertEqual( @@ -76,7 +86,10 @@ def test_volume_snapshot_list(self): """Test create, list filter""" name1 = uuid.uuid4().hex cmd_output = self.openstack( - 'volume snapshot create ' + name1 + ' --volume ' + self.VOLLY, + 'volume snapshot create ' + + name1 + + ' --volume ' + + self.VOLUME_NAME, parse_output=True, ) self.addCleanup(self.wait_for_delete, 'volume snapshot', name1) @@ -97,7 +110,10 @@ def test_volume_snapshot_list(self): name2 = uuid.uuid4().hex cmd_output = self.openstack( - 'volume snapshot create ' + name2 + ' --volume ' + self.VOLLY, + 'volume snapshot create ' + + name2 + + ' --volume ' + + self.VOLUME_NAME, parse_output=True, ) self.addCleanup(self.wait_for_delete, 'volume snapshot', name2) @@ -146,7 +162,7 @@ def test_volume_snapshot_list(self): # Test list --volume cmd_output = self.openstack( - 'volume snapshot list ' + '--volume ' + self.VOLLY, + 'volume snapshot list ' + '--volume ' + self.VOLUME_NAME, parse_output=True, ) names = [x["Name"] for x in cmd_output] @@ -169,7 +185,7 @@ def test_volume_snapshot_set(self): cmd_output = self.openstack( 'volume snapshot create ' + '--volume ' - + self.VOLLY + + self.VOLUME_NAME + ' --description aaaa ' + '--property Alpha=a ' + name, diff --git a/openstackclient/tests/functional/volume/v2/test_volume_type.py b/openstackclient/tests/functional/volume/v2/test_volume_type.py index 80bf85a5bb..c80e6c154e 100644 --- a/openstackclient/tests/functional/volume/v2/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v2/test_volume_type.py @@ -173,9 +173,9 @@ def test_encryption_type(self): 'volume type list --encryption-type', parse_output=True, ) - encryption_output = [ + encryption_output = next( t['Encryption'] for t in cmd_output if t['Name'] == encryption_type - ][0] + ) expected = { 'provider': 'LuksEncryptor', 'cipher': 'aes-xts-plain64', diff --git a/openstackclient/tests/functional/volume/v3/common.py b/openstackclient/tests/functional/volume/v3/common.py index cbab39275c..038991313a 100644 --- a/openstackclient/tests/functional/volume/v3/common.py +++ b/openstackclient/tests/functional/volume/v3/common.py @@ -10,6 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar + import fixtures from openstackclient.tests.functional.volume import base @@ -18,6 +20,8 @@ class BaseVolumeTests(base.BaseVolumeTests): """Base class for Volume functional tests.""" + haz_volume_v3: ClassVar[bool] + @classmethod def setUpClass(cls): super().setUpClass() diff --git a/openstackclient/tests/functional/volume/v3/test_volume.py b/openstackclient/tests/functional/volume/v3/test_volume.py index 244fe91c18..2380c947d2 100644 --- a/openstackclient/tests/functional/volume/v3/test_volume.py +++ b/openstackclient/tests/functional/volume/v3/test_volume.py @@ -124,7 +124,7 @@ def test_volume_set_and_unset(self): cmd_output["properties"], ) self.assertEqual( - 'false', + False, cmd_output["bootable"], ) self.wait_for_status("volume", name, "available") @@ -172,7 +172,7 @@ def test_volume_set_and_unset(self): cmd_output["volume_image_metadata"], ) self.assertEqual( - 'true', + True, cmd_output["bootable"], ) @@ -242,33 +242,3 @@ def test_volume_snapshot(self): # Deleting snapshot may take time. If volume snapshot still exists when # a parent volume delete is requested, the volume deletion will fail. self.wait_for_delete('volume snapshot', snapshot_name) - - def test_volume_list_backward_compatibility(self): - """Test backward compatibility of list command""" - name1 = uuid.uuid4().hex - cmd_output = self.openstack( - 'volume create --size 1 ' + name1, - parse_output=True, - ) - self.addCleanup(self.openstack, 'volume delete ' + name1) - self.assertEqual( - 1, - cmd_output["size"], - ) - self.wait_for_status("volume", name1, "available") - - # Test list -c "Display Name" - cmd_output = self.openstack( - 'volume list -c "Display Name"', - parse_output=True, - ) - for each_volume in cmd_output: - self.assertIn('Display Name', each_volume) - - # Test list -c "Name" - cmd_output = self.openstack( - 'volume list -c "Name"', - parse_output=True, - ) - for each_volume in cmd_output: - self.assertIn('Name', each_volume) diff --git a/openstackclient/tests/functional/volume/v3/test_volume_group.py b/openstackclient/tests/functional/volume/v3/test_volume_group.py new file mode 100644 index 0000000000..70aaed6ad4 --- /dev/null +++ b/openstackclient/tests/functional/volume/v3/test_volume_group.py @@ -0,0 +1,163 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from typing import ClassVar +import uuid + +from openstackclient.tests.functional.volume.v3 import common + + +class VolumeGroupTests(common.BaseVolumeTests): + """Functional tests for volume group.""" + + API_VERSION = '3.13' + GROUP_TYPE_NAME = uuid.uuid4().hex + GROUP_TYPE_ID: ClassVar[str] + VOLUME_TYPE_ID: ClassVar[str] + + @classmethod + def setUpClass(cls): + super().setUpClass() + cmd_output = cls.openstack( + '--os-volume-api-version ' + + cls.API_VERSION + + ' volume group type create ' + + cls.GROUP_TYPE_NAME, + parse_output=True, + ) + cls.GROUP_TYPE_ID = cmd_output['ID'] + + volume_type_name = uuid.uuid4().hex + cmd_output = cls.openstack( + 'volume type create ' + volume_type_name, + parse_output=True, + ) + cls.VOLUME_TYPE_ID = cmd_output['id'] + + @classmethod + def tearDownClass(cls): + try: + raw_output = cls.openstack( + 'volume type delete ' + cls.VOLUME_TYPE_ID + ) + cls.assertOutput('', raw_output) + raw_output = cls.openstack( + '--os-volume-api-version ' + + cls.API_VERSION + + ' volume group type delete ' + + cls.GROUP_TYPE_NAME + ) + cls.assertOutput('', raw_output) + finally: + super().tearDownClass() + + def test_volume_group(self): + # create volume group + name = uuid.uuid4().hex + description = 'description-' + uuid.uuid4().hex + cmd_output = self.openstack( + '--os-volume-api-version ' + + self.API_VERSION + + ' volume group create ' + + '--volume-group-type ' + + self.GROUP_TYPE_NAME + + ' --volume-type ' + + self.VOLUME_TYPE_ID + + ' --name ' + + name + + ' --description ' + + description, + parse_output=True, + ) + group_id = cmd_output['ID'] + self.addCleanup( + self.wait_for_delete, + '--os-volume-api-version ' + self.API_VERSION + ' volume group', + group_id, + name_field='ID', + ) + self.addCleanup( + self.openstack, + '--os-volume-api-version ' + + self.API_VERSION + + ' volume group delete ' + + group_id, + fail_ok=True, + ) + self.assertIsNotNone(group_id) + self.assertEqual(name, cmd_output['Name']) + self.assertEqual(description, cmd_output['Description']) + self.assertEqual(self.GROUP_TYPE_ID, cmd_output['Group Type']) + self.assertIn(self.VOLUME_TYPE_ID, cmd_output['Volume Types']) + + # show volume group + cmd_output = self.openstack( + '--os-volume-api-version ' + + self.API_VERSION + + ' volume group show ' + + group_id, + parse_output=True, + ) + self.assertEqual(group_id, cmd_output['ID']) + self.assertEqual(name, cmd_output['Name']) + self.assertEqual(description, cmd_output['Description']) + + # list volume group + cmd_output = self.openstack( + '--os-volume-api-version ' + + self.API_VERSION + + ' volume group list', + parse_output=True, + ) + self.assertIn(group_id, [group['ID'] for group in cmd_output]) + self.assertIn(name, [group['Name'] for group in cmd_output]) + + # set volume group + new_name = uuid.uuid4().hex + new_description = 'description-' + uuid.uuid4().hex + self.openstack( + '--os-volume-api-version ' + + self.API_VERSION + + ' volume group set ' + + '--name ' + + new_name + + ' --description ' + + new_description + + ' ' + + group_id, + ) + + # show updated volume group + cmd_output = self.openstack( + '--os-volume-api-version ' + + self.API_VERSION + + ' volume group show ' + + group_id, + parse_output=True, + ) + self.assertEqual(group_id, cmd_output['ID']) + self.assertEqual(new_name, cmd_output['Name']) + self.assertEqual(new_description, cmd_output['Description']) + + # delete volume group + raw_output = self.openstack( + '--os-volume-api-version ' + + self.API_VERSION + + ' volume group delete ' + + group_id, + ) + self.assertOutput('', raw_output) + self.wait_for_delete( + '--os-volume-api-version ' + self.API_VERSION + ' volume group', + group_id, + name_field='ID', + ) diff --git a/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py b/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py index 2c9b72ba14..6921c997dd 100644 --- a/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py +++ b/openstackclient/tests/functional/volume/v3/test_volume_snapshot.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from typing import ClassVar import uuid from openstackclient.tests.functional.volume.v3 import common @@ -18,169 +19,82 @@ class VolumeSnapshotTests(common.BaseVolumeTests): """Functional tests for volume snapshot.""" - VOLLY = uuid.uuid4().hex + VOLUME_NAME = uuid.uuid4().hex + VOLUME_ID: ClassVar[str] @classmethod def setUpClass(cls): super().setUpClass() - # create a volume for all tests to create snapshot + # create a test volume used by all snapshot tests cmd_output = cls.openstack( - 'volume create ' + '--size 1 ' + cls.VOLLY, + 'volume create ' + '--size 1 ' + cls.VOLUME_NAME, parse_output=True, ) - cls.wait_for_status('volume', cls.VOLLY, 'available') + cls.wait_for_status('volume', cls.VOLUME_NAME, 'available') cls.VOLUME_ID = cmd_output['id'] @classmethod def tearDownClass(cls): try: - cls.wait_for_status('volume', cls.VOLLY, 'available') - raw_output = cls.openstack('volume delete --force ' + cls.VOLLY) + cls.wait_for_status('volume', cls.VOLUME_NAME, 'available') + raw_output = cls.openstack( + 'volume delete --force ' + cls.VOLUME_NAME + ) cls.assertOutput('', raw_output) finally: super().tearDownClass() - def test_volume_snapshot_delete(self): - """Test create, delete multiple""" - name1 = uuid.uuid4().hex - cmd_output = self.openstack( - 'volume snapshot create ' + name1 + ' --volume ' + self.VOLLY, - parse_output=True, - ) - self.assertEqual( - name1, - cmd_output["name"], - ) + def test_volume_snapshot(self): + # create volume snapshot + name = uuid.uuid4().hex - name2 = uuid.uuid4().hex cmd_output = self.openstack( - 'volume snapshot create ' + name2 + ' --volume ' + self.VOLLY, + 'volume snapshot create ' + + '--volume ' + + self.VOLUME_NAME + + ' --description aaaa ' + + '--property Alpha=a ' + + name, parse_output=True, ) - self.assertEqual( - name2, - cmd_output["name"], - ) + snap_id = cmd_output['id'] - self.wait_for_status('volume snapshot', name1, 'available') - self.wait_for_status('volume snapshot', name2, 'available') - - del_output = self.openstack( - 'volume snapshot delete ' + name1 + ' ' + name2 + self.addCleanup(self.wait_for_delete, 'volume snapshot', snap_id) + # delete volume snapshot + self.addCleanup( + self.openstack, + 'volume snapshot delete ' + snap_id, ) - self.assertOutput('', del_output) - self.wait_for_delete('volume snapshot', name1) - self.wait_for_delete('volume snapshot', name2) + self.wait_for_status('volume snapshot', snap_id, 'available') - def test_volume_snapshot_list(self): - """Test create, list filter""" - name1 = uuid.uuid4().hex - cmd_output = self.openstack( - 'volume snapshot create ' + name1 + ' --volume ' + self.VOLLY, + # show volume snapshot + snapshot_info = self.openstack( + 'volume snapshot show ' + name, parse_output=True, ) - self.addCleanup(self.wait_for_delete, 'volume snapshot', name1) - self.addCleanup(self.openstack, 'volume snapshot delete ' + name1) - self.assertEqual( - name1, - cmd_output["name"], - ) - self.assertEqual( - self.VOLUME_ID, - cmd_output["volume_id"], - ) - self.assertEqual( - 1, - cmd_output["size"], - ) - self.wait_for_status('volume snapshot', name1, 'available') - name2 = uuid.uuid4().hex - cmd_output = self.openstack( - 'volume snapshot create ' + name2 + ' --volume ' + self.VOLLY, - parse_output=True, - ) - self.addCleanup(self.wait_for_delete, 'volume snapshot', name2) - self.addCleanup(self.openstack, 'volume snapshot delete ' + name2) - self.assertEqual( - name2, - cmd_output["name"], - ) - self.assertEqual( - self.VOLUME_ID, - cmd_output["volume_id"], - ) - self.assertEqual( - 1, - cmd_output["size"], - ) - self.wait_for_status('volume snapshot', name2, 'available') - raw_output = self.openstack( - 'volume snapshot set ' + '--state error ' + name2 - ) - self.assertOutput('', raw_output) + self.assertEqual(name, snapshot_info['name']) + self.assertEqual('aaaa', snapshot_info["description"]) + self.assertEqual({'Alpha': 'a'}, snapshot_info["properties"]) - # Test list --long, --status + # list volume snapshot --name cmd_output = self.openstack( - 'volume snapshot list ' + '--long ' + '--status error', + 'volume snapshot list --name ' + name, parse_output=True, ) - names = [x["Name"] for x in cmd_output] - self.assertNotIn(name1, names) - self.assertIn(name2, names) + names = [x['Name'] for x in cmd_output] + self.assertIn(name, names) - # Test list --volume + # list volume snapshot --volume cmd_output = self.openstack( - 'volume snapshot list ' + '--volume ' + self.VOLLY, + 'volume snapshot list ' + '--volume ' + self.VOLUME_NAME, parse_output=True, ) names = [x["Name"] for x in cmd_output] - self.assertIn(name1, names) - self.assertIn(name2, names) + self.assertIn(name, names) - # Test list --name - cmd_output = self.openstack( - 'volume snapshot list ' + '--name ' + name1, - parse_output=True, - ) - names = [x["Name"] for x in cmd_output] - self.assertIn(name1, names) - self.assertNotIn(name2, names) - - def test_volume_snapshot_set(self): - """Test create, set, unset, show, delete volume snapshot""" - name = uuid.uuid4().hex + # set volume snapshot new_name = name + "_" - cmd_output = self.openstack( - 'volume snapshot create ' - + '--volume ' - + self.VOLLY - + ' --description aaaa ' - + '--property Alpha=a ' - + name, - parse_output=True, - ) - self.addCleanup(self.wait_for_delete, 'volume snapshot', new_name) - self.addCleanup(self.openstack, 'volume snapshot delete ' + new_name) - self.assertEqual( - name, - cmd_output["name"], - ) - self.assertEqual( - 1, - cmd_output["size"], - ) - self.assertEqual( - 'aaaa', - cmd_output["description"], - ) - self.assertEqual( - {'Alpha': 'a'}, - cmd_output["properties"], - ) - self.wait_for_status('volume snapshot', name, 'available') - - # Test volume snapshot set raw_output = self.openstack( 'volume snapshot set ' + '--name ' @@ -188,11 +102,10 @@ def test_volume_snapshot_set(self): + ' --description bbbb ' + '--property Alpha=c ' + '--property Beta=b ' - + name, + + snap_id, ) self.assertOutput('', raw_output) - # Show snapshot set result cmd_output = self.openstack( 'volume snapshot show ' + new_name, parse_output=True, @@ -201,10 +114,6 @@ def test_volume_snapshot_set(self): new_name, cmd_output["name"], ) - self.assertEqual( - 1, - cmd_output["size"], - ) self.assertEqual( 'bbbb', cmd_output["description"], @@ -214,7 +123,7 @@ def test_volume_snapshot_set(self): cmd_output["properties"], ) - # Test volume snapshot unset + # unset volume snapshot raw_output = self.openstack( 'volume snapshot unset ' + '--property Alpha ' + new_name, ) @@ -229,16 +138,25 @@ def test_volume_snapshot_set(self): cmd_output["properties"], ) - # Test volume snapshot set --no-property + # set volume snapshot --no-property, --state error raw_output = self.openstack( - 'volume snapshot set ' + '--no-property ' + new_name, + 'volume snapshot set ' + + '--no-property ' + + '--state error ' + + new_name, ) self.assertOutput('', raw_output) + cmd_output = self.openstack( 'volume snapshot show ' + new_name, parse_output=True, ) - self.assertNotIn( - {'Beta': 'b'}, - cmd_output["properties"], + self.assertEqual({}, cmd_output["properties"]) + + # list volume snapshot --long --status + cmd_output = self.openstack( + 'volume snapshot list ' + '--long ' + '--status error', + parse_output=True, ) + names = [x["Name"] for x in cmd_output] + self.assertIn(new_name, names) diff --git a/openstackclient/tests/functional/volume/v3/test_volume_type.py b/openstackclient/tests/functional/volume/v3/test_volume_type.py index 421b3224f6..6862d73305 100644 --- a/openstackclient/tests/functional/volume/v3/test_volume_type.py +++ b/openstackclient/tests/functional/volume/v3/test_volume_type.py @@ -138,6 +138,7 @@ def test_multi_delete(self): def test_encryption_type(self): name = uuid.uuid4().hex encryption_type = uuid.uuid4().hex + # test create new encryption type cmd_output = self.openstack( 'volume type create ' @@ -155,6 +156,7 @@ def test_encryption_type(self): } for attr, value in expected.items(): self.assertEqual(value, cmd_output['encryption'][attr]) + # test show encryption type cmd_output = self.openstack( 'volume type show --encryption-type ' + encryption_type, @@ -168,14 +170,15 @@ def test_encryption_type(self): } for attr, value in expected.items(): self.assertEqual(value, cmd_output['encryption'][attr]) + # test list encryption type cmd_output = self.openstack( 'volume type list --encryption-type', parse_output=True, ) - encryption_output = [ + encryption_output = next( t['Encryption'] for t in cmd_output if t['Name'] == encryption_type - ][0] + ) expected = { 'provider': 'LuksEncryptor', 'cipher': 'aes-xts-plain64', @@ -184,6 +187,7 @@ def test_encryption_type(self): } for attr, value in expected.items(): self.assertEqual(value, encryption_output[attr]) + # test set existing encryption type raw_output = self.openstack( 'volume type set ' @@ -203,6 +207,7 @@ def test_encryption_type(self): } for attr, value in expected.items(): self.assertEqual(value, cmd_output['encryption'][attr]) + # test set new encryption type cmd_output = self.openstack( 'volume type create --private ' + name, @@ -235,6 +240,7 @@ def test_encryption_type(self): } for attr, value in expected.items(): self.assertEqual(value, cmd_output['encryption'][attr]) + # test unset encryption type raw_output = self.openstack( 'volume type unset --encryption-type ' + name @@ -245,6 +251,7 @@ def test_encryption_type(self): parse_output=True, ) self.assertEqual({}, cmd_output['encryption']) + # test delete encryption type raw_output = self.openstack('volume type delete ' + encryption_type) self.assertEqual('', raw_output) diff --git a/openstackclient/tests/unit/api/test_compute_v2.py b/openstackclient/tests/unit/api/test_compute_v2.py index a609025b22..d72856294a 100644 --- a/openstackclient/tests/unit/api/test_compute_v2.py +++ b/openstackclient/tests/unit/api/test_compute_v2.py @@ -17,7 +17,7 @@ from unittest import mock import uuid -from openstack.compute.v2 import _proxy +from openstack.compute import v2 as compute_v2 from osc_lib import exceptions as osc_lib_exceptions from openstackclient.api import compute_v2 as compute @@ -29,7 +29,10 @@ class TestSecurityGroup(utils.TestCase): def setUp(self): super().setUp() - self.compute_client = mock.Mock(_proxy.Proxy) + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.compute_client = mock.Mock(spec=compute_v2.Proxy) def test_create_security_group(self): sg_name = 'name-' + uuid.uuid4().hex @@ -223,7 +226,7 @@ class TestSecurityGroupRule(utils.TestCase): def setUp(self): super().setUp() - self.compute_client = mock.Mock(_proxy.Proxy) + self.compute_client = mock.Mock(spec=compute_v2.Proxy) def test_create_security_group_rule(self): sg_id = uuid.uuid4().hex @@ -281,7 +284,7 @@ class TestNetwork(utils.TestCase): def setUp(self): super().setUp() - self.compute_client = mock.Mock(_proxy.Proxy) + self.compute_client = mock.Mock(spec=compute_v2.Proxy) def test_create_network(self): net_name = 'name-' + uuid.uuid4().hex @@ -443,7 +446,7 @@ class TestFloatingIP(utils.TestCase): def setUp(self): super().setUp() - self.compute_client = mock.Mock(_proxy.Proxy) + self.compute_client = mock.Mock(spec=compute_v2.Proxy) def test_create_floating_ip(self): network = 'network-' + uuid.uuid4().hex @@ -528,7 +531,7 @@ class TestFloatingIPPool(utils.TestCase): def setUp(self): super().setUp() - self.compute_client = mock.Mock(_proxy.Proxy) + self.compute_client = mock.Mock(spec=compute_v2.Proxy) def test_list_floating_ip_pools(self): data = { diff --git a/openstackclient/tests/unit/api/test_image_v1.py b/openstackclient/tests/unit/api/test_image_v1.py deleted file mode 100644 index 4d0ac53ea9..0000000000 --- a/openstackclient/tests/unit/api/test_image_v1.py +++ /dev/null @@ -1,96 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -"""Image v1 API Library Tests""" - -from keystoneauth1 import session -from requests_mock.contrib import fixture - -from openstackclient.api import image_v1 -from openstackclient.tests.unit import utils - - -FAKE_PROJECT = 'xyzpdq' -FAKE_URL = 'http://gopher.dev10.com' - - -class TestImageAPIv1(utils.TestCase): - def setUp(self): - super().setUp() - - sess = session.Session() - self.api = image_v1.APIv1(session=sess, endpoint=FAKE_URL) - self.requests_mock = self.useFixture(fixture.Fixture()) - - -class TestImage(TestImageAPIv1): - PUB_PROT = { - 'id': '1', - 'name': 'pub1', - 'is_public': True, - 'protected': True, - } - PUB_NOPROT = { - 'id': '2', - 'name': 'pub2-noprot', - 'is_public': True, - 'protected': False, - } - NOPUB_PROT = { - 'id': '3', - 'name': 'priv3', - 'is_public': False, - 'protected': True, - } - NOPUB_NOPROT = { - 'id': '4', - 'name': 'priv4-noprot', - 'is_public': False, - 'protected': False, - } - LIST_IMAGE_RESP = [ - PUB_PROT, - PUB_NOPROT, - NOPUB_PROT, - NOPUB_NOPROT, - ] - - def test_image_list_no_options(self): - self.requests_mock.register_uri( - 'GET', - FAKE_URL + '/v1/images', - json={'images': self.LIST_IMAGE_RESP}, - status_code=200, - ) - ret = self.api.image_list() - self.assertEqual(self.LIST_IMAGE_RESP, ret) - - def test_image_list_public(self): - self.requests_mock.register_uri( - 'GET', - FAKE_URL + '/v1/images/detail', - json={'images': self.LIST_IMAGE_RESP}, - status_code=200, - ) - ret = self.api.image_list(public=True) - self.assertEqual([self.PUB_PROT, self.PUB_NOPROT], ret) - - def test_image_list_private(self): - self.requests_mock.register_uri( - 'GET', - FAKE_URL + '/v1/images/detail', - json={'images': self.LIST_IMAGE_RESP}, - status_code=200, - ) - ret = self.api.image_list(private=True) - self.assertEqual([self.NOPUB_PROT, self.NOPUB_NOPROT], ret) diff --git a/openstackclient/tests/unit/api/test_image_v2.py b/openstackclient/tests/unit/api/test_image_v2.py deleted file mode 100644 index a4cb606d87..0000000000 --- a/openstackclient/tests/unit/api/test_image_v2.py +++ /dev/null @@ -1,96 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -"""Image v2 API Library Tests""" - -from keystoneauth1 import session -from requests_mock.contrib import fixture - -from openstackclient.api import image_v2 -from openstackclient.tests.unit import utils - - -FAKE_PROJECT = 'xyzpdq' -FAKE_URL = 'http://gopher.dev20.com' - - -class TestImageAPIv2(utils.TestCase): - def setUp(self): - super().setUp() - - sess = session.Session() - self.api = image_v2.APIv2(session=sess, endpoint=FAKE_URL) - self.requests_mock = self.useFixture(fixture.Fixture()) - - -class TestImage(TestImageAPIv2): - PUB_PROT = { - 'id': '1', - 'name': 'pub1', - 'visibility': 'public', - 'protected': True, - } - PUB_NOPROT = { - 'id': '2', - 'name': 'pub2-noprot', - 'visibility': 'public', - 'protected': False, - } - NOPUB_PROT = { - 'id': '3', - 'name': 'priv3', - 'visibility': 'private', - 'protected': True, - } - NOPUB_NOPROT = { - 'id': '4', - 'name': 'priv4-noprot', - 'visibility': 'private', - 'protected': False, - } - LIST_IMAGE_RESP = [ - PUB_PROT, - PUB_NOPROT, - NOPUB_PROT, - NOPUB_NOPROT, - ] - - def test_image_list_no_options(self): - self.requests_mock.register_uri( - 'GET', - FAKE_URL + '/v2/images', - json={'images': self.LIST_IMAGE_RESP}, - status_code=200, - ) - ret = self.api.image_list() - self.assertEqual(self.LIST_IMAGE_RESP, ret) - - def test_image_list_public(self): - self.requests_mock.register_uri( - 'GET', - FAKE_URL + '/v2/images', - json={'images': [self.PUB_PROT, self.PUB_NOPROT]}, - status_code=200, - ) - ret = self.api.image_list(public=True) - self.assertEqual([self.PUB_PROT, self.PUB_NOPROT], ret) - - def test_image_list_private(self): - self.requests_mock.register_uri( - 'GET', - FAKE_URL + '/v2/images', - json={'images': [self.NOPUB_PROT, self.NOPUB_NOPROT]}, - status_code=200, - ) - ret = self.api.image_list(public=True) - self.assertEqual([self.NOPUB_PROT, self.NOPUB_NOPROT], ret) diff --git a/openstackclient/tests/unit/api/test_volume_v2.py b/openstackclient/tests/unit/api/test_volume_v2.py new file mode 100644 index 0000000000..34fe1bbb42 --- /dev/null +++ b/openstackclient/tests/unit/api/test_volume_v2.py @@ -0,0 +1,127 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Volume v2 API Library Tests""" + +import http +from unittest import mock +import uuid + +from openstack.block_storage import v2 as block_storage_v2 +from osc_lib import exceptions as osc_lib_exceptions + +from openstackclient.api import volume_v2 as volume +from openstackclient.tests.unit import fakes +from openstackclient.tests.unit import utils + + +class TestConsistencyGroup(utils.TestCase): + def setUp(self): + super().setUp() + + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.volume_client = mock.Mock(spec=block_storage_v2.Proxy) + + def test_find_consistency_group_by_id(self): + cg_id = uuid.uuid4().hex + cg_name = 'name-' + uuid.uuid4().hex + data = { + 'consistencygroup': { + 'id': cg_id, + 'name': cg_name, + 'status': 'available', + 'availability_zone': 'az1', + 'created_at': '2015-09-16T09:28:52.000000', + 'description': 'description-' + uuid.uuid4().hex, + 'volume_types': ['123456'], + } + } + self.volume_client.get.side_effect = [ + fakes.FakeResponse(data=data), + ] + + result = volume.find_consistency_group(self.volume_client, cg_id) + + self.volume_client.get.assert_has_calls( + [ + mock.call(f'/consistencygroups/{cg_id}'), + ] + ) + self.assertEqual(data['consistencygroup'], result) + + def test_find_consistency_group_by_name(self): + cg_id = uuid.uuid4().hex + cg_name = 'name-' + uuid.uuid4().hex + data = { + 'consistencygroups': [ + { + 'id': cg_id, + 'name': cg_name, + } + ], + } + self.volume_client.get.side_effect = [ + fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND), + fakes.FakeResponse(data=data), + ] + + result = volume.find_consistency_group(self.volume_client, cg_name) + + self.volume_client.get.assert_has_calls( + [ + mock.call(f'/consistencygroups/{cg_name}'), + mock.call('/consistencygroups'), + ] + ) + self.assertEqual(data['consistencygroups'][0], result) + + def test_find_consistency_group_not_found(self): + data = {'consistencygroups': []} + self.volume_client.get.side_effect = [ + fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND), + fakes.FakeResponse(data=data), + ] + self.assertRaises( + osc_lib_exceptions.NotFound, + volume.find_consistency_group, + self.volume_client, + 'invalid-cg', + ) + + def test_find_consistency_group_by_name_duplicate(self): + cg_name = 'name-' + uuid.uuid4().hex + data = { + 'consistencygroups': [ + { + 'id': uuid.uuid4().hex, + 'name': cg_name, + }, + { + 'id': uuid.uuid4().hex, + 'name': cg_name, + }, + ], + } + self.volume_client.get.side_effect = [ + fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND), + fakes.FakeResponse(data=data), + ] + + self.assertRaises( + osc_lib_exceptions.NotFound, + volume.find_consistency_group, + self.volume_client, + cg_name, + ) diff --git a/openstackclient/tests/unit/api/test_volume_v3.py b/openstackclient/tests/unit/api/test_volume_v3.py new file mode 100644 index 0000000000..5743d938bf --- /dev/null +++ b/openstackclient/tests/unit/api/test_volume_v3.py @@ -0,0 +1,127 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Volume v3 API Library Tests""" + +import http +from unittest import mock +import uuid + +from openstack.block_storage import v3 as block_storage_v3 +from osc_lib import exceptions as osc_lib_exceptions + +from openstackclient.api import volume_v3 as volume +from openstackclient.tests.unit import fakes +from openstackclient.tests.unit import utils + + +class TestConsistencyGroup(utils.TestCase): + def setUp(self): + super().setUp() + + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.volume_client = mock.Mock(spec=block_storage_v3.Proxy) + + def test_find_consistency_group_by_id(self): + cg_id = uuid.uuid4().hex + cg_name = 'name-' + uuid.uuid4().hex + data = { + 'consistencygroup': { + 'id': cg_id, + 'name': cg_name, + 'status': 'available', + 'availability_zone': 'az1', + 'created_at': '2015-09-16T09:28:52.000000', + 'description': 'description-' + uuid.uuid4().hex, + 'volume_types': ['123456'], + } + } + self.volume_client.get.side_effect = [ + fakes.FakeResponse(data=data), + ] + + result = volume.find_consistency_group(self.volume_client, cg_id) + + self.volume_client.get.assert_has_calls( + [ + mock.call(f'/consistencygroups/{cg_id}'), + ] + ) + self.assertEqual(data['consistencygroup'], result) + + def test_find_consistency_group_by_name(self): + cg_id = uuid.uuid4().hex + cg_name = 'name-' + uuid.uuid4().hex + data = { + 'consistencygroups': [ + { + 'id': cg_id, + 'name': cg_name, + } + ], + } + self.volume_client.get.side_effect = [ + fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND), + fakes.FakeResponse(data=data), + ] + + result = volume.find_consistency_group(self.volume_client, cg_name) + + self.volume_client.get.assert_has_calls( + [ + mock.call(f'/consistencygroups/{cg_name}'), + mock.call('/consistencygroups'), + ] + ) + self.assertEqual(data['consistencygroups'][0], result) + + def test_find_consistency_group_not_found(self): + data = {'consistencygroups': []} + self.volume_client.get.side_effect = [ + fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND), + fakes.FakeResponse(data=data), + ] + self.assertRaises( + osc_lib_exceptions.NotFound, + volume.find_consistency_group, + self.volume_client, + 'invalid-cg', + ) + + def test_find_consistency_group_by_name_duplicate(self): + cg_name = 'name-' + uuid.uuid4().hex + data = { + 'consistencygroups': [ + { + 'id': uuid.uuid4().hex, + 'name': cg_name, + }, + { + 'id': uuid.uuid4().hex, + 'name': cg_name, + }, + ], + } + self.volume_client.get.side_effect = [ + fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND), + fakes.FakeResponse(data=data), + ] + + self.assertRaises( + osc_lib_exceptions.NotFound, + volume.find_consistency_group, + self.volume_client, + cg_name, + ) diff --git a/openstackclient/tests/unit/common/test_availability_zone.py b/openstackclient/tests/unit/common/test_availability_zone.py index d1383b5409..6cf5463fe5 100644 --- a/openstackclient/tests/unit/common/test_availability_zone.py +++ b/openstackclient/tests/unit/common/test_availability_zone.py @@ -10,20 +10,30 @@ # License for the specific language governing permissions and limitations # under the License. +import uuid + +from openstack.block_storage.v3 import availability_zone as _volume_az +from openstack.shared_file_system.v2 import availability_zone as _share_az +from openstack.test import fakes + from openstackclient.common import availability_zone from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit.share.v2 import fakes as share_fakes from openstackclient.tests.unit import utils from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +def _create_fake_volume_az(): + return _volume_az.AvailabilityZone( + name=uuid.uuid4().hex, state={'available': True} + ) + + def _build_compute_az_datalist(compute_az, long_datalist=False): datalist = () if not long_datalist: - datalist = ( - compute_az.name, - 'available', - ) + datalist = (compute_az.name, 'available') else: for host, services in compute_az.hosts.items(): for service, state in services.items(): @@ -38,18 +48,15 @@ def _build_compute_az_datalist(compute_az, long_datalist=False): return (datalist,) -def _build_volume_az_datalist(volume_az, long_datalist=False): +def _build_network_az_datalist(network_az, long_datalist=False): datalist = () if not long_datalist: - datalist = ( - volume_az.name, - 'available', - ) + datalist = (network_az.name, network_az.state) else: datalist = ( - volume_az.name, - 'available', - '', + network_az.name, + network_az.state, + network_az.resource, '', '', '', @@ -57,22 +64,21 @@ def _build_volume_az_datalist(volume_az, long_datalist=False): return (datalist,) -def _build_network_az_datalist(network_az, long_datalist=False): +def _build_share_az_datalist(share_az, long_datalist=False): datalist = () if not long_datalist: - datalist = ( - network_az.name, - network_az.state, - ) + datalist = (share_az.name, 'available') else: - datalist = ( - network_az.name, - network_az.state, - network_az.resource, - '', - '', - '', - ) + datalist = (share_az.name, 'available', '', '', '', '') + return (datalist,) + + +def _build_volume_az_datalist(volume_az, long_datalist=False): + datalist = () + if not long_datalist: + datalist = (volume_az.name, 'available') + else: + datalist = (volume_az.name, 'available', '', '', '', '') return (datalist,) @@ -80,12 +86,9 @@ class TestAvailabilityZoneList( network_fakes.FakeClientMixin, volume_fakes.FakeClientMixin, compute_fakes.FakeClientMixin, + share_fakes.FakeClientMixin, utils.TestCommand, ): - compute_azs = compute_fakes.create_availability_zones() - volume_azs = volume_fakes.create_availability_zones(count=1) - network_azs = network_fakes.create_availability_zones() - short_columnslist = ('Zone Name', 'Zone Status') long_columnslist = ( 'Zone Name', @@ -99,12 +102,20 @@ class TestAvailabilityZoneList( def setUp(self): super().setUp() + self.compute_azs = compute_fakes.create_availability_zones() self.compute_client.availability_zones.return_value = self.compute_azs - self.volume_sdk_client.availability_zones.return_value = ( - self.volume_azs - ) + + self.network_azs = network_fakes.create_availability_zones() self.network_client.availability_zones.return_value = self.network_azs + self.share_azs = list( + fakes.generate_fake_resources(_share_az.AvailabilityZone) + ) + self.share_sdk_client.availability_zones.return_value = self.share_azs + + self.volume_azs = [_create_fake_volume_az()] + self.volume_client.availability_zones.return_value = self.volume_azs + # Get the command object to test self.cmd = availability_zone.ListAvailabilityZone(self.app, None) @@ -119,17 +130,44 @@ def test_availability_zone_list_no_options(self): columns, data = self.cmd.take_action(parsed_args) self.compute_client.availability_zones.assert_called_with(details=True) - self.volume_sdk_client.availability_zones.assert_called_with() self.network_client.availability_zones.assert_called_with() + self.share_sdk_client.availability_zones.assert_called_with() + self.volume_client.availability_zones.assert_called_with() self.assertEqual(self.short_columnslist, columns) datalist = () for compute_az in self.compute_azs: datalist += _build_compute_az_datalist(compute_az) + for network_az in self.network_azs: + datalist += _build_network_az_datalist(network_az) + for share_az in self.share_azs: + datalist += _build_share_az_datalist(share_az) for volume_az in self.volume_azs: datalist += _build_volume_az_datalist(volume_az) + self.assertEqual(datalist, tuple(data)) + + def test_availability_zone_list_no_volume_endpoint(self): + self.app.client_manager.volume_endpoint_enabled = False + + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute_client.availability_zones.assert_called_with(details=True) + self.network_client.availability_zones.assert_called_with() + self.share_sdk_client.availability_zones.assert_called_with() + self.volume_client.availability_zones.assert_not_called() + + self.assertEqual(self.short_columnslist, columns) + datalist = () + for compute_az in self.compute_azs: + datalist += _build_compute_az_datalist(compute_az) for network_az in self.network_azs: datalist += _build_network_az_datalist(network_az) + for share_az in self.share_azs: + datalist += _build_share_az_datalist(share_az) self.assertEqual(datalist, tuple(data)) def test_availability_zone_list_long(self): @@ -141,14 +179,12 @@ def test_availability_zone_list_long(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) self.compute_client.availability_zones.assert_called_with(details=True) - self.volume_sdk_client.availability_zones.assert_called_with() self.network_client.availability_zones.assert_called_with() + self.share_sdk_client.availability_zones.assert_called_with() + self.volume_client.availability_zones.assert_called_with() self.assertEqual(self.long_columnslist, columns) datalist = () @@ -156,14 +192,16 @@ def test_availability_zone_list_long(self): datalist += _build_compute_az_datalist( compute_az, long_datalist=True ) - for volume_az in self.volume_azs: - datalist += _build_volume_az_datalist( - volume_az, long_datalist=True - ) for network_az in self.network_azs: datalist += _build_network_az_datalist( network_az, long_datalist=True ) + for share_az in self.share_azs: + datalist += _build_share_az_datalist(share_az, long_datalist=True) + for volume_az in self.volume_azs: + datalist += _build_volume_az_datalist( + volume_az, long_datalist=True + ) self.assertEqual(datalist, tuple(data)) def test_availability_zone_list_compute(self): @@ -175,14 +213,12 @@ def test_availability_zone_list_compute(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) self.compute_client.availability_zones.assert_called_with(details=True) - self.volume_sdk_client.availability_zones.assert_not_called() self.network_client.availability_zones.assert_not_called() + self.share_sdk_client.availability_zones.assert_not_called() + self.volume_client.availability_zones.assert_not_called() self.assertEqual(self.short_columnslist, columns) datalist = () @@ -190,50 +226,68 @@ def test_availability_zone_list_compute(self): datalist += _build_compute_az_datalist(compute_az) self.assertEqual(datalist, tuple(data)) - def test_availability_zone_list_volume(self): + def test_availability_zone_list_network(self): arglist = [ - '--volume', + '--network', ] verifylist = [ - ('volume', True), + ('network', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.compute_client.availability_zones.assert_not_called() + self.network_client.availability_zones.assert_called_with() + self.share_sdk_client.availability_zones.assert_not_called() + self.volume_client.availability_zones.assert_not_called() + + self.assertEqual(self.short_columnslist, columns) + datalist = () + for network_az in self.network_azs: + datalist += _build_network_az_datalist(network_az) + self.assertEqual(datalist, tuple(data)) + + def test_availability_zone_list_share(self): + arglist = [ + '--share', + ] + verifylist = [ + ('share', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) self.compute_client.availability_zones.assert_not_called() - self.volume_sdk_client.availability_zones.assert_called_with() self.network_client.availability_zones.assert_not_called() + self.share_sdk_client.availability_zones.assert_called_with() + self.volume_client.availability_zones.assert_not_called() self.assertEqual(self.short_columnslist, columns) datalist = () - for volume_az in self.volume_azs: - datalist += _build_volume_az_datalist(volume_az) + for share_az in self.share_azs: + datalist += _build_share_az_datalist(share_az) self.assertEqual(datalist, tuple(data)) - def test_availability_zone_list_network(self): + def test_availability_zone_list_volume(self): arglist = [ - '--network', + '--volume', ] verifylist = [ - ('network', True), + ('volume', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) self.compute_client.availability_zones.assert_not_called() - self.volume_sdk_client.availability_zones.assert_not_called() - self.network_client.availability_zones.assert_called_with() + self.network_client.availability_zones.assert_not_called() + self.share_sdk_client.availability_zones.assert_not_called() + self.volume_client.availability_zones.assert_called_with() self.assertEqual(self.short_columnslist, columns) datalist = () - for network_az in self.network_azs: - datalist += _build_network_az_datalist(network_az) + for volume_az in self.volume_azs: + datalist += _build_volume_az_datalist(volume_az) self.assertEqual(datalist, tuple(data)) diff --git a/openstackclient/tests/unit/common/test_command.py b/openstackclient/tests/unit/common/test_command.py index 8233f8994b..1f1efceadf 100644 --- a/openstackclient/tests/unit/common/test_command.py +++ b/openstackclient/tests/unit/common/test_command.py @@ -14,9 +14,9 @@ from unittest import mock -from osc_lib.command import command from osc_lib import exceptions +from openstackclient import command from openstackclient.tests.unit import fakes as test_fakes from openstackclient.tests.unit import utils as test_utils diff --git a/openstackclient/tests/unit/common/test_extension.py b/openstackclient/tests/unit/common/test_extension.py index 53d9d65d9b..8ae3da3f5b 100644 --- a/openstackclient/tests/unit/common/test_extension.py +++ b/openstackclient/tests/unit/common/test_extension.py @@ -10,7 +10,10 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest import mock + +from openstack.block_storage.v3 import extension as _volume_extension +from openstack.identity.v2 import extension as _identity_extension +from openstack.test import fakes as sdk_fakes from openstackclient.common import extension from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes @@ -41,21 +44,23 @@ class TestExtensionList(TestExtension): 'Links', ) - volume_extension = volume_fakes.create_one_extension() - identity_extension = identity_fakes.FakeExtension.create_one_extension() + volume_extension = sdk_fakes.generate_fake_resource( + _volume_extension.Extension + ) + identity_extension = sdk_fakes.generate_fake_resource( + _identity_extension.Extension + ) compute_extension = compute_fakes.create_one_extension() network_extension = network_fakes.create_one_extension() def setUp(self): super().setUp() - self.identity_client.extensions.list.return_value = [ + self.identity_sdk_client.extensions.return_value = [ self.identity_extension ] self.compute_client.extensions.return_value = [self.compute_extension] - self.volume_sdk_client.extensions.return_value = [ - self.volume_extension - ] + self.volume_client.extensions.return_value = [self.volume_extension] self.network_client.extensions.return_value = [self.network_extension] # Get the command object to test @@ -103,11 +108,36 @@ def test_extension_list_no_options(self): ), ) self._test_extension_list_helper(arglist, verifylist, datalist) - self.identity_client.extensions.list.assert_called_with() + self.identity_sdk_client.extensions.assert_called_with() self.compute_client.extensions.assert_called_with() - self.volume_sdk_client.extensions.assert_called_with() + self.volume_client.extensions.assert_called_with() self.network_client.extensions.assert_called_with() + def test_extension_list_no_volume_endpoint(self): + self.app.client_manager.volume_endpoint_enabled = False + + arglist = [] + verifylist = [] + datalist = ( + ( + self.identity_extension.name, + self.identity_extension.alias, + self.identity_extension.description, + ), + ( + self.compute_extension.name, + self.compute_extension.alias, + self.compute_extension.description, + ), + ( + self.network_extension.name, + self.network_extension.alias, + self.network_extension.description, + ), + ) + self._test_extension_list_helper(arglist, verifylist, datalist) + self.volume_client.extensions.assert_not_called() + def test_extension_list_long(self): arglist = [ '--long', @@ -121,7 +151,7 @@ def test_extension_list_long(self): self.identity_extension.alias, self.identity_extension.description, self.identity_extension.namespace, - '', + self.identity_extension.updated_at, self.identity_extension.links, ), ( @@ -150,9 +180,9 @@ def test_extension_list_long(self): ), ) self._test_extension_list_helper(arglist, verifylist, datalist, True) - self.identity_client.extensions.list.assert_called_with() + self.identity_sdk_client.extensions.assert_called_with() self.compute_client.extensions.assert_called_with() - self.volume_sdk_client.extensions.assert_called_with() + self.volume_client.extensions.assert_called_with() self.network_client.extensions.assert_called_with() def test_extension_list_identity(self): @@ -170,7 +200,7 @@ def test_extension_list_identity(self): ), ) self._test_extension_list_helper(arglist, verifylist, datalist) - self.identity_client.extensions.list.assert_called_with() + self.identity_sdk_client.extensions.assert_called_with() def test_extension_list_network(self): arglist = [ @@ -270,7 +300,7 @@ def test_extension_list_volume(self): ), ) self._test_extension_list_helper(arglist, verifylist, datalist) - self.volume_sdk_client.extensions.assert_called_with() + self.volume_client.extensions.assert_called_with() class TestExtensionShow(TestExtension): @@ -295,8 +325,8 @@ def setUp(self): self.cmd = extension.ShowExtension(self.app, None) - self.app.client_manager.network.find_extension = mock.Mock( - return_value=self.extension_details + self.network_client.find_extension.return_value = ( + self.extension_details ) def test_show_no_options(self): @@ -323,7 +353,7 @@ def test_show_all_options(self): columns, data = self.cmd.take_action(parsed_args) - self.app.client_manager.network.find_extension.assert_called_with( + self.network_client.find_extension.assert_called_with( self.extension_details.alias, ignore_missing=False ) diff --git a/openstackclient/tests/unit/common/test_limits.py b/openstackclient/tests/unit/common/test_limits.py index a375a2ae5a..7c4d2ae92e 100644 --- a/openstackclient/tests/unit/common/test_limits.py +++ b/openstackclient/tests/unit/common/test_limits.py @@ -9,14 +9,60 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# + +from openstack.block_storage.v3 import limits as _volume_limits from openstackclient.common import limits from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes -class TestComputeLimits(compute_fakes.TestComputev2): +def _create_fake_volume_limits(): + return _volume_limits.Limit( + absolute={ + 'totalSnapshotsUsed': 1, + 'maxTotalBackups': 10, + 'maxTotalVolumeGigabytes': 1000, + 'maxTotalSnapshots': 10, + 'maxTotalBackupGigabytes': 1000, + 'totalBackupGigabytesUsed': 0, + 'maxTotalVolumes': 10, + 'totalVolumesUsed': 4, + 'totalBackupsUsed': 0, + 'totalGigabytesUsed': 35, + }, + rate=[ + { + "uri": "*", + "limit": [ + { + "value": 10, + "verb": "POST", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z", + }, + { + "value": 10, + "verb": "PUT", + "remaining": 2, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z", + }, + { + "value": 100, + "verb": "DELETE", + "remaining": 100, + "unit": "MINUTE", + "next-available": "2011-12-15T22:42:45Z", + }, + ], + } + ], + ) + + +class TestComputeLimits(compute_fakes.TestCompute): absolute_columns = ['Name', 'Value'] rate_columns = ["Verb", "URI", "Value", "Remain", "Unit", "Next Available"] @@ -101,7 +147,7 @@ def setUp(self): super().setUp() self.app.client_manager.compute_endpoint_enabled = False - self.fake_limits = volume_fakes.create_limits() + self.fake_limits = _create_fake_volume_limits() self.absolute_data = [ ('max_total_backup_gigabytes', 1000), @@ -121,7 +167,7 @@ def setUp(self): ('DELETE', '*', 100, 100, 'MINUTE', '2011-12-15T22:42:45Z'), ] - self.volume_sdk_client.get_limits.return_value = self.fake_limits + self.volume_client.get_limits.return_value = self.fake_limits def test_volume_show_absolute(self): arglist = ['--absolute'] diff --git a/openstackclient/tests/unit/common/test_logs.py b/openstackclient/tests/unit/common/test_logs.py deleted file mode 100644 index 234ba427e9..0000000000 --- a/openstackclient/tests/unit/common/test_logs.py +++ /dev/null @@ -1,221 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -# NOTE(dtroyer): This file is deprecated in Jun 2016, remove after 4.x release -# or Jun 2017. - -import logging -from unittest import mock - -from osc_lib import logs - -from openstackclient.tests.unit import utils - - -class TestContext(utils.TestCase): - def test_log_level_from_options(self): - opts = mock.Mock() - opts.verbose_level = 0 - self.assertEqual(logging.ERROR, logs.log_level_from_options(opts)) - opts.verbose_level = 1 - self.assertEqual(logging.WARNING, logs.log_level_from_options(opts)) - opts.verbose_level = 2 - self.assertEqual(logging.INFO, logs.log_level_from_options(opts)) - opts.verbose_level = 3 - self.assertEqual(logging.DEBUG, logs.log_level_from_options(opts)) - - def test_log_level_from_config(self): - cfg = {'verbose_level': 0} - self.assertEqual(logging.ERROR, logs.log_level_from_config(cfg)) - cfg = {'verbose_level': 1} - self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) - cfg = {'verbose_level': 2} - self.assertEqual(logging.INFO, logs.log_level_from_config(cfg)) - cfg = {'verbose_level': 3} - self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) - cfg = {'verbose_level': 1, 'log_level': 'critical'} - self.assertEqual(logging.CRITICAL, logs.log_level_from_config(cfg)) - cfg = {'verbose_level': 1, 'log_level': 'error'} - self.assertEqual(logging.ERROR, logs.log_level_from_config(cfg)) - cfg = {'verbose_level': 1, 'log_level': 'warning'} - self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) - cfg = {'verbose_level': 1, 'log_level': 'info'} - self.assertEqual(logging.INFO, logs.log_level_from_config(cfg)) - cfg = {'verbose_level': 1, 'log_level': 'debug'} - self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) - cfg = {'verbose_level': 1, 'log_level': 'bogus'} - self.assertEqual(logging.WARNING, logs.log_level_from_config(cfg)) - cfg = {'verbose_level': 1, 'log_level': 'info', 'debug': True} - self.assertEqual(logging.DEBUG, logs.log_level_from_config(cfg)) - - @mock.patch('warnings.simplefilter') - def test_set_warning_filter(self, simplefilter): - logs.set_warning_filter(logging.ERROR) - simplefilter.assert_called_with("ignore") - logs.set_warning_filter(logging.WARNING) - simplefilter.assert_called_with("ignore") - logs.set_warning_filter(logging.INFO) - simplefilter.assert_called_with("once") - - -class TestFileFormatter(utils.TestCase): - def test_nothing(self): - formatter = logs._FileFormatter() - self.assertEqual( - ( - '%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' - '%(name)s %(message)s' - ), - formatter.fmt, - ) - - def test_options(self): - class Opts: - cloud = 'cloudy' - os_project_name = 'projecty' - username = 'usernamey' - - options = Opts() - formatter = logs._FileFormatter(options=options) - self.assertEqual( - ( - '%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' - '%(name)s [cloudy usernamey projecty] %(message)s' - ), - formatter.fmt, - ) - - def test_config(self): - config = mock.Mock() - config.config = {'cloud': 'cloudy'} - config.auth = {'project_name': 'projecty', 'username': 'usernamey'} - formatter = logs._FileFormatter(config=config) - self.assertEqual( - ( - '%(asctime)s.%(msecs)03d %(process)d %(levelname)s ' - '%(name)s [cloudy usernamey projecty] %(message)s' - ), - formatter.fmt, - ) - - -class TestLogConfigurator(utils.TestCase): - def setUp(self): - super().setUp() - self.options = mock.Mock() - self.options.verbose_level = 1 - self.options.log_file = None - self.options.debug = False - self.root_logger = mock.Mock() - self.root_logger.setLevel = mock.Mock() - self.root_logger.addHandler = mock.Mock() - self.requests_log = mock.Mock() - self.requests_log.setLevel = mock.Mock() - self.cliff_log = mock.Mock() - self.cliff_log.setLevel = mock.Mock() - self.stevedore_log = mock.Mock() - self.stevedore_log.setLevel = mock.Mock() - self.iso8601_log = mock.Mock() - self.iso8601_log.setLevel = mock.Mock() - self.loggers = [ - self.root_logger, - self.requests_log, - self.cliff_log, - self.stevedore_log, - self.iso8601_log, - ] - - @mock.patch('logging.StreamHandler') - @mock.patch('logging.getLogger') - @mock.patch('osc_lib.logs.set_warning_filter') - def test_init(self, warning_filter, getLogger, handle): - getLogger.side_effect = self.loggers - console_logger = mock.Mock() - console_logger.setFormatter = mock.Mock() - console_logger.setLevel = mock.Mock() - handle.return_value = console_logger - - configurator = logs.LogConfigurator(self.options) - - getLogger.assert_called_with('iso8601') # last call - warning_filter.assert_called_with(logging.WARNING) - self.root_logger.setLevel.assert_called_with(logging.DEBUG) - self.root_logger.addHandler.assert_called_with(console_logger) - self.requests_log.setLevel.assert_called_with(logging.ERROR) - self.cliff_log.setLevel.assert_called_with(logging.ERROR) - self.stevedore_log.setLevel.assert_called_with(logging.ERROR) - self.iso8601_log.setLevel.assert_called_with(logging.ERROR) - self.assertFalse(configurator.dump_trace) - - @mock.patch('logging.getLogger') - @mock.patch('osc_lib.logs.set_warning_filter') - def test_init_no_debug(self, warning_filter, getLogger): - getLogger.side_effect = self.loggers - self.options.debug = True - - configurator = logs.LogConfigurator(self.options) - - warning_filter.assert_called_with(logging.DEBUG) - self.requests_log.setLevel.assert_called_with(logging.DEBUG) - self.assertTrue(configurator.dump_trace) - - @mock.patch('logging.FileHandler') - @mock.patch('logging.getLogger') - @mock.patch('osc_lib.logs.set_warning_filter') - @mock.patch('osc_lib.logs._FileFormatter') - def test_init_log_file(self, formatter, warning_filter, getLogger, handle): - getLogger.side_effect = self.loggers - self.options.log_file = '/tmp/log_file' - file_logger = mock.Mock() - file_logger.setFormatter = mock.Mock() - file_logger.setLevel = mock.Mock() - handle.return_value = file_logger - mock_formatter = mock.Mock() - formatter.return_value = mock_formatter - - logs.LogConfigurator(self.options) - - handle.assert_called_with(filename=self.options.log_file) - self.root_logger.addHandler.assert_called_with(file_logger) - file_logger.setFormatter.assert_called_with(mock_formatter) - file_logger.setLevel.assert_called_with(logging.WARNING) - - @mock.patch('logging.FileHandler') - @mock.patch('logging.getLogger') - @mock.patch('osc_lib.logs.set_warning_filter') - @mock.patch('osc_lib.logs._FileFormatter') - def test_configure(self, formatter, warning_filter, getLogger, handle): - getLogger.side_effect = self.loggers - configurator = logs.LogConfigurator(self.options) - cloud_config = mock.Mock() - config_log = '/tmp/config_log' - cloud_config.config = { - 'log_file': config_log, - 'verbose_level': 1, - 'log_level': 'info', - } - file_logger = mock.Mock() - file_logger.setFormatter = mock.Mock() - file_logger.setLevel = mock.Mock() - handle.return_value = file_logger - mock_formatter = mock.Mock() - formatter.return_value = mock_formatter - - configurator.configure(cloud_config) - - warning_filter.assert_called_with(logging.INFO) - handle.assert_called_with(filename=config_log) - self.root_logger.addHandler.assert_called_with(file_logger) - file_logger.setFormatter.assert_called_with(mock_formatter) - file_logger.setLevel.assert_called_with(logging.INFO) - self.assertFalse(configurator.dump_trace) diff --git a/openstackclient/tests/unit/common/test_module.py b/openstackclient/tests/unit/common/test_module.py index 476538a194..5f5d17d681 100644 --- a/openstackclient/tests/unit/common/test_module.py +++ b/openstackclient/tests/unit/common/test_module.py @@ -11,7 +11,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# """Test module module""" @@ -19,38 +18,65 @@ from unittest import mock from openstackclient.common import module as osc_module -from openstackclient.tests.unit import fakes from openstackclient.tests.unit import utils +class FakeModule: + def __init__(self, name): + self.name = name + + # NOTE(dtroyer): module_1 must match the version list filter (not --all) # currently == '*client*' module_name_1 = 'fakeclient' -module_version_1 = '0.1.2' +package_name_1 = 'python-fakeclient' +package_version_1 = '0.1.2' -module_name_2 = 'zlib' -module_version_2 = '1.1' +# module_2 match openstacksdk +module_name_2 = 'openstack' +package_name_2 = 'openstacksdk' +package_version_2 = '0.9.13' -# module_3 match openstacksdk -module_name_3 = 'openstack' -module_version_3 = '0.9.13' +# module_3 match sub module of fakeclient +module_name_3 = 'fakeclient.submodule' +package_name_3 = 'python-fakeclient' +package_version_3 = '0.2.2' -# module_4 match sub module of fakeclient -module_name_4 = 'fakeclient.submodule' -module_version_4 = '0.2.2' +# module_4 match non-client package +module_name_4 = 'requests' +package_name_4 = 'requests' +package_version_4 = '2.34.2' # module_5 match private module module_name_5 = '_private_module.lib' -module_version_5 = '0.0.1' MODULES = { 'sys': sys, - module_name_1: fakes.FakeModule(module_name_1, module_version_1), - module_name_2: fakes.FakeModule(module_name_2, module_version_2), - module_name_3: fakes.FakeModule(module_name_3, module_version_3), - module_name_4: fakes.FakeModule(module_name_4, module_version_4), - module_name_5: fakes.FakeModule(module_name_5, module_version_5), + module_name_1: FakeModule(module_name_1), + module_name_2: FakeModule(module_name_2), + module_name_3: FakeModule(module_name_3), + module_name_4: FakeModule(module_name_4), + module_name_5: FakeModule(module_name_5), } +PACKAGES = { + module_name_1: [package_name_1], + module_name_2: [package_name_2], + module_name_3: [package_name_3], + module_name_4: [package_name_4], +} + + +def fake_metadata_version(package): + if package == package_name_1: + return package_version_1 + if package == package_name_2: + return package_version_2 + if package == package_name_3: + return package_version_3 + if package == package_name_4: + return package_version_4 + + raise Exception('unrecognised package') class TestCommandList(utils.TestCommand): @@ -121,11 +147,6 @@ def test_command_list_with_group(self): self.assertEqual(datalist, tuple(data)) -@mock.patch.dict( - 'openstackclient.common.module.sys.modules', - values=MODULES, - clear=True, -) class TestModuleList(utils.TestCommand): def setUp(self): super().setUp() @@ -143,20 +164,31 @@ def test_module_list_no_options(self): # In base command class Lister in cliff, abstract method take_action() # returns a tuple containing the column names and an iterable # containing the data to be listed. - columns, data = self.cmd.take_action(parsed_args) + with ( + mock.patch( + 'openstackclient.common.module.importlib.metadata.packages_distributions', + return_value=PACKAGES, + ), + mock.patch( + 'openstackclient.common.module.importlib.metadata.version', + side_effect=fake_metadata_version, + ), + mock.patch.dict( + 'openstackclient.common.module.sys.modules', + values=MODULES, + clear=True, + ), + ): + columns, data = self.cmd.take_action(parsed_args) # Output xxxclient and openstacksdk, but not regular module, like: zlib self.assertIn(module_name_1, columns) - self.assertIn(module_version_1, data) - self.assertNotIn(module_name_2, columns) - self.assertNotIn(module_version_2, data) - self.assertIn(module_name_3, columns) - self.assertIn(module_version_3, data) + self.assertIn(package_version_1, data) + self.assertIn(module_name_2, columns) + self.assertIn(package_version_2, data) # Filter sub and private modules - self.assertNotIn(module_name_4, columns) - self.assertNotIn(module_version_4, data) + self.assertNotIn(module_name_3, columns) self.assertNotIn(module_name_5, columns) - self.assertNotIn(module_version_5, data) def test_module_list_all(self): arglist = [ @@ -170,17 +202,30 @@ def test_module_list_all(self): # In base command class Lister in cliff, abstract method take_action() # returns a tuple containing the column names and an iterable # containing the data to be listed. - columns, data = self.cmd.take_action(parsed_args) - - # Output xxxclient, openstacksdk and regular module, like: zlib + with ( + mock.patch( + 'openstackclient.common.module.importlib.metadata.packages_distributions', + return_value=PACKAGES, + ), + mock.patch( + 'openstackclient.common.module.importlib.metadata.version', + side_effect=fake_metadata_version, + ), + mock.patch.dict( + 'openstackclient.common.module.sys.modules', + values=MODULES, + clear=True, + ), + ): + columns, data = self.cmd.take_action(parsed_args) + + # Output xxxclient, openstacksdk and regular modules like requests self.assertIn(module_name_1, columns) - self.assertIn(module_version_1, data) + self.assertIn(package_version_1, data) self.assertIn(module_name_2, columns) - self.assertIn(module_version_2, data) - self.assertIn(module_name_3, columns) - self.assertIn(module_version_3, data) + self.assertIn(package_version_2, data) + self.assertIn(module_name_4, columns) + self.assertIn(package_version_4, data) # Filter sub and private modules - self.assertNotIn(module_name_4, columns) - self.assertNotIn(module_version_4, data) + self.assertNotIn(module_name_3, columns) self.assertNotIn(module_name_5, columns) - self.assertNotIn(module_version_5, data) diff --git a/openstackclient/tests/unit/common/test_project_cleanup.py b/openstackclient/tests/unit/common/test_project_cleanup.py index 42020646cf..d16bdb85cd 100644 --- a/openstackclient/tests/unit/common/test_project_cleanup.py +++ b/openstackclient/tests/unit/common/test_project_cleanup.py @@ -12,31 +12,24 @@ from unittest import mock +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes + from openstackclient.common import project_cleanup from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit import utils as test_utils -class TestProjectCleanup(test_utils.TestCommand): - project = identity_fakes.FakeProject.create_one_project() - +class TestProjectCleanup( + identity_fakes.FakeClientMixin, test_utils.TestCommand +): def setUp(self): super().setUp() self.cmd = project_cleanup.ProjectCleanup(self.app, None) - self.project_cleanup_mock = mock.Mock() - self.sdk_connect_as_project_mock = mock.Mock( - return_value=self.app.client_manager.sdk_connection - ) - self.app.client_manager.sdk_connection.project_cleanup = ( - self.project_cleanup_mock - ) - self.app.client_manager.sdk_connection.identity.find_project = ( - mock.Mock(return_value=self.project) - ) - self.app.client_manager.sdk_connection.connect_as_project = ( - self.sdk_connect_as_project_mock - ) + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + self.app.client_manager.sdk_connection.connect_as_project.return_value = self.app.client_manager.sdk_connection def test_project_no_options(self): arglist = [] @@ -72,7 +65,9 @@ def test_project_cleanup_with_filters(self): with mock.patch('getpass.getpass', return_value='y'): result = self.cmd.take_action(parsed_args) - self.sdk_connect_as_project_mock.assert_called_with(self.project) + self.app.client_manager.sdk_connection.connect_as_project.assert_called_with( + self.project + ) filters = {'created_at': '2200-01-01', 'updated_at': '2200-01-02'} calls = [ @@ -89,7 +84,9 @@ def test_project_cleanup_with_filters(self): skip_resources=None, ), ] - self.project_cleanup_mock.assert_has_calls(calls) + self.app.client_manager.sdk_connection.project_cleanup.assert_has_calls( + calls + ) self.assertIsNone(result) @@ -110,7 +107,9 @@ def test_project_cleanup_with_auto_approve(self): result = self.cmd.take_action(parsed_args) - self.sdk_connect_as_project_mock.assert_called_with(self.project) + self.app.client_manager.sdk_connection.connect_as_project.assert_called_with( + self.project + ) calls = [ mock.call( dry_run=True, @@ -125,7 +124,9 @@ def test_project_cleanup_with_auto_approve(self): skip_resources=None, ), ] - self.project_cleanup_mock.assert_has_calls(calls) + self.app.client_manager.sdk_connection.project_cleanup.assert_has_calls( + calls + ) self.assertIsNone(result) @@ -145,7 +146,9 @@ def test_project_cleanup_with_project(self): with mock.patch('getpass.getpass', return_value='y'): result = self.cmd.take_action(parsed_args) - self.sdk_connect_as_project_mock.assert_called_with(self.project) + self.app.client_manager.sdk_connection.connect_as_project.assert_called_with( + self.project + ) calls = [ mock.call( dry_run=True, @@ -160,7 +163,9 @@ def test_project_cleanup_with_project(self): skip_resources=None, ), ] - self.project_cleanup_mock.assert_has_calls(calls) + self.app.client_manager.sdk_connection.project_cleanup.assert_has_calls( + calls + ) self.assertIsNone(result) @@ -180,7 +185,9 @@ def test_project_cleanup_with_project_abort(self): with mock.patch('getpass.getpass', return_value='y'): result = self.cmd.take_action(parsed_args) - self.sdk_connect_as_project_mock.assert_called_with(self.project) + self.app.client_manager.sdk_connection.connect_as_project.assert_called_with( + self.project + ) calls = [ mock.call( dry_run=True, @@ -189,7 +196,9 @@ def test_project_cleanup_with_project_abort(self): skip_resources=None, ), ] - self.project_cleanup_mock.assert_has_calls(calls) + self.app.client_manager.sdk_connection.project_cleanup.assert_has_calls( + calls + ) self.assertIsNone(result) @@ -209,8 +218,10 @@ def test_project_cleanup_with_dry_run(self): result = self.cmd.take_action(parsed_args) - self.sdk_connect_as_project_mock.assert_called_with(self.project) - self.project_cleanup_mock.assert_called_once_with( + self.app.client_manager.sdk_connection.connect_as_project.assert_called_with( + self.project + ) + self.app.client_manager.sdk_connection.project_cleanup.assert_called_once_with( dry_run=True, status_queue=mock.ANY, filters={}, @@ -236,7 +247,7 @@ def test_project_cleanup_with_auth_project(self): with mock.patch('getpass.getpass', return_value='y'): result = self.cmd.take_action(parsed_args) - self.sdk_connect_as_project_mock.assert_not_called() + self.app.client_manager.sdk_connection.connect_as_project.assert_not_called() calls = [ mock.call( dry_run=True, @@ -251,7 +262,9 @@ def test_project_cleanup_with_auth_project(self): skip_resources=None, ), ] - self.project_cleanup_mock.assert_has_calls(calls) + self.app.client_manager.sdk_connection.project_cleanup.assert_has_calls( + calls + ) self.assertIsNone(result) @@ -270,7 +283,9 @@ def test_project_cleanup_with_skip_resource(self): with mock.patch('getpass.getpass', return_value='y'): result = self.cmd.take_action(parsed_args) - self.sdk_connect_as_project_mock.assert_called_with(self.project) + self.app.client_manager.sdk_connection.connect_as_project.assert_called_with( + self.project + ) calls = [ mock.call( @@ -286,6 +301,8 @@ def test_project_cleanup_with_skip_resource(self): skip_resources=[skip_resource], ), ] - self.project_cleanup_mock.assert_has_calls(calls) + self.app.client_manager.sdk_connection.project_cleanup.assert_has_calls( + calls + ) self.assertIsNone(result) diff --git a/openstackclient/tests/unit/common/test_quota.py b/openstackclient/tests/unit/common/test_quota.py index 53df4da848..477ff76f30 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +import copy from unittest import mock from openstack.block_storage.v3 import quota_set as _volume_quota_set @@ -67,6 +68,7 @@ class TestQuotaList(TestQuota): 'Networks', 'Ports', 'RBAC Policies', + 'Router Routes', 'Routers', 'Security Groups', 'Security Group Rules', @@ -79,6 +81,7 @@ class TestQuotaList(TestQuota): 'Backups', 'Backup Gigabytes', 'Gigabytes', + 'Groups', 'Per Volume Gigabytes', 'Snapshots', 'Volumes', @@ -132,6 +135,7 @@ def setUp(self): self.network_quotas[0].networks, self.network_quotas[0].ports, self.network_quotas[0].rbac_policies, + self.network_quotas[0].router_routes, self.network_quotas[0].routers, self.network_quotas[0].security_groups, self.network_quotas[0].security_group_rules, @@ -147,7 +151,7 @@ def setUp(self): _volume_quota_set.QuotaSet ) # the defaults are global hence use of return_value here - self.volume_sdk_client.get_quota_set_defaults.return_value = ( + self.volume_client.get_quota_set_defaults.return_value = ( self.default_volume_quotas ) self.volume_reference_data = ( @@ -155,6 +159,7 @@ def setUp(self): self.volume_quotas[0].backups, self.volume_quotas[0].backup_gigabytes, self.volume_quotas[0].gigabytes, + self.volume_quotas[0].groups, self.volume_quotas[0].per_volume_gigabytes, self.volume_quotas[0].snapshots, self.volume_quotas[0].volumes, @@ -332,7 +337,7 @@ def test_quota_list_network_no_project(self): def test_quota_list_volume(self): # Two projects with non-default quotas - self.volume_sdk_client.get_quota_set.side_effect = self.volume_quotas + self.volume_client.get_quota_set.side_effect = self.volume_quotas arglist = [ '--volume', @@ -351,7 +356,7 @@ def test_quota_list_volume(self): def test_quota_list_volume_default(self): # Two projects with non-default quotas - self.volume_sdk_client.get_quota_set.side_effect = [ + self.volume_client.get_quota_set.side_effect = [ self.volume_quotas[0], self.default_volume_quotas, ] @@ -381,8 +386,6 @@ def setUp(self): self.cmd = quota.SetQuota(self.app, None) def test_quota_set(self): - floating_ip_num = 100 - fix_ip_num = 100 injected_file_num = 100 injected_file_size_num = 10240 injected_path_size_num = 255 @@ -391,16 +394,10 @@ def test_quota_set(self): ram_num = 51200 instance_num = 10 property_num = 128 - secgroup_rule_num = 20 - secgroup_num = 10 servgroup_num = 10 servgroup_members_num = 10 arglist = [ - '--floating-ips', - str(floating_ip_num), - '--fixed-ips', - str(fix_ip_num), '--injected-files', str(injected_file_num), '--injected-file-size', @@ -417,10 +414,6 @@ def test_quota_set(self): str(instance_num), '--properties', str(property_num), - '--secgroup-rules', - str(secgroup_rule_num), - '--secgroups', - str(secgroup_num), '--server-groups', str(servgroup_num), '--server-group-members', @@ -428,8 +421,6 @@ def test_quota_set(self): self.projects[0].name, ] verifylist = [ - ('floating_ips', floating_ip_num), - ('fixed_ips', fix_ip_num), ('injected_files', injected_file_num), ( 'injected_file_content_bytes', @@ -441,8 +432,6 @@ def test_quota_set(self): ('ram', ram_num), ('instances', instance_num), ('metadata_items', property_num), - ('security_group_rules', secgroup_rule_num), - ('security_groups', secgroup_num), ('server_groups', servgroup_num), ('server_group_members', servgroup_members_num), ('force', False), @@ -454,18 +443,14 @@ def test_quota_set(self): result = self.cmd.take_action(parsed_args) kwargs = { - 'floating_ips': floating_ip_num, - 'fixed_ips': fix_ip_num, 'injected_files': injected_file_num, - 'injected_file_content_bytes': injected_file_size_num, # noqa: E501 + 'injected_file_content_bytes': injected_file_size_num, 'injected_file_path_bytes': injected_path_size_num, 'key_pairs': key_pair_num, 'cores': core_num, 'ram': ram_num, 'instances': instance_num, 'metadata_items': property_num, - 'security_group_rules': secgroup_rule_num, - 'security_groups': secgroup_num, 'server_groups': servgroup_num, 'server_group_members': servgroup_members_num, } @@ -523,7 +508,7 @@ def test_quota_set_volume(self): 'per_volume_gigabytes': per_volume_gigabytes, } - self.volume_sdk_client.update_quota_set.assert_called_once_with( + self.volume_client.update_quota_set.assert_called_once_with( self.projects[0].id, **kwargs ) @@ -580,7 +565,7 @@ def test_quota_set_volume_with_volume_type(self): 'per_volume_gigabytes': per_volume_gigabytes, } - self.volume_sdk_client.update_quota_set.assert_called_once_with( + self.volume_client.update_quota_set.assert_called_once_with( self.projects[0].id, **kwargs ) self.assertIsNone(result) @@ -728,7 +713,7 @@ def test_quota_set_with_class(self): kwargs_compute = { 'injected_files': injected_file_num, - 'injected_file_content_bytes': injected_file_size_num, # noqa: E501 + 'injected_file_content_bytes': injected_file_size_num, 'injected_file_path_bytes': injected_path_size_num, 'key_pairs': key_pair_num, 'cores': core_num, @@ -747,7 +732,7 @@ def test_quota_set_with_class(self): self.compute_client.update_quota_class_set.assert_called_with( self.projects[0].name, **kwargs_compute ) - self.volume_sdk_client.update_quota_class_set.assert_called_with( + self.volume_client.update_quota_class_set.assert_called_with( self.projects[0].name, **kwargs_volume ) self.assertNotCalled(self.network_client.update_quota) @@ -826,7 +811,7 @@ def test_quota_set_default(self): kwargs_compute = { 'injected_files': injected_file_num, - 'injected_file_content_bytes': injected_file_size_num, # noqa: E501 + 'injected_file_content_bytes': injected_file_size_num, 'injected_file_path_bytes': injected_path_size_num, 'key_pairs': key_pair_num, 'cores': core_num, @@ -845,7 +830,7 @@ def test_quota_set_default(self): self.compute_client.update_quota_class_set.assert_called_with( 'default', **kwargs_compute ) - self.volume_sdk_client.update_quota_class_set.assert_called_with( + self.volume_client.update_quota_class_set.assert_called_with( 'default', **kwargs_volume ) self.assertNotCalled(self.network_client.update_quota) @@ -902,7 +887,7 @@ def test_quota_set_with_force(self): self.compute_client.update_quota_set.assert_called_once_with( self.projects[0].id, **kwargs_compute ) - self.volume_sdk_client.update_quota_set.assert_called_once_with( + self.volume_client.update_quota_set.assert_called_once_with( self.projects[0].id, **kwargs_volume ) self.network_client.update_quota.assert_called_once_with( @@ -945,7 +930,7 @@ def test_quota_set_with_no_force(self): self.compute_client.update_quota_set.assert_called_once_with( self.projects[0].id, **kwargs_compute ) - self.volume_sdk_client.update_quota_set.assert_called_once_with( + self.volume_client.update_quota_set.assert_called_once_with( self.projects[0].id, **kwargs_volume ) self.network_client.update_quota.assert_called_once_with( @@ -965,6 +950,7 @@ class TestQuotaShow(TestQuota): 'pools': {'limit': 0, 'reserved': 0, 'used': 0}, 'ports': {'limit': 0, 'reserved': 0, 'used': 0}, 'rbac_policies': {'limit': 0, 'reserved': 0, 'used': 0}, + 'router_routes': {'limit': 0, 'reserved': 0, 'used': 0}, 'routers': {'limit': 0, 'reserved': 0, 'used': 0}, 'security_group_rules': {'limit': 0, 'reserved': 0, 'used': 0}, 'security_groups': {'limit': 0, 'reserved': 0, 'used': 0}, @@ -987,13 +973,13 @@ def setUp(self): self.default_compute_quotas ) - self.volume_sdk_client.get_quota_set.return_value = ( + self.volume_client.get_quota_set.return_value = ( sdk_fakes.generate_fake_resource(_volume_quota_set.QuotaSet) ) self.default_volume_quotas = sdk_fakes.generate_fake_resource( _volume_quota_set.QuotaSet ) - self.volume_sdk_client.get_quota_set_defaults.return_value = ( + self.volume_client.get_quota_set_defaults.return_value = ( self.default_volume_quotas ) @@ -1031,7 +1017,7 @@ def test_quota_show(self): self.projects[0].id, usage=False, ) - self.volume_sdk_client.get_quota_set.assert_called_once_with( + self.volume_client.get_quota_set.assert_called_once_with( self.projects[0].id, usage=False, ) @@ -1041,6 +1027,26 @@ def test_quota_show(self): ) self.assertNotCalled(self.network_client.get_quota_default) + def test_quota_show__missing_services(self): + self.app.client_manager.compute_endpoint_enabled = False + self.app.client_manager.volume_endpoint_enabled = False + self.app.client_manager.network_endpoint_enabled = False + + arglist = [ + self.projects[0].name, + ] + verifylist = [ + ('service', 'all'), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.compute_client.get_quota_set.assert_not_called() + self.volume_client.get_quota_set.assert_not_called() + self.network_client.get_quota.assert_not_called() + def test_quota_show__with_compute(self): arglist = [ '--compute', @@ -1058,7 +1064,7 @@ def test_quota_show__with_compute(self): self.projects[0].id, usage=False, ) - self.volume_sdk_client.get_quota_set.assert_not_called() + self.volume_client.get_quota_set.assert_not_called() self.network_client.get_quota.assert_not_called() def test_quota_show__with_volume(self): @@ -1075,7 +1081,7 @@ def test_quota_show__with_volume(self): self.cmd.take_action(parsed_args) self.compute_client.get_quota_set.assert_not_called() - self.volume_sdk_client.get_quota_set.assert_called_once_with( + self.volume_client.get_quota_set.assert_called_once_with( self.projects[0].id, usage=False, ) @@ -1095,13 +1101,72 @@ def test_quota_show__with_network(self): self.cmd.take_action(parsed_args) self.compute_client.get_quota_set.assert_not_called() - self.volume_sdk_client.get_quota_set.assert_not_called() + self.volume_client.get_quota_set.assert_not_called() self.network_client.get_quota.assert_called_once_with( self.projects[0].id, details=False, ) self.assertNotCalled(self.network_client.get_quota_default) + def test_quota_show__with_network_and_usage(self): + # ensure we do not interfere with other tests + self._network_quota_details = copy.deepcopy( + self._network_quota_details + ) + # set a couple of resources + self._network_quota_details["floating_ips"].update( + limit=30, reserved=20, used=7 + ) + self._network_quota_details["security_group_rules"].update( + limit=9, reserved=7, used=5 + ) + + arglist = [ + '--network', + '--usage', + self.projects[0].name, + ] + verifylist = [ + ('service', 'network'), + ('project', self.projects[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + headers, result_gen = self.cmd.take_action(parsed_args) + + self.assertEqual(('Resource', 'Limit', 'In Use', 'Reserved'), headers) + + result = sorted(result_gen) + + self.assertEqual( + [ + ('floating_ips', 30, 7, 20), + ('health_monitors', 0, 0, 0), + ('l7_policies', 0, 0, 0), + ('listeners', 0, 0, 0), + ('load_balancers', 0, 0, 0), + ('networks', 0, 0, 0), + ('pools', 0, 0, 0), + ('ports', 0, 0, 0), + ('rbac_policies', 0, 0, 0), + ('router_routes', 0, 0, 0), + ('routers', 0, 0, 0), + ('security_group_rules', 9, 5, 7), + ('security_groups', 0, 0, 0), + ('subnet_pools', 0, 0, 0), + ('subnets', 0, 0, 0), + ], + result, + ) + + self.compute_client.get_quota_set.assert_not_called() + self.volume_client.get_quota_set.assert_not_called() + self.network_client.get_quota.assert_called_once_with( + self.projects[0].id, + details=True, + ) + self.assertNotCalled(self.network_client.get_quota_default) + def test_quota_show__with_default(self): arglist = [ '--default', @@ -1118,7 +1183,7 @@ def test_quota_show__with_default(self): self.compute_client.get_quota_set_defaults.assert_called_once_with( self.projects[0].id, ) - self.volume_sdk_client.get_quota_set_defaults.assert_called_once_with( + self.volume_client.get_quota_set_defaults.assert_called_once_with( self.projects[0].id, ) self.network_client.get_quota_default.assert_called_once_with( @@ -1143,7 +1208,7 @@ def test_quota_show__with_usage(self): self.projects[0].id, usage=True, ) - self.volume_sdk_client.get_quota_set.assert_called_once_with( + self.volume_client.get_quota_set.assert_called_once_with( self.projects[0].id, usage=True, ) @@ -1152,6 +1217,22 @@ def test_quota_show__with_usage(self): details=True, ) + def test_quota_show__keypairs_usage_is_always_na(self): + """Verify that key-pair usage is overridden to N/A for project views.""" + arglist = [ + '--usage', + self.projects[0].name, + ] + parsed_args = self.check_parser(self.cmd, arglist, [('usage', True)]) + + _, data = self.cmd.take_action(parsed_args) + + keypair_row = [row for row in data if row[0] == 'key-pairs'] + + self.assertTrue(keypair_row, "key-pairs row was missing from output") + # Ensure the third column (In Use / Usage) is exactly "N/A" + self.assertEqual("N/A", keypair_row[0][2]) + def test_quota_show__no_project(self): arglist = [] verifylist = [ @@ -1164,7 +1245,7 @@ def test_quota_show__no_project(self): self.compute_client.get_quota_set.assert_called_once_with( self.projects[1].id, usage=False ) - self.volume_sdk_client.get_quota_set.assert_called_once_with( + self.volume_client.get_quota_set.assert_called_once_with( self.projects[1].id, usage=False ) self.network_client.get_quota.assert_called_once_with( @@ -1182,7 +1263,7 @@ def setUp(self): self.identity_sdk_client.find_project.return_value = self.projects[0] self.compute_client.revert_quota_set.return_value = None - self.volume_sdk_client.revert_quota_set.return_value = None + self.volume_client.revert_quota_set.return_value = None self.network_client.delete_quota.return_value = None self.cmd = quota.DeleteQuota(self.app, None) @@ -1208,7 +1289,7 @@ def test_delete(self): self.compute_client.revert_quota_set.assert_called_once_with( self.projects[0].id, ) - self.volume_sdk_client.revert_quota_set.assert_called_once_with( + self.volume_client.revert_quota_set.assert_called_once_with( self.projects[0].id, ) self.network_client.delete_quota.assert_called_once_with( @@ -1237,7 +1318,7 @@ def test_delete__compute(self): self.compute_client.revert_quota_set.assert_called_once_with( self.projects[0].id, ) - self.volume_sdk_client.revert_quota_set.assert_not_called() + self.volume_client.revert_quota_set.assert_not_called() self.network_client.delete_quota.assert_not_called() def test_delete__volume(self): @@ -1260,7 +1341,7 @@ def test_delete__volume(self): self.projects[0].id, ignore_missing=False ) self.compute_client.revert_quota_set.assert_not_called() - self.volume_sdk_client.revert_quota_set.assert_called_once_with( + self.volume_client.revert_quota_set.assert_called_once_with( self.projects[0].id, ) self.network_client.delete_quota.assert_not_called() @@ -1285,7 +1366,7 @@ def test_delete__network(self): self.projects[0].id, ignore_missing=False ) self.compute_client.revert_quota_set.assert_not_called() - self.volume_sdk_client.revert_quota_set.assert_not_called() + self.volume_client.revert_quota_set.assert_not_called() self.network_client.delete_quota.assert_called_once_with( self.projects[0].id, ) diff --git a/openstackclient/tests/unit/compute/v2/fakes.py b/openstackclient/tests/unit/compute/v2/fakes.py index 37f93b772b..338c7087aa 100644 --- a/openstackclient/tests/unit/compute/v2/fakes.py +++ b/openstackclient/tests/unit/compute/v2/fakes.py @@ -11,7 +11,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# import random import re @@ -19,7 +18,7 @@ import uuid from keystoneauth1 import discover -from openstack.compute.v2 import _proxy +from openstack.compute import v2 as compute_v2 from openstack.compute.v2 import availability_zone as _availability_zone from openstack.compute.v2 import extension as _extension from openstack.compute.v2 import flavor as _flavor @@ -29,7 +28,9 @@ from openstack.compute.v2 import server_action as _server_action from openstack.compute.v2 import server_interface as _server_interface from openstack.compute.v2 import server_migration as _server_migration +from openstack.compute.v2 import server_share as _server_share from openstack.compute.v2 import volume_attachment as _volume_attachment +from openstack.test import fakes as sdk_fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.image.v2 import fakes as image_fakes @@ -42,7 +43,10 @@ class FakeClientMixin: def setUp(self): super().setUp() - self.app.client_manager.compute = mock.Mock(_proxy.Proxy) + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.app.client_manager.compute = mock.Mock(spec=compute_v2.Proxy) self.compute_client = self.app.client_manager.compute self.set_compute_api_version() # default to the lowest @@ -64,7 +68,7 @@ def set_compute_api_version(self, version: str = '2.1'): ) -class TestComputev2( +class TestCompute( identity_fakes.FakeClientMixin, network_fakes.FakeClientMixin, image_fakes.FakeClientMixin, @@ -102,20 +106,6 @@ def create_one_agent(attrs=None): return agent_attrs -def create_agents(attrs=None, count=2): - """Create multiple fake agents. - - :param dict attrs: A dictionary with all attributes - :param int count: The number of agents to fake - :return: A list of dictionaries faking the agents - """ - agents = [] - for i in range(0, count): - agents.append(create_one_agent(attrs)) - - return agents - - def create_one_extension(attrs=None): """Create a fake extension. @@ -350,20 +340,6 @@ def create_one_flavor(attrs=None): return flavor -def create_flavors(attrs=None, count=2): - """Create multiple fake flavors. - - :param dict attrs: A dictionary with all attributes - :param int count: The number of flavors to fake - :return: A list of fake :class:`openstack.compute.v2.flavor.Flavor` objects - """ - flavors = [] - for i in range(0, count): - flavors.append(create_one_flavor(attrs)) - - return flavors - - def create_one_flavor_access(attrs=None): """Create a fake flavor access. @@ -497,19 +473,6 @@ def create_one_floating_ip_pool(attrs=None): return floating_ip_pool_attrs -def create_floating_ip_pools(attrs=None, count=2): - """Create multiple fake floating IP pools. - - :param dict attrs: A dictionary with all attributes - :param int count: The number of floating IP pools to fake - :return: A list of dictionaries faking the floating IP pools - """ - floating_ip_pools = [] - for i in range(0, count): - floating_ip_pools.append(create_one_floating_ip_pool(attrs)) - return floating_ip_pools - - def create_one_network(attrs=None): """Create a fake network. @@ -728,21 +691,6 @@ def create_one_server_migration(attrs=None): return migration -def create_server_migrations(attrs=None, methods=None, count=2): - """Create multiple server migrations. - - :param dict attrs: A dictionary with all attributes - :param int count: The number of server migrations to fake - :return A list of fake - openstack.compute.v2.server_migration.ServerMigration objects - """ - migrations = [] - for i in range(0, count): - migrations.append(create_one_server_migration(attrs, methods)) - - return migrations - - def create_one_volume_attachment(attrs=None): """Create a fake volume attachment. @@ -788,6 +736,29 @@ def create_volume_attachments(attrs=None, count=2): return volume_attachments +def create_one_share(attrs=None): + """Create a fake share attachment. + + :param dict attrs: A dictionary with all attributes + :return: A fake openstack.compute.v2.server_share.ShareMapping + object + """ + return sdk_fakes.generate_fake_resource( + _server_share.ShareMapping, **(attrs or {}) + ) + + +def create_shares(attrs=None, count=2): + """Create multiple fake share attachments. + + :param dict attrs: A dictionary with all attributes + :param int count: The number of share attachments to fake + :return: A list of fake + openstack.compute.v2.server_share.ShareMapping objects + """ + return [create_one_share(attrs) for _ in range(count)] + + def create_one_server_interface(attrs=None): """Create a fake ServerInterface. diff --git a/openstackclient/tests/unit/compute/v2/test_agent.py b/openstackclient/tests/unit/compute/v2/test_agent.py index f04ba1066e..292e0182dd 100644 --- a/openstackclient/tests/unit/compute/v2/test_agent.py +++ b/openstackclient/tests/unit/compute/v2/test_agent.py @@ -37,7 +37,7 @@ def _generate_fake_agent(): } -class TestAgentCreate(compute_fakes.TestComputev2): +class TestAgentCreate(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -106,7 +106,7 @@ def test_agent_create(self): self.assertEqual(self.data, data) -class TestAgentDelete(compute_fakes.TestComputev2): +class TestAgentDelete(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -180,7 +180,7 @@ def test_agent_delete_no_input(self): ) -class TestAgentList(compute_fakes.TestComputev2): +class TestAgentList(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -247,7 +247,7 @@ def test_agent_list_with_hypervisor(self): ) -class TestAgentSet(compute_fakes.TestComputev2): +class TestAgentSet(compute_fakes.TestCompute): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/compute/v2/test_aggregate.py b/openstackclient/tests/unit/compute/v2/test_aggregate.py index 813cbb95fa..d4a2797e93 100644 --- a/openstackclient/tests/unit/compute/v2/test_aggregate.py +++ b/openstackclient/tests/unit/compute/v2/test_aggregate.py @@ -27,7 +27,7 @@ from openstackclient.tests.unit.image.v2 import fakes as image_fakes -class TestAggregate(compute_fakes.TestComputev2): +class TestAggregate(compute_fakes.TestCompute): columns = ( 'availability_zone', 'created_at', @@ -165,9 +165,11 @@ def setUp(self): sdk_fakes.generate_fake_resources(_aggregate.Aggregate, 2) ) - self.compute_client.find_aggregate = mock.Mock( - side_effect=[self.fake_ags[0], self.fake_ags[1]] - ) + self.compute_client.find_aggregate.side_effect = [ + self.fake_ags[0], + self.fake_ags[1], + ] + self.cmd = aggregate.DeleteAggregate(self.app, None) def test_aggregate_delete(self): @@ -324,6 +326,70 @@ def test_aggregate_list_with_long(self): self.assertEqual(expected_columns, columns) self.assertCountEqual(expected_data, tuple(data)) + def test_aggregate_list_with_availability_zone(self): + self.set_compute_api_version('2.41') + + arglist = [ + '--availability-zone', + self.fake_ag.availability_zone, + ] + verifylist = [ + ('availability_zone', self.fake_ag.availability_zone), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + _, data = self.cmd.take_action(parsed_args) + + # the matching aggregate is returned + self.assertEqual(1, len(tuple(data))) + + def test_aggregate_list_with_availability_zone_no_match(self): + self.set_compute_api_version('2.41') + + arglist = [ + '--availability-zone', + 'does-not-exist', + ] + verifylist = [ + ('availability_zone', 'does-not-exist'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + _, data = self.cmd.take_action(parsed_args) + + # no aggregate matches the filter + self.assertEqual(0, len(tuple(data))) + + def test_aggregate_list_with_host(self): + self.set_compute_api_version('2.41') + + arglist = [ + '--host', + self.fake_ag.hosts[0], + ] + verifylist = [ + ('host', self.fake_ag.hosts[0]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + _, data = self.cmd.take_action(parsed_args) + + # the aggregate containing the host is returned + self.assertEqual(1, len(tuple(data))) + + def test_aggregate_list_with_host_no_match(self): + self.set_compute_api_version('2.41') + + arglist = [ + '--host', + 'does-not-exist', + ] + verifylist = [ + ('host', 'does-not-exist'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + _, data = self.cmd.take_action(parsed_args) + + # no aggregate contains the host + self.assertEqual(0, len(tuple(data))) + class TestAggregateRemoveHost(TestAggregate): def setUp(self): diff --git a/openstackclient/tests/unit/compute/v2/test_console.py b/openstackclient/tests/unit/compute/v2/test_console.py index 423290c118..84918e91e7 100644 --- a/openstackclient/tests/unit/compute/v2/test_console.py +++ b/openstackclient/tests/unit/compute/v2/test_console.py @@ -13,7 +13,6 @@ # under the License. # -from unittest import mock from openstack.compute.v2 import server as _server from openstack.test import fakes as sdk_fakes @@ -23,7 +22,7 @@ from openstackclient.tests.unit import utils -class TestConsoleLog(compute_fakes.TestComputev2): +class TestConsoleLog(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -78,7 +77,7 @@ def test_show_lines(self): ) -class TestConsoleUrlShow(compute_fakes.TestComputev2): +class TestConsoleUrlShow(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -90,9 +89,7 @@ def setUp(self): 'protocol': 'fake_protocol', 'type': 'fake_type', } - self.compute_client.create_console = mock.Mock( - return_value=fake_console_data - ) + self.compute_client.create_console.return_value = fake_console_data self.columns = ( 'protocol', diff --git a/openstackclient/tests/unit/compute/v2/test_console_connection.py b/openstackclient/tests/unit/compute/v2/test_console_connection.py index ab9cb0c05c..240ed375ad 100644 --- a/openstackclient/tests/unit/compute/v2/test_console_connection.py +++ b/openstackclient/tests/unit/compute/v2/test_console_connection.py @@ -19,7 +19,7 @@ from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -class TestConsoleTokens(compute_fakes.TestComputev2): +class TestConsoleTokens(compute_fakes.TestCompute): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/compute/v2/test_flavor.py b/openstackclient/tests/unit/compute/v2/test_flavor.py index fa8d5ad56e..eacd21ade2 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -16,77 +16,71 @@ from openstack.compute.v2 import flavor as _flavor from openstack import exceptions as sdk_exceptions +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from openstackclient.compute.v2 import flavor from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit import utils as tests_utils -class TestFlavor(compute_fakes.TestComputev2): +class TestFlavorCreate(compute_fakes.TestCompute): def setUp(self): super().setUp() - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() - - -class TestFlavorCreate(TestFlavor): - flavor = compute_fakes.create_one_flavor(attrs={'links': 'flavor-links'}) - project = identity_fakes.FakeProject.create_one_project() - - columns = ( - 'OS-FLV-DISABLED:disabled', - 'OS-FLV-EXT-DATA:ephemeral', - 'description', - 'disk', - 'id', - 'name', - 'os-flavor-access:is_public', - 'properties', - 'ram', - 'rxtx_factor', - 'swap', - 'vcpus', - ) - - data = ( - flavor.is_disabled, - flavor.ephemeral, - flavor.description, - flavor.disk, - flavor.id, - flavor.name, - flavor.is_public, - format_columns.DictColumn(flavor.extra_specs), - flavor.ram, - flavor.rxtx_factor, - flavor.swap, - flavor.vcpus, - ) - data_private = ( - flavor.is_disabled, - flavor.ephemeral, - flavor.description, - flavor.disk, - flavor.id, - flavor.name, - False, - format_columns.DictColumn(flavor.extra_specs), - flavor.ram, - flavor.rxtx_factor, - flavor.swap, - flavor.vcpus, - ) + self.flavor = sdk_fakes.generate_fake_resource( + _flavor.Flavor, links='flavor-links' + ) + self.project = sdk_fakes.generate_fake_resource(_project.Project) - def setUp(self): - super().setUp() + self.columns = ( + 'OS-FLV-DISABLED:disabled', + 'OS-FLV-EXT-DATA:ephemeral', + 'description', + 'disk', + 'id', + 'name', + 'os-flavor-access:is_public', + 'properties', + 'ram', + 'rxtx_factor', + 'swap', + 'vcpus', + ) + self.data = ( + self.flavor.is_disabled, + self.flavor.ephemeral, + self.flavor.description, + self.flavor.disk, + self.flavor.id, + self.flavor.name, + self.flavor.is_public, + format_columns.DictColumn(self.flavor.extra_specs), + self.flavor.ram, + self.flavor.rxtx_factor, + self.flavor.swap, + self.flavor.vcpus, + ) + self.data_private = ( + self.flavor.is_disabled, + self.flavor.ephemeral, + self.flavor.description, + self.flavor.disk, + self.flavor.id, + self.flavor.name, + False, + format_columns.DictColumn(self.flavor.extra_specs), + self.flavor.ram, + self.flavor.rxtx_factor, + self.flavor.swap, + self.flavor.vcpus, + ) - # Return a project - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = self.project self.compute_client.create_flavor.return_value = self.flavor + self.cmd = flavor.CreateFlavor(self.app, None) def test_flavor_create_default_options(self): @@ -104,7 +98,6 @@ def test_flavor_create_default_options(self): 'id': None, 'ephemeral': 0, 'swap': 0, - 'rxtx_factor': 1.0, 'is_public': True, } @@ -233,7 +226,7 @@ def test_flavor_create_other_options(self): ('vcpus', self.flavor.vcpus), ('rxtx_factor', self.flavor.rxtx_factor), ('public', False), - ('description', 'description'), + ('description', self.flavor.description), ('project', self.project.id), ('properties', {'key1': 'value1', 'key2': 'value2'}), ('name', self.flavor.name), @@ -245,7 +238,7 @@ def test_flavor_create_other_options(self): 'ram': self.flavor.ram, 'vcpus': self.flavor.vcpus, 'disk': self.flavor.disk, - 'id': 'auto', + 'id': None, 'ephemeral': self.flavor.ephemeral, 'swap': self.flavor.swap, 'rxtx_factor': self.flavor.rxtx_factor, @@ -328,7 +321,7 @@ def test_flavor_create_with_description(self): str(self.flavor.vcpus), '--rxtx-factor', str(self.flavor.rxtx_factor), - '--private', + '--public', '--description', 'fake description', self.flavor.name, @@ -341,7 +334,7 @@ def test_flavor_create_with_description(self): ('swap', self.flavor.swap), ('vcpus', self.flavor.vcpus), ('rxtx_factor', self.flavor.rxtx_factor), - ('public', False), + ('public', True), ('description', 'fake description'), ('name', self.flavor.name), ] @@ -358,14 +351,14 @@ def test_flavor_create_with_description(self): 'ephemeral': self.flavor.ephemeral, 'swap': self.flavor.swap, 'rxtx_factor': self.flavor.rxtx_factor, - 'is_public': self.flavor.is_public, + 'is_public': True, 'description': 'fake description', } self.compute_client.create_flavor.assert_called_once_with(**args) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.data_private, data) + self.assertCountEqual(self.data, data) def test_flavor_create_with_description_pre_v255(self): self.set_compute_api_version('2.54') @@ -393,13 +386,41 @@ def test_flavor_create_with_description_pre_v255(self): exceptions.CommandError, self.cmd.take_action, parsed_args ) + def test_flavor_create_with_rxtx_factor_post_v2102(self): + self.set_compute_api_version('2.102') -class TestFlavorDelete(TestFlavor): - flavors = compute_fakes.create_flavors(count=2) + arglist = [ + '--id', + self.flavor.id, + '--ram', + str(self.flavor.ram), + '--vcpus', + str(self.flavor.vcpus), + '--rxtx-factor', + '1.0', + self.flavor.name, + ] + verifylist = [ + ('ram', self.flavor.ram), + ('vcpus', self.flavor.vcpus), + ('rxtx_factor', 1.0), + ('name', self.flavor.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestFlavorDelete(compute_fakes.TestCompute): def setUp(self): super().setUp() + self.flavors = list( + sdk_fakes.generate_fake_resources(_flavor.Flavor, 2) + ) + self.compute_client.delete_flavor.return_value = None self.cmd = flavor.DeleteFlavor(self.app, None) @@ -473,52 +494,52 @@ def test_multi_flavors_delete_with_exception(self): self.compute_client.delete_flavor.assert_has_calls(delete_calls) -class TestFlavorList(TestFlavor): - _flavor = compute_fakes.create_one_flavor() - - columns = ( - 'ID', - 'Name', - 'RAM', - 'Disk', - 'Ephemeral', - 'VCPUs', - 'Is Public', - ) - columns_long = columns + ('Swap', 'RXTX Factor', 'Properties') - - data = ( - ( - _flavor.id, - _flavor.name, - _flavor.ram, - _flavor.disk, - _flavor.ephemeral, - _flavor.vcpus, - _flavor.is_public, - ), - ) - data_long = ( - data[0] - + ( - _flavor.swap, - _flavor.rxtx_factor, - format_columns.DictColumn(_flavor.extra_specs), - ), - ) - +class TestFlavorList(compute_fakes.TestCompute): def setUp(self): super().setUp() - self.api_mock = mock.Mock() - self.api_mock.side_effect = [ - [self._flavor], - [], - ] + self._flavor = sdk_fakes.generate_fake_resource( + _flavor.Flavor, extra_specs={'property': 'value'} + ) - self.compute_client.flavors = self.api_mock + self.columns = ( + 'ID', + 'Name', + 'RAM', + 'Disk', + 'Ephemeral', + 'VCPUs', + 'Is Public', + ) + self.columns_long = ( + *self.columns, + 'Swap', + 'RXTX Factor', + 'Properties', + ) + + self.data = ( + ( + self._flavor.id, + self._flavor.name, + self._flavor.ram, + self._flavor.disk, + self._flavor.ephemeral, + self._flavor.vcpus, + self._flavor.is_public, + ), + ) + self.data_long = ( + self.data[0] + + ( + self._flavor.swap, + self._flavor.rxtx_factor, + format_columns.DictColumn(self._flavor.extra_specs), + ), + ) + + self.compute_client.flavors.side_effect = [[self._flavor], []] - # Get the command object to test self.cmd = flavor.ListFlavor(self.app, None) def test_flavor_list_no_options(self): @@ -653,7 +674,9 @@ def test_flavor_list_long(self): def test_flavor_list_long_no_extra_specs(self): # use flavor with no extra specs for this test - flavor = compute_fakes.create_one_flavor(attrs={"extra_specs": {}}) + flavor = sdk_fakes.generate_fake_resource( + _flavor.Flavor, extra_specs={} + ) self.data = ( ( flavor.id, @@ -673,15 +696,9 @@ def test_flavor_list_long_no_extra_specs(self): format_columns.DictColumn(flavor.extra_specs), ), ) - self.api_mock.side_effect = [ - [flavor], - [], - ] - self.compute_client.flavors = self.api_mock - self.compute_client.fetch_flavor_extra_specs = mock.Mock( - return_value=None - ) + self.compute_client.flavors.side_effect = [[flavor], []] + self.compute_client.fetch_flavor_extra_specs.return_value = None arglist = [ '--long', @@ -743,19 +760,18 @@ def test_flavor_list_min_disk_min_ram(self): self.assertEqual(tuple(self.data), tuple(data)) -class TestFlavorSet(TestFlavor): - # Return value of self.compute_client.find_flavor(). - flavor = compute_fakes.create_one_flavor( - attrs={'os-flavor-access:is_public': False} - ) - project = identity_fakes.FakeProject.create_one_project() - +class TestFlavorSet(compute_fakes.TestCompute): def setUp(self): super().setUp() + self.flavor = sdk_fakes.generate_fake_resource( + _flavor.Flavor, is_public=False, extra_specs={'property': 'value'} + ) + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.compute_client.find_flavor.return_value = self.flavor - # Return a project - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = self.project + self.cmd = flavor.SetFlavor(self.app, None) def test_flavor_set_property(self): @@ -959,47 +975,43 @@ def test_flavor_set_description_using_name_pre_v255(self): ) -class TestFlavorShow(TestFlavor): - # Return value of self.compute_client.find_flavor(). - flavor_access = compute_fakes.create_one_flavor_access() - flavor = compute_fakes.create_one_flavor() - - columns = ( - 'OS-FLV-DISABLED:disabled', - 'OS-FLV-EXT-DATA:ephemeral', - 'access_project_ids', - 'description', - 'disk', - 'id', - 'name', - 'os-flavor-access:is_public', - 'properties', - 'ram', - 'rxtx_factor', - 'swap', - 'vcpus', - ) - - data = ( - flavor.is_disabled, - flavor.ephemeral, - None, - flavor.description, - flavor.disk, - flavor.id, - flavor.name, - flavor.is_public, - format_columns.DictColumn(flavor.extra_specs), - flavor.ram, - flavor.rxtx_factor, - flavor.swap, - flavor.vcpus, - ) - +class TestFlavorShow(compute_fakes.TestCompute): def setUp(self): super().setUp() - # Return value of _find_resource() + self.flavor_access = compute_fakes.create_one_flavor_access() + self.flavor = sdk_fakes.generate_fake_resource(_flavor.Flavor) + + self.columns = ( + 'OS-FLV-DISABLED:disabled', + 'OS-FLV-EXT-DATA:ephemeral', + 'access_project_ids', + 'description', + 'disk', + 'id', + 'name', + 'os-flavor-access:is_public', + 'properties', + 'ram', + 'rxtx_factor', + 'swap', + 'vcpus', + ) + self.data = ( + self.flavor.is_disabled, + self.flavor.ephemeral, + None, + self.flavor.description, + self.flavor.disk, + self.flavor.id, + self.flavor.name, + self.flavor.is_public, + format_columns.DictColumn(self.flavor.extra_specs), + self.flavor.ram, + self.flavor.rxtx_factor, + self.flavor.swap, + self.flavor.vcpus, + ) self.compute_client.find_flavor.return_value = self.flavor self.compute_client.get_flavor_access.return_value = [ self.flavor_access @@ -1035,10 +1047,8 @@ def test_public_flavor_show(self): self.assertCountEqual(self.data, data) def test_private_flavor_show(self): - private_flavor = compute_fakes.create_one_flavor( - attrs={ - 'os-flavor-access:is_public': False, - } + private_flavor = sdk_fakes.generate_fake_resource( + _flavor.Flavor, is_public=False ) self.compute_client.find_flavor.return_value = private_flavor @@ -1076,24 +1086,19 @@ def test_private_flavor_show(self): self.assertCountEqual(data_with_project, data) -class TestFlavorUnset(TestFlavor): - # Return value of self.compute_client.find_flavor(). - flavor = compute_fakes.create_one_flavor( - attrs={'os-flavor-access:is_public': False} - ) - project = identity_fakes.FakeProject.create_one_project() - +class TestFlavorUnset(compute_fakes.TestCompute): def setUp(self): super().setUp() + self.flavor = sdk_fakes.generate_fake_resource( + _flavor.Flavor, is_public=False + ) + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.compute_client.find_flavor.return_value = self.flavor - # Return a project - self.projects_mock.get.return_value = self.project - self.cmd = flavor.UnsetFlavor(self.app, None) + self.identity_sdk_client.find_project.return_value = self.project - self.mock_shortcut = ( - self.compute_client.delete_flavor_extra_specs_property - ) + self.cmd = flavor.UnsetFlavor(self.app, None) def test_flavor_unset_property(self): arglist = ['--property', 'property', 'baremetal'] @@ -1107,7 +1112,9 @@ def test_flavor_unset_property(self): self.compute_client.find_flavor.assert_called_with( parsed_args.flavor, get_extra_specs=True, ignore_missing=False ) - self.mock_shortcut.assert_called_with(self.flavor.id, 'property') + self.compute_client.delete_flavor_extra_specs_property.assert_called_with( + self.flavor.id, 'property' + ) self.compute_client.flavor_remove_tenant_access.assert_not_called() self.assertIsNone(result) @@ -1126,21 +1133,16 @@ def test_flavor_unset_properties(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) + self.compute_client.find_flavor.assert_called_with( parsed_args.flavor, get_extra_specs=True, ignore_missing=False ) - calls = [ - mock.call(self.flavor.id, 'property1'), - mock.call(self.flavor.id, 'property2'), - ] - self.mock_shortcut.assert_has_calls(calls) - - # A bit tricky way to ensure we do not unset other properties - calls.append(mock.call(self.flavor.id, 'property')) - self.assertRaises( - AssertionError, self.mock_shortcut.assert_has_calls, calls + self.compute_client.delete_flavor_extra_specs_property.assert_has_calls( + [ + mock.call(self.flavor.id, 'property1'), + mock.call(self.flavor.id, 'property2'), + ] ) - self.compute_client.flavor_remove_tenant_access.assert_not_called() def test_flavor_unset_project(self): diff --git a/openstackclient/tests/unit/compute/v2/test_host.py b/openstackclient/tests/unit/compute/v2/test_host.py index 8d38f8353b..6d81b3d8c6 100644 --- a/openstackclient/tests/unit/compute/v2/test_host.py +++ b/openstackclient/tests/unit/compute/v2/test_host.py @@ -58,7 +58,7 @@ def _generate_fake_host(): } -class TestHostList(compute_fakes.TestComputev2): +class TestHostList(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -111,7 +111,7 @@ def test_host_list_with_option(self): self.assertEqual(self.data, list(data)) -class TestHostSet(compute_fakes.TestComputev2): +class TestHostSet(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -160,7 +160,7 @@ def test_host_set(self): ) -class TestHostShow(compute_fakes.TestComputev2): +class TestHostShow(compute_fakes.TestCompute): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/compute/v2/test_hypervisor.py b/openstackclient/tests/unit/compute/v2/test_hypervisor.py index 4282982a27..71e017da38 100644 --- a/openstackclient/tests/unit/compute/v2/test_hypervisor.py +++ b/openstackclient/tests/unit/compute/v2/test_hypervisor.py @@ -24,7 +24,7 @@ from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -class TestHypervisorList(compute_fakes.TestComputev2): +class TestHypervisorList(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -280,7 +280,7 @@ def test_hypervisor_list_with_marker_pre_v233(self): ) -class TestHypervisorShow(compute_fakes.TestComputev2): +class TestHypervisorShow(compute_fakes.TestCompute): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/compute/v2/test_hypervisor_stats.py b/openstackclient/tests/unit/compute/v2/test_hypervisor_stats.py index 045efda2d2..7e75b3a5b2 100644 --- a/openstackclient/tests/unit/compute/v2/test_hypervisor_stats.py +++ b/openstackclient/tests/unit/compute/v2/test_hypervisor_stats.py @@ -12,20 +12,12 @@ # License for the specific language governing permissions and limitations # under the License. # -from unittest import mock from openstackclient.compute.v2 import hypervisor_stats from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes from openstackclient.tests.unit import fakes -class TestHypervisorStats(compute_fakes.TestComputev2): - def setUp(self): - super().setUp() - - self.compute_client.get = mock.Mock() - - # Not in fakes.py because hypervisor stats has been deprecated @@ -61,7 +53,7 @@ def create_one_hypervisor_stats(attrs=None): return stats_info -class TestHypervisorStatsShow(TestHypervisorStats): +class TestHypervisorStatsShow(compute_fakes.TestCompute): _stats = create_one_hypervisor_stats() def setUp(self): diff --git a/openstackclient/tests/unit/compute/v2/test_keypair.py b/openstackclient/tests/unit/compute/v2/test_keypair.py index 4eaaf4c9b0..3f887eb32c 100644 --- a/openstackclient/tests/unit/compute/v2/test_keypair.py +++ b/openstackclient/tests/unit/compute/v2/test_keypair.py @@ -28,18 +28,7 @@ from openstackclient.tests.unit import utils as tests_utils -class TestKeypair(compute_fakes.TestComputev2): - def setUp(self): - super().setUp() - - # Initialize the user mock - self._user = sdk_fakes.generate_fake_resource(_user.User) - self.users_mock = self.identity_client.users - self.users_mock.reset_mock() - self.users_mock.get.return_value = self._user - - -class TestKeypairCreate(TestKeypair): +class TestKeypairCreate(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -90,8 +79,8 @@ def test_keypair_create_no_options(self, mock_generate): public_key=mock_generate.return_value.public_key, ) - self.assertEqual({}, columns) - self.assertEqual({}, data) + self.assertEqual((), columns) + self.assertEqual((), data) def test_keypair_create_public_key(self): self.data = ( @@ -260,38 +249,44 @@ def test_keypair_create_with_key_type_pre_v22(self): def test_key_pair_create_with_user(self, mock_generate): self.set_compute_api_version('2.10') + self.user = sdk_fakes.generate_fake_resource(_user.User) + self.identity_sdk_client.find_user.return_value = self.user + arglist = [ '--user', - self._user.name, + self.user.name, self.keypair.name, ] verifylist = [ - ('user', self._user.name), + ('user', self.user.name), ('name', self.keypair.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) + self.identity_sdk_client.find_user.assert_called_once_with( + name_or_id=self.user.name, ignore_missing=False + ) self.compute_client.create_keypair.assert_called_with( name=self.keypair.name, - user_id=self._user.id, + user_id=self.user.id, public_key=mock_generate.return_value.public_key, ) - self.assertEqual({}, columns) - self.assertEqual({}, data) + self.assertEqual((), columns) + self.assertEqual((), data) def test_key_pair_create_with_user_pre_v210(self): self.set_compute_api_version('2.9') arglist = [ '--user', - self._user.name, + 'user', self.keypair.name, ] verifylist = [ - ('user', self._user.name), + ('user', 'user'), ('name', self.keypair.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -304,7 +299,7 @@ def test_key_pair_create_with_user_pre_v210(self): ) -class TestKeypairDelete(TestKeypair): +class TestKeypairDelete(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -375,9 +370,12 @@ def test_delete_multiple_keypairs_with_exception(self): def test_keypair_delete_with_user(self): self.set_compute_api_version('2.10') - arglist = ['--user', self._user.name, self.keypairs[0].name] + self.user = sdk_fakes.generate_fake_resource(_user.User) + self.identity_sdk_client.find_user.return_value = self.user + + arglist = ['--user', self.user.name, self.keypairs[0].name] verifylist = [ - ('user', self._user.name), + ('user', self.user.name), ('name', [self.keypairs[0].name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -387,16 +385,16 @@ def test_keypair_delete_with_user(self): self.assertIsNone(ret) self.compute_client.delete_keypair.assert_called_with( self.keypairs[0].name, - user_id=self._user.id, + user_id=self.user.id, ignore_missing=False, ) def test_keypair_delete_with_user_pre_v210(self): self.set_compute_api_version('2.9') - arglist = ['--user', self._user.name, self.keypairs[0].name] + arglist = ['--user', 'foo', self.keypairs[0].name] verifylist = [ - ('user', self._user.name), + ('user', 'foo'), ('name', [self.keypairs[0].name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -409,7 +407,7 @@ def test_keypair_delete_with_user_pre_v210(self): ) -class TestKeypairList(TestKeypair): +class TestKeypairList(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -421,8 +419,6 @@ def setUp(self): # Get the command object to test self.cmd = keypair.ListKeypair(self.app, None) - self._project = sdk_fakes.generate_fake_resource(_project.Project) - def test_keypair_list_no_options(self): arglist = [] verifylist = [] @@ -476,24 +472,25 @@ def test_keypair_list_v22(self): def test_keypair_list_with_user(self): self.set_compute_api_version('2.35') - users_mock = self.identity_client.users - users_mock.reset_mock() - users_mock.get.return_value = self._user + self.user = sdk_fakes.generate_fake_resource(_user.User) + self.identity_sdk_client.find_user.return_value = self.user arglist = [ '--user', - self._user.name, + self.user.name, ] verifylist = [ - ('user', self._user.name), + ('user', self.user.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - users_mock.get.assert_called_with(self._user.name) + self.identity_sdk_client.find_user.assert_called_once_with( + name_or_id=self.user.name, ignore_missing=False + ) self.compute_client.keypairs.assert_called_with( - user_id=self._user.id, + user_id=self.user.id, ) self.assertEqual(('Name', 'Fingerprint', 'Type'), columns) @@ -513,10 +510,10 @@ def test_keypair_list_with_user_pre_v210(self): arglist = [ '--user', - self._user.name, + 'foo', ] verifylist = [ - ('user', self._user.name), + ('user', 'foo'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -530,30 +527,32 @@ def test_keypair_list_with_user_pre_v210(self): def test_keypair_list_with_project(self): self.set_compute_api_version('2.35') - projects_mock = self.identity_client.projects - projects_mock.reset_mock() - projects_mock.get.return_value = self._project + self.user = sdk_fakes.generate_fake_resource(_user.User) + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project role_assignments_mock = self.identity_sdk_client.role_assignments role_assignments_mock.reset_mock() assignment = sdk_fakes.generate_fake_resource( _role_assignment.RoleAssignment ) - assignment.user = self._user + assignment.user = self.user role_assignments_mock.return_value = [assignment] - arglist = ['--project', self._project.name] - verifylist = [('project', self._project.name)] + arglist = ['--project', self.project.name] + verifylist = [('project', self.project.name)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - projects_mock.get.assert_called_with(self._project.name) + self.identity_sdk_client.find_project.assert_called_once_with( + name_or_id=self.project.name, ignore_missing=False + ) role_assignments_mock.assert_called_with( - scope_project_id=self._project.id + scope_project_id=self.project.id ) self.compute_client.keypairs.assert_called_with( - user_id=self._user.id, + user_id=self.user.id, ) self.assertEqual(('Name', 'Fingerprint', 'Type'), columns) @@ -571,8 +570,8 @@ def test_keypair_list_with_project(self): def test_keypair_list_with_project_pre_v210(self): self.set_compute_api_version('2.9') - arglist = ['--project', self._project.name] - verifylist = [('project', self._project.name)] + arglist = ['--project', 'foo'] + verifylist = [('project', 'foo')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) ex = self.assertRaises( @@ -585,9 +584,9 @@ def test_keypair_list_with_project_pre_v210(self): def test_keypair_list_conflicting_user_options(self): arglist = [ '--user', - self._user.name, + 'foo', '--project', - self._project.name, + 'bar', ] self.assertRaises( @@ -671,7 +670,7 @@ def test_keypair_list_with_marker_pre_v235(self): ) -class TestKeypairShow(TestKeypair): +class TestKeypairShow(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -740,12 +739,14 @@ def test_keypair_show_public(self): columns, data = self.cmd.take_action(parsed_args) - self.assertEqual({}, columns) - self.assertEqual({}, data) + self.assertEqual((), columns) + self.assertEqual((), data) def test_keypair_show_with_user(self): self.set_compute_api_version('2.10') + self.user = sdk_fakes.generate_fake_resource(_user.User) + self.identity_sdk_client.find_user.return_value = self.user self.keypair = sdk_fakes.generate_fake_resource(_keypair.Keypair) self.compute_client.find_keypair.return_value = self.keypair @@ -762,22 +763,24 @@ def test_keypair_show_with_user(self): arglist = [ '--user', - self._user.name, + self.user.name, self.keypair.name, ] verifylist = [ - ('user', self._user.name), + ('user', self.user.name), ('name', self.keypair.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.users_mock.get.assert_called_with(self._user.name) + self.identity_sdk_client.find_user.assert_called_once_with( + name_or_id=self.user.name, ignore_missing=False + ) self.compute_client.find_keypair.assert_called_with( self.keypair.name, ignore_missing=False, - user_id=self._user.id, + user_id=self.user.id, ) self.assertEqual(self.columns, columns) @@ -789,11 +792,11 @@ def test_keypair_show_with_user_pre_v210(self): self.keypair = sdk_fakes.generate_fake_resource(_keypair.Keypair) arglist = [ '--user', - self._user.name, + 'foo', self.keypair.name, ] verifylist = [ - ('user', self._user.name), + ('user', 'foo'), ('name', self.keypair.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/unit/compute/v2/test_server.py b/openstackclient/tests/unit/compute/v2/test_server.py index eb0b934e39..07314f7d3f 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -13,7 +13,6 @@ # under the License. import base64 -import collections import getpass import json import tempfile @@ -21,9 +20,13 @@ import uuid import iso8601 +from openstack.block_storage.v3 import snapshot as _snapshot +from openstack.block_storage.v3 import volume as _volume +from openstack.compute.v2 import flavor as _flavor from openstack.compute.v2 import server as _server from openstack.compute.v2 import server_group as _server_group from openstack import exceptions as sdk_exceptions +from openstack.image.v2 import image as _image from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions @@ -35,7 +38,6 @@ from openstackclient.tests.unit.image.v2 import fakes as image_fakes from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as test_utils -from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes class TestPowerStateColumn(test_utils.TestCase): @@ -63,7 +65,7 @@ def test_human_readable(self): self.assertEqual('N/A', server.PowerStateColumn(0x08).human_readable()) -class TestServer(compute_fakes.TestComputev2): +class TestServer(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -89,10 +91,6 @@ def setUp(self): # Get the command object to test self.cmd = server.AddFixedIP(self.app, None) - # Mock network methods - self.find_network = mock.Mock() - self.app.client_manager.network.find_network = self.find_network - def test_server_add_fixed_ip_pre_v249_with_tag(self): self.set_compute_api_version('2.48') @@ -344,61 +342,7 @@ def test_server_add_fixed_ip_with_fixed_ip_with_tag(self): ) -class TestServerAddFloatingIPCompute(compute_fakes.TestComputev2): - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - self.server = compute_fakes.create_one_server() - self.compute_client.find_server.return_value = self.server - - self.cmd = server.AddFloatingIP(self.app, None) - - def test_server_add_floating_ip_default(self): - arglist = [ - self.server.name, - '1.2.3.4', - ] - verifylist = [ - ('server', self.server.name), - ('ip_address', '1.2.3.4'), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - - self.compute_client.find_server.assert_called_once_with( - self.server.name, ignore_missing=False - ) - self.compute_client.add_floating_ip_to_server.assert_called_once_with( - self.server, '1.2.3.4', fixed_address=None - ) - - def test_server_add_floating_ip_fixed(self): - arglist = [ - '--fixed-ip-address', - '5.6.7.8', - self.server.name, - '1.2.3.4', - ] - verifylist = [ - ('fixed_ip_address', '5.6.7.8'), - ('server', self.server.name), - ('ip_address', '1.2.3.4'), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - - self.compute_client.find_server.assert_called_once_with( - self.server.name, ignore_missing=False - ) - self.compute_client.add_floating_ip_to_server.assert_called_once_with( - self.server, '1.2.3.4', fixed_address='5.6.7.8' - ) - - -class TestServerAddFloatingIPNetwork( +class TestServerAddFloatingIP( TestServer, network_fakes.TestNetworkV2, ): @@ -408,7 +352,7 @@ def setUp(self): self.server = compute_fakes.create_one_server() self.compute_client.find_server.return_value = self.server - self.network_client.update_ip = mock.Mock(return_value=None) + self.network_client.update_ip.return_value = None # Get the command object to test self.cmd = server.AddFloatingIP(self.app, None) @@ -416,8 +360,8 @@ def setUp(self): def test_server_add_floating_ip(self): _port = network_fakes.create_one_port() _floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip() - self.network_client.find_ip = mock.Mock(return_value=_floating_ip) - self.network_client.ports = mock.Mock(return_value=[_port]) + self.network_client.find_ip.return_value = _floating_ip + self.network_client.ports.return_value = [_port] arglist = [ self.server.id, _floating_ip['floating_ip_address'], @@ -448,8 +392,8 @@ def test_server_add_floating_ip(self): def test_server_add_floating_ip_no_ports(self): floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip() - self.network_client.find_ip = mock.Mock(return_value=floating_ip) - self.network_client.ports = mock.Mock(return_value=[]) + self.network_client.find_ip.return_value = floating_ip + self.network_client.ports.return_value = [] arglist = [ self.server.id, @@ -479,17 +423,17 @@ def test_server_add_floating_ip_no_ports(self): def test_server_add_floating_ip_no_external_gateway(self, success=False): _port = network_fakes.create_one_port() _floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip() - self.network_client.find_ip = mock.Mock(return_value=_floating_ip) + self.network_client.find_ip.return_value = _floating_ip return_value = [_port] # In the success case, we'll have two ports, where the first port is # not attached to an external gateway but the second port is. if success: return_value.append(_port) - self.network_client.ports = mock.Mock(return_value=return_value) + self.network_client.ports.return_value = return_value side_effect = [sdk_exceptions.NotFoundException()] if success: side_effect.append(None) - self.network_client.update_ip = mock.Mock(side_effect=side_effect) + self.network_client.update_ip.side_effect = side_effect arglist = [ self.server.id, _floating_ip['floating_ip_address'], @@ -535,8 +479,8 @@ def test_server_add_floating_ip_one_external_gateway(self): def test_server_add_floating_ip_with_fixed_ip(self): _port = network_fakes.create_one_port() _floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip() - self.network_client.find_ip = mock.Mock(return_value=_floating_ip) - self.network_client.ports = mock.Mock(return_value=[_port]) + self.network_client.find_ip.return_value = _floating_ip + self.network_client.ports.return_value = [_port] # The user has specified a fixed ip that matches one of the ports # already attached to the instance. arglist = [ @@ -575,8 +519,8 @@ def test_server_add_floating_ip_with_fixed_ip(self): def test_server_add_floating_ip_with_fixed_ip_no_port_found(self): _port = network_fakes.create_one_port() _floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip() - self.network_client.find_ip = mock.Mock(return_value=_floating_ip) - self.network_client.ports = mock.Mock(return_value=[_port]) + self.network_client.find_ip.return_value = _floating_ip + self.network_client.ports.return_value = [_port] # The user has specified a fixed ip that does not match any of the # ports already attached to the instance. nonexistent_ip = '10.0.0.9' @@ -614,9 +558,6 @@ def setUp(self): # Get the command object to test self.cmd = server.AddPort(self.app, None) - self.find_port = mock.Mock() - self.app.client_manager.network.find_port = self.find_port - def _test_server_add_port(self, port_id): servers = self.setup_sdk_servers_mock(count=1) port = 'fake-port' @@ -636,21 +577,23 @@ def _test_server_add_port(self, port_id): self.assertIsNone(result) def test_server_add_port(self): - self._test_server_add_port(self.find_port.return_value.id) - self.find_port.assert_called_once_with( + self._test_server_add_port( + self.network_client.find_port.return_value.id + ) + self.network_client.find_port.assert_called_once_with( 'fake-port', ignore_missing=False ) def test_server_add_port_no_neutron(self): self.app.client_manager.network_endpoint_enabled = False self._test_server_add_port('fake-port') - self.find_port.assert_not_called() + self.network_client.find_port.assert_not_called() def test_server_add_port_with_tag(self): self.set_compute_api_version('2.49') servers = self.setup_sdk_servers_mock(count=1) - self.find_port.return_value.id = 'fake-port' + self.network_client.find_port.return_value.id = 'fake-port' arglist = [ servers[0].id, 'fake-port', @@ -675,7 +618,7 @@ def test_server_add_port_with_tag_pre_v249(self): self.set_compute_api_version('2.48') servers = self.setup_sdk_servers_mock(count=1) - self.find_port.return_value.id = 'fake-port' + self.network_client.find_port.return_value.id = 'fake-port' arglist = [ servers[0].id, 'fake-port', @@ -704,8 +647,8 @@ def setUp(self): self.server = compute_fakes.create_one_server() self.compute_client.find_server.return_value = self.server - self.volume = volume_fakes.create_one_sdk_volume() - self.volume_sdk_client.find_volume.return_value = self.volume + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume attrs = { 'server_id': self.server.id, @@ -1025,8 +968,8 @@ def test_server_remove_volume(self): self.assertIsNone(result) self.compute_client.delete_volume_attachment.assert_called_once_with( - self.volume, self.server, + self.volume, ignore_missing=False, ) @@ -1038,9 +981,6 @@ def setUp(self): # Get the command object to test self.cmd = server.AddNetwork(self.app, None) - self.find_network = mock.Mock() - self.app.client_manager.network.find_network = self.find_network - def _test_server_add_network(self, net_id): servers = self.setup_sdk_servers_mock(count=1) network = 'fake-network' @@ -1060,21 +1000,23 @@ def _test_server_add_network(self, net_id): self.assertIsNone(result) def test_server_add_network(self): - self._test_server_add_network(self.find_network.return_value.id) - self.find_network.assert_called_once_with( + self._test_server_add_network( + self.network_client.find_network.return_value.id + ) + self.network_client.find_network.assert_called_once_with( 'fake-network', ignore_missing=False ) def test_server_add_network_no_neutron(self): self.app.client_manager.network_endpoint_enabled = False self._test_server_add_network('fake-network') - self.find_network.assert_not_called() + self.network_client.find_network.assert_not_called() def test_server_add_network_with_tag(self): self.set_compute_api_version('2.49') servers = self.setup_sdk_servers_mock(count=1) - self.find_network.return_value.id = 'fake-network' + self.network_client.find_network.return_value.id = 'fake-network' arglist = [ servers[0].id, @@ -1100,7 +1042,7 @@ def test_server_add_network_with_tag_pre_v249(self): self.set_compute_api_version('2.48') servers = self.setup_sdk_servers_mock(count=1) - self.find_network.return_value.id = 'fake-network' + self.network_client.find_network.return_value.id = 'fake-network' arglist = [ servers[0].id, @@ -1123,7 +1065,7 @@ def test_server_add_network_with_tag_pre_v249(self): ) -class TestServerAddSecurityGroup(compute_fakes.TestComputev2): +class TestServerAddSecurityGroup(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -1218,7 +1160,6 @@ class TestServerCreate(TestServer): 'locked', 'locked_reason', 'name', - 'pinned_availability_zone', 'progress', 'project_id', 'properties', @@ -1267,7 +1208,6 @@ def datalist(self): None, # locked None, # locked_reason self.server.name, - None, # pinned_availability_zone None, # progress None, # project_id format_columns.DictColumn({}), # properties @@ -1301,8 +1241,8 @@ def setUp(self): self.compute_client.create_server.return_value = self.server self.compute_client.get_server.return_value = self.server - self.volume = volume_fakes.create_one_volume() - self.snapshot = volume_fakes.create_one_snapshot() + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) # Get the command object to test self.cmd = server.CreateServer(self.app, None) @@ -1367,6 +1307,71 @@ def test_server_create_minimal(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist(), data) + def test_server_create_without_volume_endpoint(self): + self.app.client_manager.volume_endpoint_enabled = False + + arglist = [ + '--image', + self.image.id, + '--flavor', + self.flavor.id, + self.server.name, + ] + verifylist = [ + ('image', self.image.id), + ('flavor', self.flavor.id), + ('config_drive', False), + ('server_name', self.server.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.compute_client.create_server.assert_called_once_with( + name=self.server.name, + image_id=self.image.id, + flavor_id=self.flavor.id, + min_count=1, + max_count=1, + networks=[], + block_device_mapping=[ + { + 'uuid': self.image.id, + 'boot_index': 0, + 'source_type': 'image', + 'destination_type': 'local', + 'delete_on_termination': True, + }, + ], + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + + def test_server_create_volume_requires_volume_endpoint(self): + self.app.client_manager.volume_endpoint_enabled = False + + arglist = [ + '--flavor', + self.flavor.id, + '--volume', + self.volume.name, + self.server.name, + ] + verifylist = [ + ('flavor', self.flavor.id), + ('volume', self.volume.name), + ('server_name', self.server.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + self.assertIn( + 'Volume service is not available in the current cloud', str(ex) + ) + def test_server_create_with_options(self): server_group = sdk_fakes.generate_fake_resource( _server_group.ServerGroup @@ -1615,8 +1620,8 @@ def find_port(name_or_id, ignore_missing): port_port2.id: port_port2, }[name_or_id] - self.app.client_manager.network.find_network.side_effect = find_network - self.app.client_manager.network.find_port.side_effect = find_port + self.network_client.find_network.side_effect = find_network + self.network_client.find_port.side_effect = find_port arglist = [ '--image', @@ -1705,7 +1710,7 @@ def find_port(name_or_id, ignore_missing): }, { 'uuid': network_net2.id, - 'fixed': '10.0.0.2', + 'fixed_ip': '10.0.0.2', }, { 'port': port_port1.id, @@ -1735,7 +1740,7 @@ def test_server_create_with_network_tag(self): self.set_compute_api_version('2.43') network = network_fakes.create_one_network() - self.app.client_manager.network.find_network.return_value = network + self.network_client.find_network.return_value = network arglist = [ '--image', @@ -2463,7 +2468,7 @@ def test_server_create_userdata(self): self.assertEqual(self.datalist(), data) def test_server_create_with_volume(self): - self.volume_client.volumes.get.return_value = self.volume + self.volume_client.find_volume.return_value = self.volume arglist = [ '--flavor', @@ -2481,8 +2486,8 @@ def test_server_create_with_volume(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_client.volumes.get.assert_called_once_with( - self.volume.name + self.volume_client.find_volume.assert_called_once_with( + self.volume.name, ignore_missing=False ) self.compute_client.create_server.assert_called_once_with( name=self.server.name, @@ -2505,7 +2510,7 @@ def test_server_create_with_volume(self): self.assertEqual(self.datalist(), data) def test_server_create_with_snapshot(self): - self.volume_client.volume_snapshots.get.return_value = self.snapshot + self.volume_client.find_snapshot.return_value = self.snapshot arglist = [ '--flavor', @@ -2523,8 +2528,8 @@ def test_server_create_with_snapshot(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_client.volume_snapshots.get.assert_called_once_with( - self.snapshot.name + self.volume_client.find_snapshot.assert_called_once_with( + self.snapshot.name, ignore_missing=False ) self.compute_client.create_server.assert_called_once_with( name=self.server.name, @@ -2576,7 +2581,7 @@ def test_server_create_with_block_device(self): columns, data = self.cmd.take_action(parsed_args) # we don't do any validation of IDs when using the legacy option - self.volume_client.volumes.get.assert_not_called() + self.volume_client.find_volume.assert_not_called() self.compute_client.create_server.assert_called_once_with( name=self.server.name, image_id='', @@ -2600,7 +2605,7 @@ def test_server_create_with_block_device(self): def test_server_create_with_block_device_full(self): self.set_compute_api_version('2.67') - self.volume_alt = volume_fakes.create_one_volume() + self.volume_alt = sdk_fakes.generate_fake_resource(_volume.Volume) block_device = ( f'uuid={self.volume.id},source_type=volume,' f'destination_type=volume,disk_bus=ide,device_type=disk,' @@ -2654,7 +2659,7 @@ def test_server_create_with_block_device_full(self): columns, data = self.cmd.take_action(parsed_args) # we don't do any validation of IDs when using the legacy option - self.volume_client.volumes.get.assert_not_called() + self.volume_client.find_volume.assert_not_called() self.compute_client.create_server.assert_called_once_with( name=self.server.name, image_id=self.image.id, @@ -2736,7 +2741,7 @@ def test_server_create_with_block_device_from_file(self): columns, data = self.cmd.take_action(parsed_args) # we don't do any validation of IDs when using the legacy option - self.volume_client.volumes.get.assert_not_called() + self.volume_client.find_volume.assert_not_called() self.compute_client.create_server.assert_called_once_with( name=self.server.name, image_id=self.image.id, @@ -2893,7 +2898,7 @@ def test_server_create_with_block_device_volume_type_pre_v267(self): self.compute_client.create_server.assert_not_called() def test_server_create_with_block_device_mapping(self): - self.volume_client.volumes.get.return_value = self.volume + self.volume_client.find_volume.return_value = self.volume arglist = [ '--image', @@ -2926,8 +2931,8 @@ def test_server_create_with_block_device_mapping(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_client.volumes.get.assert_called_once_with( - self.volume.name + self.volume_client.find_volume.assert_called_once_with( + self.volume.name, ignore_missing=False ) self.compute_client.create_server.assert_called_once_with( name=self.server.name, @@ -2958,7 +2963,7 @@ def test_server_create_with_block_device_mapping(self): self.assertEqual(self.datalist(), data) def test_server_create_with_block_device_mapping_min_input(self): - self.volume_client.volumes.get.return_value = self.volume + self.volume_client.find_volume.return_value = self.volume arglist = [ '--image', @@ -2990,8 +2995,8 @@ def test_server_create_with_block_device_mapping_min_input(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_client.volumes.get.assert_called_once_with( - self.volume.name + self.volume_client.find_volume.assert_called_once_with( + self.volume.name, ignore_missing=False ) self.compute_client.create_server.assert_called_once_with( name=self.server.name, @@ -3021,7 +3026,7 @@ def test_server_create_with_block_device_mapping_min_input(self): self.assertEqual(self.datalist(), data) def test_server_create_with_block_device_mapping_default_input(self): - self.volume_client.volumes.get.return_value = self.volume + self.volume_client.find_volume.return_value = self.volume arglist = [ '--image', @@ -3053,8 +3058,8 @@ def test_server_create_with_block_device_mapping_default_input(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_client.volumes.get.assert_called_once_with( - self.volume.name + self.volume_client.find_volume.assert_called_once_with( + self.volume.name, ignore_missing=False ) self.compute_client.create_server.assert_called_once_with( name=self.server.name, @@ -3084,7 +3089,7 @@ def test_server_create_with_block_device_mapping_default_input(self): self.assertEqual(self.datalist(), data) def test_server_create_with_block_device_mapping_full_input(self): - self.volume_client.volumes.get.return_value = self.volume + self.volume_client.find_volume.return_value = self.volume arglist = [ '--image', @@ -3118,8 +3123,8 @@ def test_server_create_with_block_device_mapping_full_input(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_client.volumes.get.assert_called_once_with( - self.volume.name + self.volume_client.find_volume.assert_called_once_with( + self.volume.name, ignore_missing=False ) self.compute_client.create_server.assert_called_once_with( name=self.server.name, @@ -3151,8 +3156,8 @@ def test_server_create_with_block_device_mapping_full_input(self): self.assertEqual(self.datalist(), data) def test_server_create_with_block_device_mapping_snapshot(self): - self.snapshot = volume_fakes.create_one_snapshot() - self.volume_client.volume_snapshots.get.return_value = self.snapshot + self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) + self.volume_client.find_snapshot.return_value = self.snapshot arglist = [ '--image', @@ -3186,8 +3191,8 @@ def test_server_create_with_block_device_mapping_snapshot(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_client.volume_snapshots.get.assert_called_once_with( - self.snapshot.name + self.volume_client.find_snapshot.assert_called_once_with( + self.snapshot.name, ignore_missing=False ) self.compute_client.create_server.assert_called_once_with( name=self.server.name, @@ -3219,7 +3224,7 @@ def test_server_create_with_block_device_mapping_snapshot(self): self.assertEqual(self.datalist(), data) def test_server_create_with_block_device_mapping_multiple(self): - self.volume_client.volumes.get.return_value = self.volume + self.volume_client.find_volume.return_value = self.volume arglist = [ '--image', @@ -3261,8 +3266,8 @@ def test_server_create_with_block_device_mapping_multiple(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_client.volumes.get.assert_has_calls( - [mock.call(self.volume.name)] * 2 + self.volume_client.find_volume.assert_has_calls( + [mock.call(self.volume.name, ignore_missing=False)] * 2 ) self.compute_client.create_server.assert_called_once_with( name=self.server.name, @@ -4379,7 +4384,7 @@ def test_server_create_with_trusted_image_cert_boot_from_volume(self): self.compute_client.create_server.assert_not_called() -class TestServerDelete(compute_fakes.TestComputev2): +class TestServerDelete(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -4456,6 +4461,55 @@ def test_server_delete_multi_servers(self): ) self.assertIsNone(result) + def test_server_delete_multi_servers_with_exceptions(self): + servers = compute_fakes.create_servers(count=2) + self.compute_client.find_server.side_effect = [ + servers[0], + sdk_exceptions.ResourceNotFound(), + servers[1], + ] + + arglist = [servers[0].id, 'unexist_server', servers[1].id] + + verifylist = [ + ('force', False), + ('all_projects', False), + ('wait', False), + ( + 'server', + [servers[0].id, 'unexist_server', servers[1].id], + ), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertEqual('1 of 3 servers failed to delete.', str(exc)) + + self.compute_client.find_server.assert_has_calls( + [ + mock.call( + servers[0].id, ignore_missing=False, all_projects=False + ), + mock.call( + 'unexist_server', ignore_missing=False, all_projects=False + ), + mock.call( + servers[1].id, ignore_missing=False, all_projects=False + ), + ] + ) + + self.compute_client.delete_server.assert_has_calls( + [ + mock.call(servers[0], force=False), + mock.call(servers[1], force=False), + ] + ) + def test_server_delete_with_all_projects(self): arglist = [ self.server.id, @@ -4589,10 +4643,17 @@ class _TestServerList(TestServer): 'Flavor Name', 'Flavor ID', 'Availability Zone', - 'Pinned Availability Zone', 'Host', 'Properties', - 'Scheduler Hints', + ) + columns_all_projects = ( + 'ID', + 'Name', + 'Status', + 'Networks', + 'Image', + 'Flavor', + 'Project ID', ) def setUp(self): @@ -4650,17 +4711,19 @@ class TestServerList(_TestServerList): def setUp(self): super().setUp() - Image = collections.namedtuple('Image', 'id name') self.image_client.images.return_value = [ - Image(id=s.image['id'], name=self.image.name) + sdk_fakes.generate_fake_resource( + _image.Image, id=s.image['id'], name=self.image.name + ) # Image will be an empty string if boot-from-volume for s in self.servers if s.image ] - Flavor = collections.namedtuple('Flavor', 'id name') self.compute_client.flavors.return_value = [ - Flavor(id=s.flavor['id'], name=self.flavor.name) + sdk_fakes.generate_fake_resource( + _flavor.Flavor, id=s.flavor['id'], name=self.flavor.name + ) for s in self.servers ] @@ -4729,10 +4792,8 @@ def test_server_list_long_option(self): self.flavor.name, s.flavor['id'], getattr(s, 'availability_zone'), - getattr(s, 'pinned_availability_zone', ''), server.HostColumn(getattr(s, 'hypervisor_hostname')), format_columns.DictColumn(s.metadata), - format_columns.DictListColumn(None), ) for s in self.servers ) @@ -4755,6 +4816,36 @@ def test_server_list_long_option(self): self.assertEqual(self.columns_long, columns) self.assertEqual(self.data, tuple(data)) + def test_server_list_all_projects_option(self): + self.data = tuple( + ( + s.id, + s.name, + s.status, + server.AddressesColumn(s.addresses), + # Image will be an empty string if boot-from-volume + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, + self.flavor.name, + s.project_id, + ) + for s in self.servers + ) + arglist = [ + '--all-projects', + ] + verifylist = [ + ('all_projects', True), + ('long', False), + ('deleted', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.image_client.images.assert_called() + self.compute_client.flavors.assert_called() + self.assertEqual(self.columns_all_projects, columns) + self.assertEqual(self.data, tuple(data)) + def test_server_list_column_option(self): arglist = [ '-c', @@ -4776,13 +4867,9 @@ def test_server_list_column_option(self): '-c', 'Availability Zone', '-c', - 'Pinned Availability Zone', - '-c', 'Host', '-c', 'Properties', - '-c', - 'Scheduler Hints', '--long', ] verifylist = [ @@ -4790,7 +4877,7 @@ def test_server_list_column_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + columns, _data = self.cmd.take_action(parsed_args) self.compute_client.servers.assert_called_with(**self.kwargs) self.assertIn('Project ID', columns) @@ -4802,10 +4889,8 @@ def test_server_list_column_option(self): self.assertIn('Image ID', columns) self.assertIn('Flavor ID', columns) self.assertIn('Availability Zone', columns) - self.assertIn('Pinned Availability Zone', columns) self.assertIn('Host', columns) self.assertIn('Properties', columns) - self.assertIn('Scheduler Hints', columns) self.assertCountEqual(columns, set(columns)) def test_server_list_no_name_lookup_option(self): @@ -5216,10 +5301,8 @@ def test_server_list_long_with_host_status_v216(self): self.flavor.name, s.flavor['id'], getattr(s, 'availability_zone'), - getattr(s, 'pinned_availability_zone', ''), server.HostColumn(getattr(s, 'hypervisor_hostname')), format_columns.DictColumn(s.metadata), - format_columns.DictListColumn(s.scheduler_hints), ) for s in self.servers ) @@ -5248,16 +5331,17 @@ def test_server_list_long_with_host_status_v216(self): self.compute_client.servers.return_value = servers # Make sure the returned image and flavor IDs match the servers. - Image = collections.namedtuple('Image', 'id name') self.image_client.images.return_value = [ - Image(id=s.image['id'], name=self.image.name) + sdk_fakes.generate_fake_resource( + _image.Image, id=s.image['id'], name=self.image.name + ) # Image will be an empty string if boot-from-volume for s in servers if s.image ] # Add the expected host_status column and data. - columns_long = self.columns_long + ('Host Status',) + columns_long = (*self.columns_long, 'Host Status') self.data2 = tuple( ( s.id, @@ -5272,10 +5356,8 @@ def test_server_list_long_with_host_status_v216(self): self.flavor.name, s.flavor['id'], getattr(s, 'availability_zone'), - getattr(s, 'pinned_availability_zone', ''), server.HostColumn(getattr(s, 'hypervisor_hostname')), format_columns.DictColumn(s.metadata), - format_columns.DictListColumn(s.scheduler_hints), s.host_status, ) for s in servers @@ -5310,7 +5392,6 @@ class TestServerListV273(_TestServerList): 'Image ID', 'Flavor', 'Availability Zone', - 'Pinned Availability Zone', 'Host', 'Properties', 'Scheduler Hints', @@ -5335,9 +5416,10 @@ def setUp(self): self.servers = self.setup_sdk_servers_mock(3) self.compute_client.servers.return_value = self.servers - Image = collections.namedtuple('Image', 'id name') self.image_client.images.return_value = [ - Image(id=s.image['id'], name=self.image.name) + sdk_fakes.generate_fake_resource( + _image.Image, id=s.image['id'], name=self.image.name + ) # Image will be an empty string if boot-from-volume for s in self.servers if s.image @@ -5490,7 +5572,7 @@ def test_server_list_v269_with_partial_constructs(self): } fake_server = _server.Server(**server_dict) self.servers.append(fake_server) - columns, data = self.cmd.take_action(parsed_args) + _columns, data = self.cmd.take_action(parsed_args) # get the first three servers out since our interest is in the partial # server. next(data) @@ -5508,42 +5590,343 @@ def test_server_list_v269_with_partial_constructs(self): self.assertEqual(expected_row, partial_server) -class TestServerAction(compute_fakes.TestComputev2): - def run_method_with_sdk_servers(self, method_name, server_count): - servers = compute_fakes.create_servers(count=server_count) - self.compute_client.find_server.side_effect = servers - - arglist = [s.id for s in servers] - verifylist = [ - ('server', arglist), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - calls = [mock.call(s.id) for s in servers] - method = getattr(self.compute_client, method_name) - method.assert_has_calls(calls) - self.assertIsNone(result) - +class TestServerListV296(_TestServerList): + columns = ( + 'ID', + 'Name', + 'Status', + 'Networks', + 'Image', + 'Flavor', + ) + columns_long = ( + 'ID', + 'Name', + 'Status', + 'Task State', + 'Power State', + 'Networks', + 'Image Name', + 'Image ID', + 'Flavor', + 'Availability Zone', + 'Host', + 'Properties', + 'Pinned Availability Zone', + ) -class TestServerLock(TestServerAction): def setUp(self): super().setUp() + self.set_compute_api_version('2.96') - # Get the command object to test - self.cmd = server.LockServer(self.app, None) - - def test_server_lock(self): - self.run_method_with_sdk_servers('lock_server', 1) - - def test_server_lock_multi_servers(self): - self.run_method_with_sdk_servers('lock_server', 3) + self.image_client.images.return_value = [ + sdk_fakes.generate_fake_resource( + _image.Image, id=s.image['id'], name=self.image.name + ) + # Image will be an empty string if boot-from-volume + for s in self.servers + if s.image + ] - def test_server_lock_with_reason(self): - self.set_compute_api_version('2.73') + self.compute_client.flavors.return_value = [ + sdk_fakes.generate_fake_resource( + _flavor.Flavor, id=s.flavor['id'], name=self.flavor.name + ) + for s in self.servers + ] - self.server = compute_fakes.create_one_server() + self.data = tuple( + ( + s.id, + s.name, + s.status, + server.AddressesColumn(s.addresses), + # Image will be an empty string if boot-from-volume + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, + self.flavor.name, + ) + for s in self.servers + ) + + def test_server_list_long_option(self): + self.data = tuple( + ( + s.id, + s.name, + s.status, + getattr(s, 'task_state'), + server.PowerStateColumn(getattr(s, 'power_state')), + server.AddressesColumn(s.addresses), + # Image will be an empty string if boot-from-volume + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, + s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, + self.flavor.name, + getattr(s, 'availability_zone'), + server.HostColumn(getattr(s, 'hypervisor_hostname')), + format_columns.DictColumn(s.metadata), + getattr(s, 'pinned_availability_zone', ''), + ) + for s in self.servers + ) + arglist = [ + '--long', + ] + verifylist = [ + ('all_projects', False), + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.compute_client.servers.assert_called_with(**self.kwargs) + image_ids = {s.image['id'] for s in self.servers if s.image} + self.image_client.images.assert_called_once_with( + id=f'in:{",".join(image_ids)}', + ) + self.compute_client.flavors.assert_called_once_with(is_public=None) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data, tuple(data)) + + def test_server_list_column_option(self): + arglist = [ + '-c', + 'Project ID', + '-c', + 'User ID', + '-c', + 'Created At', + '-c', + 'Security Groups', + '-c', + 'Task State', + '-c', + 'Power State', + '-c', + 'Image ID', + '-c', + 'Flavor ID', + '-c', + 'Availability Zone', + '-c', + 'Host', + '-c', + 'Properties', + '-c', + 'Pinned Availability Zone', + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, _data = self.cmd.take_action(parsed_args) + + self.compute_client.servers.assert_called_with(**self.kwargs) + self.assertIn('Project ID', columns) + self.assertIn('User ID', columns) + self.assertIn('Created At', columns) + self.assertIn('Security Groups', columns) + self.assertIn('Task State', columns) + self.assertIn('Power State', columns) + self.assertIn('Image ID', columns) + self.assertIn('Flavor ID', columns) + self.assertIn('Availability Zone', columns) + self.assertIn('Pinned Availability Zone', columns) + self.assertIn('Host', columns) + self.assertIn('Properties', columns) + self.assertCountEqual(columns, set(columns)) + + +class TestServerListV2100(_TestServerList): + columns = ( + 'ID', + 'Name', + 'Status', + 'Networks', + 'Image', + 'Flavor', + ) + columns_long = ( + 'ID', + 'Name', + 'Status', + 'Task State', + 'Power State', + 'Networks', + 'Image Name', + 'Image ID', + 'Flavor', + 'Availability Zone', + 'Host', + 'Properties', + 'Pinned Availability Zone', + 'Scheduler Hints', + ) + + def setUp(self): + super().setUp() + self.set_compute_api_version('2.100') + + self.image_client.images.return_value = [ + sdk_fakes.generate_fake_resource( + _image.Image, id=s.image['id'], name=self.image.name + ) + # Image will be an empty string if boot-from-volume + for s in self.servers + if s.image + ] + + self.compute_client.flavors.return_value = [ + sdk_fakes.generate_fake_resource( + _flavor.Flavor, id=s.flavor['id'], name=self.flavor.name + ) + for s in self.servers + ] + + self.data = tuple( + ( + s.id, + s.name, + s.status, + server.AddressesColumn(s.addresses), + # Image will be an empty string if boot-from-volume + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, + self.flavor.name, + ) + for s in self.servers + ) + + def test_server_list_long_option(self): + self.data = tuple( + ( + s.id, + s.name, + s.status, + getattr(s, 'task_state'), + server.PowerStateColumn(getattr(s, 'power_state')), + server.AddressesColumn(s.addresses), + # Image will be an empty string if boot-from-volume + self.image.name if s.image else server.IMAGE_STRING_FOR_BFV, + s.image['id'] if s.image else server.IMAGE_STRING_FOR_BFV, + self.flavor.name, + getattr(s, 'availability_zone'), + server.HostColumn(getattr(s, 'hypervisor_hostname')), + format_columns.DictColumn(s.metadata), + getattr(s, 'pinned_availability_zone', ''), + format_columns.DictListColumn(None), + ) + for s in self.servers + ) + arglist = [ + '--long', + ] + verifylist = [ + ('all_projects', False), + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.compute_client.servers.assert_called_with(**self.kwargs) + image_ids = {s.image['id'] for s in self.servers if s.image} + self.image_client.images.assert_called_once_with( + id=f'in:{",".join(image_ids)}', + ) + self.compute_client.flavors.assert_called_once_with(is_public=None) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data, tuple(data)) + + def test_server_list_column_option(self): + arglist = [ + '-c', + 'Project ID', + '-c', + 'User ID', + '-c', + 'Created At', + '-c', + 'Security Groups', + '-c', + 'Task State', + '-c', + 'Power State', + '-c', + 'Image ID', + '-c', + 'Flavor ID', + '-c', + 'Availability Zone', + '-c', + 'Host', + '-c', + 'Properties', + '-c', + 'Pinned Availability Zone', + '-c', + 'Scheduler Hints', + '--long', + ] + verifylist = [ + ('long', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, _data = self.cmd.take_action(parsed_args) + + self.compute_client.servers.assert_called_with(**self.kwargs) + self.assertIn('Project ID', columns) + self.assertIn('User ID', columns) + self.assertIn('Created At', columns) + self.assertIn('Security Groups', columns) + self.assertIn('Task State', columns) + self.assertIn('Power State', columns) + self.assertIn('Image ID', columns) + self.assertIn('Flavor ID', columns) + self.assertIn('Availability Zone', columns) + self.assertIn('Pinned Availability Zone', columns) + self.assertIn('Host', columns) + self.assertIn('Properties', columns) + self.assertIn('Scheduler Hints', columns) + self.assertCountEqual(columns, set(columns)) + + +class TestServerAction(compute_fakes.TestCompute): + def run_method_with_sdk_servers(self, method_name, server_count): + servers = compute_fakes.create_servers(count=server_count) + self.compute_client.find_server.side_effect = servers + + arglist = [s.id for s in servers] + verifylist = [ + ('server', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [mock.call(s.id) for s in servers] + method = getattr(self.compute_client, method_name) + method.assert_has_calls(calls) + self.assertIsNone(result) + + +class TestServerLock(TestServerAction): + def setUp(self): + super().setUp() + + # Get the command object to test + self.cmd = server.LockServer(self.app, None) + + def test_server_lock(self): + self.run_method_with_sdk_servers('lock_server', 1) + + def test_server_lock_multi_servers(self): + self.run_method_with_sdk_servers('lock_server', 3) + + def test_server_lock_with_reason(self): + self.set_compute_api_version('2.73') + + self.server = compute_fakes.create_one_server() self.compute_client.find_server.return_value = self.server self.compute_client.lock_server.return_value = None @@ -7000,6 +7383,7 @@ def setUp(self): 'image': self.image, 'networks': {}, 'adminPass': 'passw0rd', + 'status': 'ACTIVE', } self.server = compute_fakes.create_one_server(attrs=attrs) attrs['id'] = self.server.id @@ -7137,11 +7521,38 @@ def test_evacuate_with_wait_ok(self, mock_wait_for_status): mock_wait_for_status.assert_called_once_with( self.compute_client.get_server, self.server.id, + success_status=['ACTIVE'], + callback=mock.ANY, + ) + + @mock.patch.object(common_utils, 'wait_for_status', return_value=True) + def test_evacuate_with_wait_ok_shutoff(self, mock_wait_for_status): + self.server.status = 'SHUTOFF' + self.compute_client.get_server.return_value = self.server + + args = [ + self.server.id, + '--wait', + ] + verify_args = [ + ('server', self.server.id), + ('wait', True), + ] + evac_args = { + 'host': None, + 'on_shared_storage': False, + 'admin_pass': None, + } + self._test_evacuate(args, verify_args, evac_args) + mock_wait_for_status.assert_called_once_with( + self.compute_client.get_server, + self.server.id, + success_status=['ACTIVE', 'SHUTOFF'], callback=mock.ANY, ) -class TestServerRemoveFixedIP(compute_fakes.TestComputev2): +class TestServerRemoveFixedIP(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -7170,7 +7581,7 @@ def test_server_remove_fixed_ip(self): self.assertIsNone(result) -class TestServerRescue(compute_fakes.TestComputev2): +class TestServerRescue(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -7194,7 +7605,7 @@ def test_rescue(self): self.server.id, ignore_missing=False ) self.compute_client.rescue_server.assert_called_once_with( - self.server, admin_pass=None, image_ref=None + self.server, admin_pass=None, image=None ) self.assertIsNone(result) @@ -7221,7 +7632,7 @@ def test_rescue_with_image(self): self.server.id, ignore_missing=False ) self.compute_client.rescue_server.assert_called_once_with( - self.server, admin_pass=None, image_ref=new_image.id + self.server, admin_pass=None, image=new_image.id ) self.assertIsNone(result) @@ -7244,54 +7655,23 @@ def test_rescue_with_password(self): self.server.id, ignore_missing=False ) self.compute_client.rescue_server.assert_called_once_with( - self.server, admin_pass=password, image_ref=None + self.server, admin_pass=password, image=None ) self.assertIsNone(result) -class TestServerRemoveFloatingIPCompute(compute_fakes.TestComputev2): +class TestServerRemoveFloatingIP(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network_endpoint_enabled = False - self.server = compute_fakes.create_one_server() - self.compute_client.find_server.return_value = self.server - - self.cmd = server.RemoveFloatingIP(self.app, None) - - def test_server_remove_floating_ip(self): - arglist = [ - self.server.name, - '1.2.3.4', - ] - verifylist = [ - ('server', self.server.name), - ('ip_address', '1.2.3.4'), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - - self.compute_client.find_server.assert_called_once_with( - self.server.name, ignore_missing=False - ) - self.compute_client.remove_floating_ip_from_server.assert_called_once_with( - self.server, '1.2.3.4' - ) - - -class TestServerRemoveFloatingIPNetwork(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - self.network_client.update_ip = mock.Mock(return_value=None) + self.network_client.update_ip.return_value = None # Get the command object to test self.cmd = server.RemoveFloatingIP(self.app, None) def test_server_remove_floating_ip_default(self): _floating_ip = network_fakes.FakeFloatingIP.create_one_floating_ip() - self.network_client.find_ip = mock.Mock(return_value=_floating_ip) + self.network_client.find_ip.return_value = _floating_ip arglist = [ 'fake_server', _floating_ip['ip'], @@ -7324,9 +7704,6 @@ def setUp(self): # Get the command object to test self.cmd = server.RemovePort(self.app, None) - self.find_port = mock.Mock() - self.app.client_manager.network.find_port = self.find_port - def _test_server_remove_port(self, port_id): servers = self.setup_sdk_servers_mock(count=1) port = 'fake-port' @@ -7349,15 +7726,17 @@ def _test_server_remove_port(self, port_id): self.assertIsNone(result) def test_server_remove_port(self): - self._test_server_remove_port(self.find_port.return_value.id) - self.find_port.assert_called_once_with( + self._test_server_remove_port( + self.network_client.find_port.return_value.id + ) + self.network_client.find_port.assert_called_once_with( 'fake-port', ignore_missing=False ) def test_server_remove_port_no_neutron(self): self.app.client_manager.network_endpoint_enabled = False self._test_server_remove_port('fake-port') - self.find_port.assert_not_called() + self.network_client.find_port.assert_not_called() class TestServerRemoveNetwork(TestServer): @@ -7367,16 +7746,12 @@ def setUp(self): # Get the command object to test self.cmd = server.RemoveNetwork(self.app, None) - # Set method to be tested. - self.fake_inf = mock.Mock() - - self.find_network = mock.Mock() - self.app.client_manager.network.find_network = self.find_network - self.compute_client.server_interfaces.return_value = [self.fake_inf] + self.fake_if = mock.Mock() + self.compute_client.server_interfaces.return_value = [self.fake_if] def _test_server_remove_network(self, network_id): - self.fake_inf.net_id = network_id - self.fake_inf.port_id = 'fake-port' + self.fake_if.net_id = network_id + self.fake_if.port_id = 'fake-port' servers = self.setup_sdk_servers_mock(count=1) network = 'fake-network' @@ -7401,15 +7776,17 @@ def _test_server_remove_network(self, network_id): self.assertIsNone(result) def test_server_remove_network(self): - self._test_server_remove_network(self.find_network.return_value.id) - self.find_network.assert_called_once_with( + self._test_server_remove_network( + self.network_client.find_network.return_value.id + ) + self.network_client.find_network.assert_called_once_with( 'fake-network', ignore_missing=False ) def test_server_remove_network_no_neutron(self): self.app.client_manager.network_endpoint_enabled = False self._test_server_remove_network('fake-network') - self.find_network.assert_not_called() + self.network_client.find_network.assert_not_called() class TestServerRemoveSecurityGroup(TestServer): @@ -7475,7 +7852,7 @@ def test_server_remove_security_group(self): self.assertIsNone(result) -class TestServerResize(compute_fakes.TestComputev2): +class TestServerResize(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -7500,15 +7877,13 @@ def test_server_resize_no_options(self): ('server', self.server.id), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - self.compute_client.find_server.assert_called_once_with( - self.server.id, ignore_missing=False + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, ) - self.compute_client.find_flavor.assert_not_called() - self.compute_client.resize_server.assert_not_called() - self.assertIsNone(result) def test_server_resize(self): arglist = [ @@ -7683,7 +8058,7 @@ def test_server_resize_with_wait_fails(self, mock_wait_for_status): ) -class TestServerResizeConfirm(compute_fakes.TestComputev2): +class TestServerResizeConfirm(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -7715,7 +8090,7 @@ def test_resize_confirm(self): # TODO(stephenfin): Remove in OSC 7.0 -class TestServerMigrateConfirm(compute_fakes.TestComputev2): +class TestServerMigrateConfirm(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -7753,7 +8128,7 @@ def test_migrate_confirm(self): ) -class TestServerConfirmMigration(compute_fakes.TestComputev2): +class TestServerConfirmMigration(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -7784,7 +8159,7 @@ def test_migration_confirm(self): self.assertIsNone(result) -class TestServerResizeRevert(compute_fakes.TestComputev2): +class TestServerResizeRevert(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -7816,7 +8191,7 @@ def test_resize_revert(self): # TODO(stephenfin): Remove in OSC 7.0 -class TestServerMigrateRevert(compute_fakes.TestComputev2): +class TestServerMigrateRevert(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -7854,7 +8229,7 @@ def test_migrate_revert(self): ) -class TestServerRevertMigration(compute_fakes.TestComputev2): +class TestServerRevertMigration(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -8294,6 +8669,49 @@ def test_server_set_with_hostname_pre_v290(self): exceptions.CommandError, self.cmd.take_action, parsed_args ) + def test_server_set_with_pinned_availability_zone(self): + self.set_compute_api_version('2.104') + + arglist = [ + '--pinned-availability-zone', + 'az1', + self.server.id, + ] + verifylist = [ + ('pinned_availability_zone', 'az1'), + ('server', self.server.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.compute_client.update_server.assert_called_once_with( + self.server, pinned_availability_zone='az1' + ) + self.compute_client.set_server_metadata.assert_not_called() + self.compute_client.reset_server_state.assert_not_called() + self.compute_client.change_server_password.assert_not_called() + self.compute_client.clear_server_password.assert_not_called() + self.compute_client.add_tag_to_server.assert_not_called() + self.assertIsNone(result) + + def test_server_set_with_pinned_availability_zone_pre_v2104(self): + self.set_compute_api_version('2.103') + + arglist = [ + '--pinned-availability-zone', + 'az1', + self.server.id, + ] + verifylist = [ + ('pinned_availability_zone', 'az1'), + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + class TestServerShelve(TestServer): def setUp(self): @@ -8475,7 +8893,6 @@ def setUp(self): 'locked', 'locked_reason', 'name', - 'pinned_availability_zone', 'progress', 'project_id', 'properties', @@ -8503,7 +8920,7 @@ def setUp(self): None, # OS-EXT-SRV-ATTR:user_data server.PowerStateColumn( self.server.power_state - ), # OS-EXT-STS:power_state # noqa: E501 + ), # OS-EXT-STS:power_state None, # OS-EXT-STS:task_state None, # OS-EXT-STS:vm_state None, # OS-SRV-USG:launched_at @@ -8525,7 +8942,6 @@ def setUp(self): None, # locked None, # locked_reason self.server.name, - None, # pinned_availability_zone None, # progress 'tenant-id-xxx', # project_id format_columns.DictColumn({}), # properties @@ -8921,7 +9337,7 @@ def test_server_unpause_multi_servers(self): self.run_method_with_sdk_servers('unpause_server', 3) -class TestServerUnrescue(compute_fakes.TestComputev2): +class TestServerUnrescue(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -9099,6 +9515,46 @@ def test_server_unset_with_tag_pre_v226(self): '--os-compute-api-version 2.26 or greater is required', str(ex) ) + def test_server_unset_with_pinned_availability_zone(self): + self.set_compute_api_version('2.104') + + arglist = [ + '--pinned-availability-zone', + self.server.id, + ] + verifylist = [ + ('pinned_availability_zone', True), + ('server', self.server.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.compute_client.update_server.assert_called_once_with( + self.server, pinned_availability_zone=None + ) + self.assertIsNone(result) + + def test_server_unset_with_pinned_availability_zone_pre_v2104(self): + self.set_compute_api_version('2.103') + + arglist = [ + '--pinned-availability-zone', + self.server.id, + ] + verifylist = [ + ('pinned_availability_zone', True), + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + ex = self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + self.assertIn( + '--os-compute-api-version 2.104 or greater is required', str(ex) + ) + class TestServerUnshelve(TestServer): def setUp(self): @@ -9464,7 +9920,6 @@ def test_prep_server_detail(self): 'locked': None, 'locked_reason': None, 'name': _server.name, - 'pinned_availability_zone': None, 'progress': None, 'project_id': 'tenant-id-xxx', 'properties': format_columns.DictColumn({}), @@ -9551,7 +10006,6 @@ def test_prep_server_detail_v247(self): 'locked': None, 'locked_reason': None, 'name': _server.name, - 'pinned_availability_zone': None, 'progress': None, 'project_id': 'tenant-id-xxx', 'properties': format_columns.DictColumn({}), diff --git a/openstackclient/tests/unit/compute/v2/test_server_backup.py b/openstackclient/tests/unit/compute/v2/test_server_backup.py index 0d039d2108..a66d20cac3 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_backup.py +++ b/openstackclient/tests/unit/compute/v2/test_server_backup.py @@ -22,7 +22,7 @@ from openstackclient.tests.unit.image.v2 import fakes as image_fakes -class TestServerBackupCreate(compute_fakes.TestComputev2): +class TestServerBackupCreate(compute_fakes.TestCompute): def image_columns(self, image): columnlist = ( 'id', @@ -161,9 +161,7 @@ def test_server_backup_wait_fail(self, mock_wait_for_status): @mock.patch.object(common_utils, 'wait_for_status', return_value=True) def test_server_backup_wait_ok(self, mock_wait_for_status): - self.image_client.get_image = mock.Mock( - side_effect=self.image, - ) + self.image_client.get_image.side_effect = (self.image,) arglist = [ '--name', diff --git a/openstackclient/tests/unit/compute/v2/test_server_event.py b/openstackclient/tests/unit/compute/v2/test_server_event.py index 710a82ea78..b2343faebf 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_event.py +++ b/openstackclient/tests/unit/compute/v2/test_server_event.py @@ -21,7 +21,7 @@ from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -class TestListServerEvent(compute_fakes.TestComputev2): +class TestListServerEvent(compute_fakes.TestCompute): fake_server = compute_fakes.create_one_server() fake_event = compute_fakes.create_one_server_action() @@ -365,7 +365,7 @@ def test_server_event_list_with_marker_pre_v258(self): ) -class TestShowServerEvent(compute_fakes.TestComputev2): +class TestShowServerEvent(compute_fakes.TestCompute): fake_server = compute_fakes.create_one_server() fake_event = compute_fakes.create_one_server_action() columns = ( diff --git a/openstackclient/tests/unit/compute/v2/test_server_group.py b/openstackclient/tests/unit/compute/v2/test_server_group.py index a91824a9cb..d24b5c11f1 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_group.py +++ b/openstackclient/tests/unit/compute/v2/test_server_group.py @@ -23,7 +23,7 @@ from openstackclient.tests.unit import utils as tests_utils -class TestServerGroup(compute_fakes.TestComputev2): +class TestServerGroup(compute_fakes.TestCompute): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/compute/v2/test_server_image.py b/openstackclient/tests/unit/compute/v2/test_server_image.py index 726bc81ecf..e1a08ce0e2 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_image.py +++ b/openstackclient/tests/unit/compute/v2/test_server_image.py @@ -21,7 +21,7 @@ from openstackclient.tests.unit.image.v2 import fakes as image_fakes -class TestServerImageCreate(compute_fakes.TestComputev2): +class TestServerImageCreate(compute_fakes.TestCompute): def image_columns(self, image): columnlist = ( 'id', diff --git a/openstackclient/tests/unit/compute/v2/test_server_migration.py b/openstackclient/tests/unit/compute/v2/test_server_migration.py index 5016c25720..147b0ecdc4 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_migration.py +++ b/openstackclient/tests/unit/compute/v2/test_server_migration.py @@ -10,15 +10,17 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import project as _project +from openstack.identity.v3 import user as _user +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from osc_lib import utils as common_utils from openstackclient.compute.v2 import server_migration from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestListMigration(compute_fakes.TestComputev2): +class TestListMigration(compute_fakes.TestCompute): """Test fetch all migrations.""" MIGRATION_COLUMNS = [ @@ -441,20 +443,14 @@ class TestListMigrationV280(TestListMigration): 'updated_at', ] - project = identity_fakes.FakeProject.create_one_project() - user = identity_fakes.FakeUser.create_one_user() - def setUp(self): super().setUp() - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() - - self.users_mock = self.identity_client.users - self.users_mock.reset_mock() + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.user = sdk_fakes.generate_fake_resource(_user.User) - self.projects_mock.get.return_value = self.project - self.users_mock.get.return_value = self.user + self.identity_sdk_client.find_project.return_value = self.project + self.identity_sdk_client.find_user.return_value = self.user self.set_compute_api_version('2.80') @@ -690,7 +686,7 @@ def test_get_migrations_with_project_and_user_pre_v280(self): ) -class TestServerMigrationShow(compute_fakes.TestComputev2): +class TestServerMigrationShow(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -893,7 +889,7 @@ def test_server_migration_show_invalid_id(self): ) -class TestServerMigrationAbort(compute_fakes.TestComputev2): +class TestServerMigrationAbort(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -1008,7 +1004,7 @@ def test_server_migration_abort_by_uuid_pre_v259(self): ) -class TestServerMigrationForceComplete(compute_fakes.TestComputev2): +class TestServerMigrationForceComplete(compute_fakes.TestCompute): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/compute/v2/test_server_share.py b/openstackclient/tests/unit/compute/v2/test_server_share.py new file mode 100644 index 0000000000..2777066bc7 --- /dev/null +++ b/openstackclient/tests/unit/compute/v2/test_server_share.py @@ -0,0 +1,287 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 openstackclient.compute.v2 import server_share +from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes + + +class TestServerShareList(compute_fakes.TestCompute): + def setUp(self): + super().setUp() + + self.set_compute_api_version('2.97') + + self.server = compute_fakes.create_one_server() + self.shares = compute_fakes.create_shares() + + self.compute_client.find_server.return_value = self.server + self.compute_client.share_attachments.return_value = self.shares + + self.cmd = server_share.ListServerShare(self.app, None) + + def test_server_share_list(self): + arglist = [ + self.server.id, + ] + verifylist = [ + ('server', self.server.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(("Share ID", "Status", "Tag"), columns) + self.assertEqual( + ( + ( + self.shares[0].share_id, + self.shares[0].status, + self.shares[0].tag, + ), + ( + self.shares[1].share_id, + self.shares[1].status, + self.shares[1].tag, + ), + ), + tuple(data), + ) + self.compute_client.share_attachments.assert_called_once_with( + self.server, + ) + + def test_server_share_list_pre_v297(self): + self.set_compute_api_version('2.96') + arglist = [self.server.id] + verifylist = [('server', self.server.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + +class TestServerShareShow(compute_fakes.TestCompute): + def setUp(self): + super().setUp() + + self.set_compute_api_version('2.97') + + self.server = compute_fakes.create_one_server() + self.share = compute_fakes.create_one_share() + + self.compute_client.find_server.return_value = self.server + self.compute_client.get_share_attachment.return_value = self.share + + self.shared_file_system_client = ( + self.app.client_manager.sdk_connection.shared_file_system + ) + self._manila_share_id = uuid.uuid4().hex + self.shared_file_system_client.find_share.return_value = type( + 'FakeShare', (), {'id': self._manila_share_id} + )() + + self.cmd = server_share.ShowServerShare(self.app, None) + + def test_server_share_show(self): + arglist = [ + self.server.id, + self._manila_share_id, + ] + verifylist = [ + ('server', self.server.id), + ('share', self._manila_share_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual( + ('Export Location', 'Share ID', 'Status', 'Tag', 'UUID'), columns + ) + self.assertEqual( + ( + self.share.export_location, + self.share.share_id, + self.share.status, + self.share.tag, + self.share.uuid, + ), + tuple(data), + ) + self.shared_file_system_client.find_share.assert_called_once_with( + self._manila_share_id, ignore_missing=False + ) + self.compute_client.get_share_attachment.assert_called_once_with( + self.server, self._manila_share_id + ) + + def test_server_share_show_pre_v297(self): + self.set_compute_api_version('2.96') + arglist = [self.server.id, self._manila_share_id] + verifylist = [ + ('server', self.server.id), + ('share', self._manila_share_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + +class TestServerShareCreate(compute_fakes.TestCompute): + def setUp(self): + super().setUp() + + self.set_compute_api_version('2.97') + + self.server = compute_fakes.create_one_server() + self.share = compute_fakes.create_one_share() + + self.compute_client.find_server.return_value = self.server + self.compute_client.create_share_attachment.return_value = self.share + + self.shared_file_system_client = ( + self.app.client_manager.sdk_connection.shared_file_system + ) + self._manila_share_id = uuid.uuid4().hex + self.shared_file_system_client.find_share.return_value = type( + 'FakeShare', (), {'id': self._manila_share_id} + )() + + self.cmd = server_share.AddServerShare(self.app, None) + + def test_server_share_create(self): + arglist = [ + self.server.id, + self._manila_share_id, + "--tag", + "my-tag", + ] + verifylist = [ + ('server', self.server.id), + ('share', self._manila_share_id), + ('tag', 'my-tag'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual( + ("Export Location", "Share ID", "Status", "Tag", "UUID"), columns + ) + self.assertEqual( + ( + self.share.export_location, + self.share.share_id, + self.share.status, + self.share.tag, + self.share.uuid, + ), + tuple(data), + ) + self.shared_file_system_client.find_share.assert_called_once_with( + self._manila_share_id, ignore_missing=False + ) + self.compute_client.create_share_attachment.assert_called_once_with( + self.server, self._manila_share_id, tag='my-tag' + ) + + def test_server_share_create_no_tag(self): + arglist = [ + self.server.id, + self._manila_share_id, + ] + verifylist = [ + ('server', self.server.id), + ('share', self._manila_share_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.compute_client.create_share_attachment.assert_called_once_with( + self.server, self._manila_share_id + ) + + def test_server_share_create_pre_v297(self): + self.set_compute_api_version('2.96') + arglist = [self.server.id, self._manila_share_id] + verifylist = [ + ('server', self.server.id), + ('share', self._manila_share_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + +class TestServerShareDelete(compute_fakes.TestCompute): + def setUp(self): + super().setUp() + + self.set_compute_api_version('2.97') + + self.server = compute_fakes.create_one_server() + + self.compute_client.find_server.return_value = self.server + + self.shared_file_system_client = ( + self.app.client_manager.sdk_connection.shared_file_system + ) + self._manila_share_id = uuid.uuid4().hex + self.shared_file_system_client.find_share.return_value = type( + 'FakeShare', (), {'id': self._manila_share_id} + )() + + self.cmd = server_share.RemoveServerShare(self.app, None) + + def test_server_share_delete(self): + arglist = [ + self.server.id, + self._manila_share_id, + ] + verifylist = [ + ('server', self.server.id), + ('share', self._manila_share_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.shared_file_system_client.find_share.assert_called_once_with( + self._manila_share_id, ignore_missing=False + ) + self.compute_client.delete_share_attachment.assert_called_once_with( + self.server, + self._manila_share_id, + ) + + def test_server_share_delete_pre_v297(self): + self.set_compute_api_version('2.96') + arglist = [self.server.id, self._manila_share_id] + verifylist = [ + ('server', self.server.id), + ('share', self._manila_share_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) diff --git a/openstackclient/tests/unit/compute/v2/test_server_volume.py b/openstackclient/tests/unit/compute/v2/test_server_volume.py index 9f26c6d5e3..77e3bc5881 100644 --- a/openstackclient/tests/unit/compute/v2/test_server_volume.py +++ b/openstackclient/tests/unit/compute/v2/test_server_volume.py @@ -20,7 +20,7 @@ from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -class TestServerVolumeList(compute_fakes.TestComputev2): +class TestServerVolumeList(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -220,7 +220,7 @@ def test_server_volume_list_with_attachment_ids(self): ) -class TestServerVolumeUpdate(compute_fakes.TestComputev2): +class TestServerVolumeUpdate(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -228,7 +228,7 @@ def setUp(self): self.compute_client.find_server.return_value = self.server self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.find_volume.return_value = self.volume + self.volume_client.find_volume.return_value = self.volume # Get the command object to test self.cmd = server_volume.UpdateServerVolume(self.app, None) diff --git a/openstackclient/tests/unit/compute/v2/test_service.py b/openstackclient/tests/unit/compute/v2/test_service.py index c64a2b9c8a..b9a0f83061 100644 --- a/openstackclient/tests/unit/compute/v2/test_service.py +++ b/openstackclient/tests/unit/compute/v2/test_service.py @@ -22,7 +22,7 @@ from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -class TestServiceDelete(compute_fakes.TestComputev2): +class TestServiceDelete(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -77,9 +77,7 @@ def test_multi_services_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) delete_mock_result = [None, exceptions.CommandError] - self.compute_client.delete_service = mock.Mock( - side_effect=delete_mock_result - ) + self.compute_client.delete_service.side_effect = delete_mock_result try: self.cmd.take_action(parsed_args) @@ -97,7 +95,7 @@ def test_multi_services_delete_with_exception(self): ) -class TestServiceList(compute_fakes.TestComputev2): +class TestServiceList(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -263,7 +261,7 @@ def test_service_list_with_long_option_2_11(self): self.assertEqual(expected_data, list(data)) -class TestServiceSet(compute_fakes.TestComputev2): +class TestServiceSet(compute_fakes.TestCompute): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/compute/v2/test_usage.py b/openstackclient/tests/unit/compute/v2/test_usage.py index 6169678185..33b25e369f 100644 --- a/openstackclient/tests/unit/compute/v2/test_usage.py +++ b/openstackclient/tests/unit/compute/v2/test_usage.py @@ -22,45 +22,37 @@ from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -class TestUsage(compute_fakes.TestComputev2): +class TestUsageList(compute_fakes.TestCompute): def setUp(self): super().setUp() - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.projects.return_value = [self.project] - -class TestUsageList(TestUsage): - project = sdk_fakes.generate_fake_resource(_project.Project) - # Return value of self.usage_mock.list(). - usages = [ - sdk_fakes.generate_fake_resource(_usage.Usage, project_id=project.name) - ] - - columns = ( - "Project", - "Servers", - "RAM MB-Hours", - "CPU Hours", - "Disk GB-Hours", - ) - - data = [ - ( - usage_cmds.ProjectColumn(usages[0].project_id), - usage_cmds.CountColumn(usages[0].server_usages), - usage_cmds.FloatColumn(usages[0].total_memory_mb_usage), - usage_cmds.FloatColumn(usages[0].total_vcpus_usage), - usage_cmds.FloatColumn(usages[0].total_local_gb_usage), + self.usages = [ + sdk_fakes.generate_fake_resource( + _usage.Usage, project_id=self.project.name + ) + ] + self.columns = ( + "Project", + "Servers", + "RAM MB-Hours", + "CPU Hours", + "Disk GB-Hours", ) - ] - - def setUp(self): - super().setUp() + self.data = [ + ( + usage_cmds.ProjectColumn(self.usages[0].project_id), + usage_cmds.CountColumn(self.usages[0].server_usages), + usage_cmds.FloatColumn(self.usages[0].total_memory_mb_usage), + usage_cmds.FloatColumn(self.usages[0].total_vcpus_usage), + usage_cmds.FloatColumn(self.usages[0].total_local_gb_usage), + ) + ] self.compute_client.usages.return_value = self.usages - self.projects_mock.list.return_value = [self.project] # Get the command object to test self.cmd = usage_cmds.ListUsage(self.app, None) @@ -75,7 +67,7 @@ def test_usage_list_no_options(self): columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with() + self.identity_sdk_client.projects.assert_called_once() self.assertCountEqual(self.columns, columns) self.assertCountEqual(tuple(self.data), tuple(data)) @@ -96,7 +88,7 @@ def test_usage_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with() + self.identity_sdk_client.projects.assert_called_once() self.compute_client.usages.assert_called_with( start=datetime.datetime(2016, 11, 11, 0, 0), end=datetime.datetime(2016, 12, 20, 0, 0), @@ -117,7 +109,7 @@ def test_usage_list_with_pagination(self): columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with() + self.identity_sdk_client.projects.assert_called_once() self.compute_client.usages.assert_has_calls( [mock.call(start=mock.ANY, end=mock.ANY, detailed=True)] ) @@ -125,35 +117,34 @@ def test_usage_list_with_pagination(self): self.assertCountEqual(tuple(self.data), tuple(data)) -class TestUsageShow(TestUsage): - project = sdk_fakes.generate_fake_resource(_project.Project) +class TestUsageShow(compute_fakes.TestCompute): # Return value of self.usage_mock.list(). - usage = sdk_fakes.generate_fake_resource( - _usage.Usage, project_id=project.name - ) - - columns = ( - 'Project', - 'Servers', - 'RAM MB-Hours', - 'CPU Hours', - 'Disk GB-Hours', - ) - - data = ( - usage_cmds.ProjectColumn(usage.project_id), - usage_cmds.CountColumn(usage.server_usages), - usage_cmds.FloatColumn(usage.total_memory_mb_usage), - usage_cmds.FloatColumn(usage.total_vcpus_usage), - usage_cmds.FloatColumn(usage.total_local_gb_usage), - ) - def setUp(self): super().setUp() + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + + self.usage = sdk_fakes.generate_fake_resource( + _usage.Usage, project_id=self.project.name + ) self.compute_client.get_usage.return_value = self.usage - self.projects_mock.get.return_value = self.project + self.columns = ( + 'Project', + 'Servers', + 'RAM MB-Hours', + 'CPU Hours', + 'Disk GB-Hours', + ) + self.data = ( + usage_cmds.ProjectColumn(self.usage.project_id), + usage_cmds.CountColumn(self.usage.server_usages), + usage_cmds.FloatColumn(self.usage.total_memory_mb_usage), + usage_cmds.FloatColumn(self.usage.total_vcpus_usage), + usage_cmds.FloatColumn(self.usage.total_local_gb_usage), + ) + # Get the command object to test self.cmd = usage_cmds.ShowUsage(self.app, None) diff --git a/openstackclient/tests/unit/fakes.py b/openstackclient/tests/unit/fakes.py index 0d378ff8dc..be9d7f218d 100644 --- a/openstackclient/tests/unit/fakes.py +++ b/openstackclient/tests/unit/fakes.py @@ -12,13 +12,43 @@ # License for the specific language governing permissions and limitations # under the License. +# TODO(stephenfin): Remove the contents of this module in favour of the osc_lib +# version once our min version is bumped to 4.3.0 + import json -import sys from unittest import mock from keystoneauth1 import fixture +from osc_lib.tests.fakes import ( + FakeApp, + FakeClientManager as BaseFakeClientManager, + FakeLog, + FakeOptions, + FakeResource as BaseFakeResource, + FakeStdout, +) import requests +__all__ = [ + 'AUTH_TOKEN', + 'AUTH_URL', + 'INTERFACE', + 'PASSWORD', + 'PROJECT_NAME', + 'REGION_NAME', + 'TEST_RESPONSE_DICT', + 'TEST_RESPONSE_DICT_V3', + 'TEST_VERSIONS', + 'USERNAME', + 'VERSION', + 'FakeApp', + 'FakeClientManager', + 'FakeLog', + 'FakeOptions', + 'FakeResource', + 'FakeResponse', + 'FakeStdout', +] AUTH_TOKEN = "foobar" AUTH_URL = "http://0.0.0.0" @@ -47,79 +77,15 @@ TEST_VERSIONS = fixture.DiscoveryList(href=AUTH_URL) -class FakeStdout: - def __init__(self): - self.content = [] - - def write(self, text): - self.content.append(text) - - def make_string(self): - result = '' - for line in self.content: - result = result + line - return result - - -class FakeLog: - def __init__(self): - self.messages = {} - - def debug(self, msg): - self.messages['debug'] = msg - - def info(self, msg): - self.messages['info'] = msg - - def warning(self, msg): - self.messages['warning'] = msg - - def error(self, msg): - self.messages['error'] = msg - - def critical(self, msg): - self.messages['critical'] = msg - - -class FakeApp: - def __init__(self, _stdout, _log): - self.stdout = _stdout - self.client_manager = None - self.api_version = {} - self.stdin = sys.stdin - self.stdout = _stdout or sys.stdout - self.stderr = sys.stderr - self.log = _log - - -class FakeOptions: - def __init__(self, **kwargs): - self.os_beta_command = False - - -class FakeClient: - def __init__(self, **kwargs): - self.endpoint = kwargs['endpoint'] - self.token = kwargs['token'] - - -class FakeClientManager: +class FakeClientManager(BaseFakeClientManager): _api_version = { 'image': '2', } def __init__(self): - self.compute = None - self.identity = None - self.image = None - self.object_store = None - self.volume = None - self.network = None - self.sdk_connection = mock.Mock() + super().__init__() - self.session = None - self.auth_ref = None - self.auth_plugin_name = None + self.sdk_connection = mock.Mock() self.network_endpoint_enabled = True self.compute_endpoint_enabled = True @@ -158,64 +124,7 @@ def is_volume_endpoint_enabled(self, client=None): return self.volume_endpoint_enabled -class FakeModule: - def __init__(self, name, version): - self.name = name - self.__version__ = version - # Workaround for openstacksdk case - self.version = mock.Mock() - self.version.__version__ = version - - -class FakeResource: - def __init__(self, manager=None, info=None, loaded=False, methods=None): - """Set attributes and methods for a resource. - - :param manager: - The resource manager - :param Dictionary info: - A dictionary with all attributes - :param bool loaded: - True if the resource is loaded in memory - :param Dictionary methods: - A dictionary with all methods - """ - info = info or {} - methods = methods or {} - - self.__name__ = type(self).__name__ - self.manager = manager - self._info = info - self._add_details(info) - self._add_methods(methods) - self._loaded = loaded - - def _add_details(self, info): - for k, v in info.items(): - setattr(self, k, v) - - def _add_methods(self, methods): - """Fake methods with MagicMock objects. - - For each <@key, @value> pairs in methods, add an callable MagicMock - object named @key as an attribute, and set the mock's return_value to - @value. When users access the attribute with (), @value will be - returned, which looks like a function call. - """ - for name, ret in methods.items(): - method = mock.Mock(return_value=ret) - setattr(self, name, method) - - def __repr__(self): - reprkeys = sorted( - k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager' - ) - info = ", ".join(f"{k}={getattr(self, k)}" for k in reprkeys) - return f"<{self.__class__.__name__} {info}>" - - def keys(self): - return self._info.keys() - +class FakeResource(BaseFakeResource): def to_dict(self): return self._info @@ -247,11 +156,3 @@ def __init__( self._content = json.dumps(data) if not isinstance(self._content, bytes): self._content = self._content.encode() - - -class FakeModel(dict): - def __getattr__(self, key): - try: - return self[key] - except KeyError: - raise AttributeError(key) diff --git a/openstackclient/tests/unit/identity/test_common.py b/openstackclient/tests/unit/identity/test_common.py new file mode 100644 index 0000000000..ae85262dc0 --- /dev/null +++ b/openstackclient/tests/unit/identity/test_common.py @@ -0,0 +1,100 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from openstack import exceptions as sdk_exc +from openstack.identity.v3 import user as _user +from openstack.test import fakes as sdk_fakes +from osc_lib import exceptions + +from openstackclient.identity import common +from openstackclient.tests.unit import utils as test_utils + + +class TestFindSDKId(test_utils.TestCase): + def setUp(self): + super().setUp() + self.user = sdk_fakes.generate_fake_resource(_user.User) + self.identity_sdk_client = mock.Mock() + self.identity_sdk_client.find_user = mock.Mock() + + def test_find_sdk_id_validate(self): + self.identity_sdk_client.find_user.side_effect = [self.user] + + result = common._find_sdk_id( + self.identity_sdk_client.find_user, + name_or_id=self.user.id, + validate_actor_existence=True, + ) + self.assertEqual(self.user.id, result) + + def test_find_sdk_id_no_validate(self): + self.identity_sdk_client.find_user.side_effect = [self.user] + + result = common._find_sdk_id( + self.identity_sdk_client.find_user, + name_or_id=self.user.id, + validate_actor_existence=False, + ) + self.assertEqual(self.user.id, result) + + def test_find_sdk_id_not_found_validate(self): + self.identity_sdk_client.find_user.side_effect = [ + sdk_exc.ResourceNotFound, + ] + + self.assertRaises( + exceptions.CommandError, + common._find_sdk_id, + self.identity_sdk_client.find_user, + name_or_id=self.user.id, + validate_actor_existence=True, + ) + + def test_find_sdk_id_not_found_no_validate(self): + self.identity_sdk_client.find_user.side_effect = [ + sdk_exc.ResourceNotFound, + ] + + result = common._find_sdk_id( + self.identity_sdk_client.find_user, + name_or_id=self.user.id, + validate_actor_existence=False, + ) + self.assertEqual(self.user.id, result) + + def test_find_sdk_id_forbidden_validate(self): + self.identity_sdk_client.find_user.side_effect = [ + sdk_exc.ForbiddenException, + ] + + result = common._find_sdk_id( + self.identity_sdk_client.find_user, + name_or_id=self.user.id, + validate_actor_existence=True, + ) + + self.assertEqual(self.user.id, result) + + def test_find_sdk_id_forbidden_no_validate(self): + self.identity_sdk_client.find_user.side_effect = [ + sdk_exc.ForbiddenException, + ] + + result = common._find_sdk_id( + self.identity_sdk_client.find_user, + name_or_id=self.user.id, + validate_actor_existence=False, + ) + + self.assertEqual(self.user.id, result) diff --git a/openstackclient/tests/unit/identity/v2_0/fakes.py b/openstackclient/tests/unit/identity/v2_0/fakes.py index c5089d48b5..898357bfb1 100644 --- a/openstackclient/tests/unit/identity/v2_0/fakes.py +++ b/openstackclient/tests/unit/identity/v2_0/fakes.py @@ -19,7 +19,7 @@ from keystoneauth1 import access from keystoneauth1 import fixture -from openstack.identity.v2 import _proxy +from openstack.identity import v2 as identity_v2 from openstackclient.tests.unit import fakes from openstackclient.tests.unit import utils @@ -155,7 +155,7 @@ def fake_auth_ref(fake_token, fake_service=None): return auth_ref -class FakeIdentityv2Client: +class FakeIdentityClient: def __init__(self, **kwargs): self.roles = mock.Mock() self.roles.resource_class = fakes.FakeResource(None, {}) @@ -167,14 +167,10 @@ def __init__(self, **kwargs): self.tokens.resource_class = fakes.FakeResource(None, {}) self.users = mock.Mock() self.users.resource_class = fakes.FakeResource(None, {}) - self.ec2 = mock.Mock() - self.ec2.resource_class = fakes.FakeResource(None, {}) self.endpoints = mock.Mock() self.endpoints.resource_class = fakes.FakeResource(None, {}) self.extensions = mock.Mock() self.extensions.resource_class = fakes.FakeResource(None, {}) - self.auth_token = kwargs['token'] - self.management_url = kwargs['endpoint'] def __getattr__(self, name): # Map v3 'projects' back to v2 'tenants' @@ -188,7 +184,7 @@ class FakeClientMixin: def setUp(self): super().setUp() - self.app.client_manager.identity = FakeIdentityv2Client( + self.app.client_manager.identity = FakeIdentityClient( endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) @@ -197,58 +193,19 @@ def setUp(self): # TODO(stephenfin): Rename to 'identity_client' once all commands are # migrated to SDK self.app.client_manager.sdk_connection.identity = mock.Mock( - _proxy.Proxy + identity_v2.Proxy ) self.identity_sdk_client = ( self.app.client_manager.sdk_connection.identity ) -class TestIdentityv2( +class TestIdentity( FakeClientMixin, utils.TestCommand, ): ... -class FakeExtension: - """Fake one or more extension.""" - - @staticmethod - def create_one_extension(attrs=None): - """Create a fake extension. - - :param Dictionary attrs: - A dictionary with all attributes - :return: - A FakeResource object with name, namespace, etc. - """ - attrs = attrs or {} - - # Set default attributes. - extension_info = { - 'name': 'name-' + uuid.uuid4().hex, - 'namespace': ( - 'http://docs.openstack.org/identity/api/ext/OS-KSCRUD/v1.0' - ), - 'description': 'description-' + uuid.uuid4().hex, - 'updated': '2013-07-07T12:00:0-00:00', - 'alias': 'OS-KSCRUD', - 'links': ( - '[{"href":' - '"https://github.com/openstack/identity-api", "type":' - ' "text/html", "rel": "describedby"}]' - ), - } - - # Overwrite default attributes. - extension_info.update(attrs) - - extension = fakes.FakeResource( - info=copy.deepcopy(extension_info), loaded=True - ) - return extension - - class FakeCatalog: """Fake one or more catalog.""" diff --git a/openstackclient/tests/unit/identity/v2_0/test_endpoint.py b/openstackclient/tests/unit/identity/v2_0/test_endpoint.py index 0bc82bc7ef..7a9bc02975 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_endpoint.py +++ b/openstackclient/tests/unit/identity/v2_0/test_endpoint.py @@ -15,7 +15,7 @@ from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes -class TestEndpoint(identity_fakes.TestIdentityv2): +class TestEndpoint(identity_fakes.TestIdentity): fake_service = identity_fakes.FakeService.create_one_service() attr = { 'service_name': fake_service.name, diff --git a/openstackclient/tests/unit/identity/v2_0/test_project.py b/openstackclient/tests/unit/identity/v2_0/test_project.py index 9b203b22b3..b1b35d978b 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_project.py +++ b/openstackclient/tests/unit/identity/v2_0/test_project.py @@ -24,7 +24,7 @@ from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes -class TestProject(identity_fakes.TestIdentityv2): +class TestProject(identity_fakes.TestIdentity): fake_project = identity_fakes.FakeProject.create_one_project() fake_projects = identity_fakes.FakeProject.create_projects() @@ -195,7 +195,7 @@ def test_project_create_property(self): self.fake_project.name, ] verifylist = [ - ('property', {'fee': 'fi', 'fo': 'fum'}), + ('properties', {'fee': 'fi', 'fo': 'fum'}), ('name', self.fake_project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -464,7 +464,7 @@ def test_project_set_unexist_project(self): ('description', None), ('enable', False), ('disable', False), - ('property', None), + ('properties', None), ] self.projects_mock.get.side_effect = exceptions.NotFound(None) self.projects_mock.find.side_effect = exceptions.NotFound(None) @@ -588,7 +588,7 @@ def test_project_set_property(self): self.fake_project.name, ] verifylist = [ - ('property', {'fee': 'fi', 'fo': 'fum'}), + ('properties', {'fee': 'fi', 'fo': 'fum'}), ('project', self.fake_project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -683,7 +683,7 @@ def test_project_unset_key(self): self.fake_proj.name, ] verifylist = [ - ('property', ['fee', 'fo']), + ('properties', ['fee', 'fo']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/unit/identity/v2_0/test_role.py b/openstackclient/tests/unit/identity/v2_0/test_role.py index 117d2f0011..6eeb4320b1 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_role.py +++ b/openstackclient/tests/unit/identity/v2_0/test_role.py @@ -23,7 +23,7 @@ from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes -class TestRole(identity_fakes.TestIdentityv2): +class TestRole(identity_fakes.TestIdentity): attr = {} attr['endpoints'] = [ { diff --git a/openstackclient/tests/unit/identity/v2_0/test_role_assignment.py b/openstackclient/tests/unit/identity/v2_0/test_role_assignment.py index 741b3ee193..658f094c14 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_role_assignment.py +++ b/openstackclient/tests/unit/identity/v2_0/test_role_assignment.py @@ -21,7 +21,7 @@ from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes -class TestRoleAssignment(identity_fakes.TestIdentityv2): +class TestRoleAssignment(identity_fakes.TestIdentity): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/identity/v2_0/test_service.py b/openstackclient/tests/unit/identity/v2_0/test_service.py index 22e7b837af..218e713af7 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_service.py +++ b/openstackclient/tests/unit/identity/v2_0/test_service.py @@ -20,7 +20,7 @@ from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes -class TestService(identity_fakes.TestIdentityv2): +class TestService(identity_fakes.TestIdentity): fake_service = identity_fakes.FakeService.create_one_service() def setUp(self): diff --git a/openstackclient/tests/unit/identity/v2_0/test_token.py b/openstackclient/tests/unit/identity/v2_0/test_token.py index 56a5a2c458..bbd4c79f32 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_token.py +++ b/openstackclient/tests/unit/identity/v2_0/test_token.py @@ -16,7 +16,7 @@ from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes -class TestTokenIssue(identity_fakes.TestIdentityv2): +class TestTokenIssue(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -77,7 +77,7 @@ def test_token_issue_with_unscoped_token(self): self.assertEqual(datalist, data) -class TestTokenRevoke(identity_fakes.TestIdentityv2): +class TestTokenRevoke(identity_fakes.TestIdentity): TOKEN = 'fob' def setUp(self): diff --git a/openstackclient/tests/unit/identity/v2_0/test_user.py b/openstackclient/tests/unit/identity/v2_0/test_user.py index 725a821800..758a970881 100644 --- a/openstackclient/tests/unit/identity/v2_0/test_user.py +++ b/openstackclient/tests/unit/identity/v2_0/test_user.py @@ -23,7 +23,7 @@ from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes -class TestUser(identity_fakes.TestIdentityv2): +class TestUser(identity_fakes.TestIdentity): fake_project = identity_fakes.FakeProject.create_one_project() attr = { 'tenantId': fake_project.id, diff --git a/openstackclient/tests/unit/identity/v3/fakes.py b/openstackclient/tests/unit/identity/v3/fakes.py index ad5ceb2840..0e00911e19 100644 --- a/openstackclient/tests/unit/identity/v3/fakes.py +++ b/openstackclient/tests/unit/identity/v3/fakes.py @@ -20,7 +20,7 @@ from keystoneauth1 import access from keystoneauth1 import fixture -from openstack.identity.v3 import _proxy +from openstack.identity import v3 as identity_v3 from osc_lib.cli import format_columns from openstackclient.tests.unit import fakes @@ -578,12 +578,10 @@ def __init__(self, **kwargs): self.auth = FakeAuth() -class FakeIdentityv3Client: +class FakeIdentityClient: def __init__(self, **kwargs): self.domains = mock.Mock() self.domains.resource_class = fakes.FakeResource(None, {}) - self.credentials = mock.Mock() - self.credentials.resource_class = fakes.FakeResource(None, {}) self.endpoints = mock.Mock() self.endpoints.resource_class = fakes.FakeResource(None, {}) self.endpoint_filter = mock.Mock() @@ -596,24 +594,14 @@ def __init__(self, **kwargs): self.oauth1.resource_class = fakes.FakeResource(None, {}) self.projects = mock.Mock() self.projects.resource_class = fakes.FakeResource(None, {}) - self.regions = mock.Mock() - self.regions.resource_class = fakes.FakeResource(None, {}) self.roles = mock.Mock() self.roles.resource_class = fakes.FakeResource(None, {}) self.services = mock.Mock() self.services.resource_class = fakes.FakeResource(None, {}) - self.session = mock.Mock() - self.session.auth.auth_ref.service_catalog.resource_class = ( - fakes.FakeResource(None, {}) - ) self.tokens = mock.Mock() self.tokens.resource_class = fakes.FakeResource(None, {}) - self.trusts = mock.Mock() - self.trusts.resource_class = fakes.FakeResource(None, {}) self.users = mock.Mock() self.users.resource_class = fakes.FakeResource(None, {}) - self.role_assignments = mock.Mock() - self.role_assignments.resource_class = fakes.FakeResource(None, {}) self.auth_token = kwargs['token'] self.management_url = kwargs['endpoint'] self.auth = FakeAuth() @@ -623,55 +611,34 @@ def __init__(self, **kwargs): self.application_credentials.resource_class = fakes.FakeResource( None, {} ) - self.access_rules = mock.Mock() - self.access_rules.resource_class = fakes.FakeResource(None, {}) self.inference_rules = mock.Mock() self.inference_rules.resource_class = fakes.FakeResource(None, {}) - self.registered_limits = mock.Mock() - self.registered_limits.resource_class = fakes.FakeResource(None, {}) - self.limits = mock.Mock() - self.limits.resource_class = fakes.FakeResource(None, {}) class FakeFederationManager: def __init__(self, **kwargs): - self.identity_providers = mock.Mock() - self.identity_providers.resource_class = fakes.FakeResource(None, {}) - self.mappings = mock.Mock() - self.mappings.resource_class = fakes.FakeResource(None, {}) - self.protocols = mock.Mock() - self.protocols.resource_class = fakes.FakeResource(None, {}) self.projects = mock.Mock() self.projects.resource_class = fakes.FakeResource(None, {}) self.domains = mock.Mock() self.domains.resource_class = fakes.FakeResource(None, {}) - self.service_providers = mock.Mock() - self.service_providers.resource_class = fakes.FakeResource(None, {}) -class FakeFederatedClient(FakeIdentityv3Client): +class FakeFederatedClient(FakeIdentityClient): def __init__(self, **kwargs): super().__init__(**kwargs) self.federation = FakeFederationManager() -class FakeOAuth1Client(FakeIdentityv3Client): +class FakeOAuth1Client(FakeIdentityClient): def __init__(self, **kwargs): super().__init__(**kwargs) - self.access_tokens = mock.Mock() - self.access_tokens.resource_class = fakes.FakeResource(None, {}) - self.consumers = mock.Mock() - self.consumers.resource_class = fakes.FakeResource(None, {}) - self.request_tokens = mock.Mock() - self.request_tokens.resource_class = fakes.FakeResource(None, {}) - class FakeClientMixin: def setUp(self): super().setUp() - self.app.client_manager.identity = FakeIdentityv3Client( + self.app.client_manager.identity = FakeIdentityClient( endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN, ) @@ -680,19 +647,28 @@ def setUp(self): # TODO(stephenfin): Rename to 'identity_client' once all commands are # migrated to SDK self.app.client_manager.sdk_connection.identity = mock.Mock( - _proxy.Proxy + identity_v3.Proxy ) + self.app.client_manager.sdk_connection.identity.api_version = '3' self.identity_sdk_client = ( self.app.client_manager.sdk_connection.identity ) -class TestIdentityv3( +class TestIdentity( FakeClientMixin, utils.TestCommand, ): ... +class FakeModel(dict): + def __getattr__(self, key): + try: + return self[key] + except KeyError: + raise AttributeError(key) + + # We don't use FakeClientMixin since we want a different fake legacy client class TestFederatedIdentity(utils.TestCommand): def setUp(self): @@ -706,8 +682,9 @@ def setUp(self): # TODO(stephenfin): Rename to 'identity_client' once all commands are # migrated to SDK self.app.client_manager.sdk_connection.identity = mock.Mock( - _proxy.Proxy + identity_v3.Proxy ) + self.app.client_manager.sdk_connection.identity.api_version = '3' self.identity_sdk_client = ( self.app.client_manager.sdk_connection.identity ) @@ -726,8 +703,9 @@ def setUp(self): # TODO(stephenfin): Rename to 'identity_client' once all commands are # migrated to SDK self.app.client_manager.sdk_connection.identity = mock.Mock( - _proxy.Proxy + identity_v3.Proxy ) + self.app.client_manager.sdk_connection.identity.api_version = '3' self.identity_sdk_client = ( self.app.client_manager.sdk_connection.identity ) @@ -1075,7 +1053,7 @@ def create_one_endpoint_filter(attrs=None): # Overwrite default attributes if there are some attributes set endpoint_filter_info.update(attrs) - endpoint_filter = fakes.FakeModel(copy.deepcopy(endpoint_filter_info)) + endpoint_filter = FakeModel(copy.deepcopy(endpoint_filter_info)) return endpoint_filter @@ -1133,7 +1111,7 @@ def create_one_endpointgroup_filter(attrs=None): # Overwrite default attributes if there are some attributes set endpointgroup_filter_info.update(attrs) - endpointgroup_filter = fakes.FakeModel( + endpointgroup_filter = FakeModel( copy.deepcopy(endpointgroup_filter_info) ) diff --git a/openstackclient/tests/unit/identity/v3/test_access_rule.py b/openstackclient/tests/unit/identity/v3/test_access_rule.py index 0fc68dd366..6ef2d5869e 100644 --- a/openstackclient/tests/unit/identity/v3/test_access_rule.py +++ b/openstackclient/tests/unit/identity/v3/test_access_rule.py @@ -24,7 +24,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestAccessRuleDelete(identity_fakes.TestIdentityv3): +class TestAccessRuleDelete(identity_fakes.TestIdentity): access_rule = sdk_fakes.generate_fake_resource(_access_rule.AccessRule) def setUp(self): @@ -93,7 +93,7 @@ def test_delete_multi_access_rules_with_exception(self): ) -class TestAccessRuleList(identity_fakes.TestIdentityv3): +class TestAccessRuleList(identity_fakes.TestIdentity): access_rule = sdk_fakes.generate_fake_resource(_access_rule.AccessRule) def setUp(self): @@ -129,7 +129,7 @@ def test_access_rule_list(self): self.assertEqual(datalist, tuple(data)) -class TestAccessRuleShow(identity_fakes.TestIdentityv3): +class TestAccessRuleShow(identity_fakes.TestIdentity): access_rule = sdk_fakes.generate_fake_resource(_access_rule.AccessRule) def setUp(self): diff --git a/openstackclient/tests/unit/identity/v3/test_application_credential.py b/openstackclient/tests/unit/identity/v3/test_application_credential.py index a4ecec5dc4..31b2b1782b 100644 --- a/openstackclient/tests/unit/identity/v3/test_application_credential.py +++ b/openstackclient/tests/unit/identity/v3/test_application_credential.py @@ -30,19 +30,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestApplicationCredentialCreate(identity_fakes.TestIdentityv3): - columns = ( - 'id', - 'name', - 'description', - 'project_id', - 'roles', - 'unrestricted', - 'access_rules', - 'expires_at', - 'secret', - ) - +class TestApplicationCredentialCreate(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -52,12 +40,25 @@ def setUp(self): roles=[], ) - self.datalist = ( + self.columns = ( + 'ID', + 'Name', + 'Description', + 'Project ID', + 'Roles', + 'Unrestricted', + 'Access Rules', + 'Expires At', + 'Secret', + ) + self.data = ( self.application_credential.id, self.application_credential.name, self.application_credential.description, self.application_credential.project_id, - self.application_credential.roles, + application_credential.RolesColumn( + self.application_credential.roles + ), self.application_credential.unrestricted, self.application_credential.access_rules, self.application_credential.expires_at, @@ -101,7 +102,7 @@ def test_application_credential_create_basic(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertEqual(self.data, data) def test_application_credential_create_with_options(self): name = self.application_credential.name @@ -119,7 +120,7 @@ def test_application_credential_create_with_options(self): verifylist = [ ('name', self.application_credential.name), ('secret', 'moresecuresecret'), - ('role', [self.roles.id]), + ('roles', [self.roles.id]), ('expiration', '2024-01-01T00:00:00'), ('description', 'credential for testing'), ] @@ -147,7 +148,7 @@ def test_application_credential_create_with_options(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertEqual(self.data, data) def test_application_credential_create_with_access_rules_string(self): name = self.application_credential.name @@ -191,7 +192,7 @@ def test_application_credential_create_with_access_rules_string(self): ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertEqual(self.data, data) @mock.patch('openstackclient.identity.v3.application_credential.json.load') @mock.patch('openstackclient.identity.v3.application_credential.open') @@ -231,10 +232,10 @@ def test_application_credential_create_with_access_rules_file( ) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertEqual(self.data, data) -class TestApplicationCredentialDelete(identity_fakes.TestIdentityv3): +class TestApplicationCredentialDelete(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -301,7 +302,7 @@ def test_delete_multi_app_creds_with_exception(self): calls = [] for a in arglist: - calls.append(call(user_id, a)) + calls.append(call(user_id, a, ignore_missing=False)) self.identity_sdk_client.find_application_credential.assert_has_calls( calls @@ -315,7 +316,7 @@ def test_delete_multi_app_creds_with_exception(self): ) -class TestApplicationCredentialList(identity_fakes.TestIdentityv3): +class TestApplicationCredentialList(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -345,7 +346,9 @@ def setUp(self): self.application_credential.name, self.application_credential.description, self.application_credential.project_id, - '', + application_credential.RolesColumn( + self.application_credential.roles + ), self.application_credential.unrestricted, self.application_credential.access_rules, self.application_credential.expires_at, @@ -396,7 +399,7 @@ def test_application_credential_list_user(self): ) -class TestApplicationCredentialShow(identity_fakes.TestIdentityv3): +class TestApplicationCredentialShow(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -408,6 +411,29 @@ def setUp(self): self.application_credential ) + self.columns = ( + 'ID', + 'Name', + 'Description', + 'Project ID', + 'Roles', + 'Unrestricted', + 'Access Rules', + 'Expires At', + ) + self.data = ( + self.application_credential.id, + self.application_credential.name, + self.application_credential.description, + self.application_credential.project_id, + application_credential.RolesColumn( + self.application_credential.roles + ), + self.application_credential.unrestricted, + self.application_credential.access_rules, + self.application_credential.expires_at, + ) + # Get the command object to test self.cmd = application_credential.ShowApplicationCredential( self.app, None @@ -431,28 +457,8 @@ def test_application_credential_show(self): columns, data = self.cmd.take_action(parsed_args) self.identity_sdk_client.find_application_credential.assert_called_with( - user_id, self.application_credential.id + user_id, self.application_credential.id, ignore_missing=False ) - collist = ( - 'id', - 'name', - 'description', - 'project_id', - 'roles', - 'unrestricted', - 'access_rules', - 'expires_at', - ) - self.assertEqual(collist, columns) - datalist = ( - self.application_credential.id, - self.application_credential.name, - self.application_credential.description, - self.application_credential.project_id, - self.application_credential.roles, - self.application_credential.unrestricted, - self.application_credential.access_rules, - self.application_credential.expires_at, - ) - self.assertEqual(datalist, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/identity/v3/test_credential.py b/openstackclient/tests/unit/identity/v3/test_credential.py index f21fd71a0f..eaf8662f04 100644 --- a/openstackclient/tests/unit/identity/v3/test_credential.py +++ b/openstackclient/tests/unit/identity/v3/test_credential.py @@ -24,7 +24,7 @@ from openstackclient.tests.unit import utils -class TestCredentialCreate(identity_fakes.TestIdentityv3): +class TestCredentialCreate(identity_fakes.TestIdentity): user = sdk_fakes.generate_fake_resource(_user.User) project = sdk_fakes.generate_fake_resource(_project.Project) columns = ( @@ -118,7 +118,7 @@ def test_credential_create_with_options(self): self.assertEqual(self.data, data) -class TestCredentialDelete(identity_fakes.TestIdentityv3): +class TestCredentialDelete(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -196,7 +196,7 @@ def test_credential_multi_delete_with_exception(self): ) -class TestCredentialList(identity_fakes.TestIdentityv3): +class TestCredentialList(identity_fakes.TestIdentity): credential = sdk_fakes.generate_fake_resource(_credential.Credential) columns = ('ID', 'Type', 'User ID', 'Data', 'Project ID') @@ -260,7 +260,7 @@ def test_credential_list_with_options(self): self.assertEqual(self.data, tuple(data)) -class TestCredentialSet(identity_fakes.TestIdentityv3): +class TestCredentialSet(identity_fakes.TestIdentity): credential = sdk_fakes.generate_fake_resource(_credential.Credential) def setUp(self): @@ -350,7 +350,7 @@ def test_credential_set_valid_with_project(self): self.assertIsNone(result) -class TestCredentialShow(identity_fakes.TestIdentityv3): +class TestCredentialShow(identity_fakes.TestIdentity): columns = ( 'blob', 'id', diff --git a/openstackclient/tests/unit/identity/v3/test_domain.py b/openstackclient/tests/unit/identity/v3/test_domain.py index fe3f655e4c..0ae8b4335e 100644 --- a/openstackclient/tests/unit/identity/v3/test_domain.py +++ b/openstackclient/tests/unit/identity/v3/test_domain.py @@ -10,33 +10,32 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.identity.v3 import domain as _domain +from openstack.test import fakes as sdk_fakes from openstackclient.identity.v3 import domain from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestDomain(identity_fakes.TestIdentityv3): - def setUp(self): - super().setUp() - - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - self.domains_mock.reset_mock() - - -class TestDomainCreate(TestDomain): - columns = ('description', 'enabled', 'id', 'name', 'tags') +class TestDomainCreate(identity_fakes.TestIdentity): + columns = ( + 'id', + 'name', + 'enabled', + 'description', + 'options', + ) def setUp(self): super().setUp() - self.domain = identity_fakes.FakeDomain.create_one_domain() - self.domains_mock.create.return_value = self.domain + self.domain = sdk_fakes.generate_fake_resource(_domain.Domain) + self.identity_sdk_client.create_domain.return_value = self.domain self.datalist = ( - self.domain.description, - True, self.domain.id, self.domain.name, - self.domain.tags, + self.domain.is_enabled, + self.domain.description, + self.domain.options, ) # Get the command object to test @@ -61,9 +60,9 @@ def test_domain_create_no_options(self): 'name': self.domain.name, 'description': None, 'options': {}, - 'enabled': True, + 'is_enabled': True, } - self.domains_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_domain.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) @@ -90,9 +89,9 @@ def test_domain_create_description(self): 'name': self.domain.name, 'description': 'new desc', 'options': {}, - 'enabled': True, + 'is_enabled': True, } - self.domains_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_domain.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) @@ -103,7 +102,7 @@ def test_domain_create_enable(self): self.domain.name, ] verifylist = [ - ('enable', True), + ('is_enabled', True), ('name', self.domain.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -118,9 +117,9 @@ def test_domain_create_enable(self): 'name': self.domain.name, 'description': None, 'options': {}, - 'enabled': True, + 'is_enabled': True, } - self.domains_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_domain.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) @@ -131,7 +130,7 @@ def test_domain_create_disable(self): self.domain.name, ] verifylist = [ - ('disable', True), + ('is_enabled', False), ('name', self.domain.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -146,9 +145,9 @@ def test_domain_create_disable(self): 'name': self.domain.name, 'description': None, 'options': {}, - 'enabled': False, + 'is_enabled': False, } - self.domains_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_domain.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) @@ -174,9 +173,9 @@ def test_domain_create_with_immutable(self): 'name': self.domain.name, 'description': None, 'options': {'immutable': True}, - 'enabled': True, + 'is_enabled': True, } - self.domains_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_domain.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) @@ -187,7 +186,7 @@ def test_domain_create_with_no_immutable(self): self.domain.name, ] verifylist = [ - ('no_immutable', True), + ('immutable', False), ('name', self.domain.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -202,23 +201,23 @@ def test_domain_create_with_no_immutable(self): 'name': self.domain.name, 'description': None, 'options': {'immutable': False}, - 'enabled': True, + 'is_enabled': True, } - self.domains_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_domain.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) -class TestDomainDelete(TestDomain): - domain = identity_fakes.FakeDomain.create_one_domain() +class TestDomainDelete(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): super().setUp() # This is the return value for utils.find_resource() - self.domains_mock.get.return_value = self.domain - self.domains_mock.delete.return_value = None + self.identity_sdk_client.find_domain.return_value = self.domain + self.identity_sdk_client.delete_domain.return_value = None # Get the command object to test self.cmd = domain.DeleteDomain(self.app, None) @@ -234,19 +233,35 @@ def test_domain_delete(self): result = self.cmd.take_action(parsed_args) - self.domains_mock.delete.assert_called_with( + self.identity_sdk_client.delete_domain.assert_called_with( self.domain.id, ) self.assertIsNone(result) -class TestDomainList(TestDomain): - domain = identity_fakes.FakeDomain.create_one_domain() +class TestDomainList(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource( + resource_type=_domain.Domain, is_enabled=True + ) + columns = ( + 'ID', + 'Name', + 'Enabled', + 'Description', + ) def setUp(self): super().setUp() - self.domains_mock.list.return_value = [self.domain] + self.identity_sdk_client.domains.return_value = [self.domain] + self.datalist = ( + ( + self.domain.id, + self.domain.name, + self.domain.is_enabled, + self.domain.description, + ), + ) # Get the command object to test self.cmd = domain.ListDomain(self.app, None) @@ -260,19 +275,10 @@ def test_domain_list_no_options(self): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.domains_mock.list.assert_called_with() + self.identity_sdk_client.domains.assert_called_with() - collist = ('ID', 'Name', 'Enabled', 'Description') - self.assertEqual(collist, columns) - datalist = ( - ( - self.domain.id, - self.domain.name, - True, - self.domain.description, - ), - ) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) def test_domain_list_with_option_name(self): arglist = ['--name', self.domain.name] @@ -285,23 +291,14 @@ def test_domain_list_with_option_name(self): columns, data = self.cmd.take_action(parsed_args) kwargs = {'name': self.domain.name} - self.domains_mock.list.assert_called_with(**kwargs) + self.identity_sdk_client.domains.assert_called_with(**kwargs) - collist = ('ID', 'Name', 'Enabled', 'Description') - self.assertEqual(collist, columns) - datalist = ( - ( - self.domain.id, - self.domain.name, - True, - self.domain.description, - ), - ) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) def test_domain_list_with_option_enabled(self): arglist = ['--enabled'] - verifylist = [('enabled', True)] + verifylist = [('is_enabled', True)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # In base command class Lister in cliff, abstract method take_action() @@ -309,31 +306,22 @@ def test_domain_list_with_option_enabled(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - kwargs = {'enabled': True} - self.domains_mock.list.assert_called_with(**kwargs) + kwargs = {'is_enabled': True} + self.identity_sdk_client.domains.assert_called_with(**kwargs) - collist = ('ID', 'Name', 'Enabled', 'Description') - self.assertEqual(collist, columns) - datalist = ( - ( - self.domain.id, - self.domain.name, - True, - self.domain.description, - ), - ) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) -class TestDomainSet(TestDomain): - domain = identity_fakes.FakeDomain.create_one_domain() +class TestDomainSet(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): super().setUp() - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_domain.return_value = self.domain - self.domains_mock.update.return_value = self.domain + self.identity_sdk_client.update_domain.return_value = self.domain # Get the command object to test self.cmd = domain.SetDomain(self.app, None) @@ -350,7 +338,9 @@ def test_domain_set_no_options(self): result = self.cmd.take_action(parsed_args) kwargs = {} - self.domains_mock.update.assert_called_with(self.domain.id, **kwargs) + self.identity_sdk_client.update_domain.assert_called_with( + self.domain.id, **kwargs + ) self.assertIsNone(result) def test_domain_set_name(self): @@ -371,7 +361,9 @@ def test_domain_set_name(self): kwargs = { 'name': 'qwerty', } - self.domains_mock.update.assert_called_with(self.domain.id, **kwargs) + self.identity_sdk_client.update_domain.assert_called_with( + self.domain.id, **kwargs + ) self.assertIsNone(result) def test_domain_set_description(self): @@ -392,7 +384,9 @@ def test_domain_set_description(self): kwargs = { 'description': 'new desc', } - self.domains_mock.update.assert_called_with(self.domain.id, **kwargs) + self.identity_sdk_client.update_domain.assert_called_with( + self.domain.id, **kwargs + ) self.assertIsNone(result) def test_domain_set_enable(self): @@ -401,7 +395,7 @@ def test_domain_set_enable(self): self.domain.id, ] verifylist = [ - ('enable', True), + ('is_enabled', True), ('domain', self.domain.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -410,9 +404,11 @@ def test_domain_set_enable(self): # Set expected values kwargs = { - 'enabled': True, + 'is_enabled': True, } - self.domains_mock.update.assert_called_with(self.domain.id, **kwargs) + self.identity_sdk_client.update_domain.assert_called_with( + self.domain.id, **kwargs + ) self.assertIsNone(result) def test_domain_set_disable(self): @@ -421,7 +417,7 @@ def test_domain_set_disable(self): self.domain.id, ] verifylist = [ - ('disable', True), + ('is_enabled', False), ('domain', self.domain.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -430,9 +426,11 @@ def test_domain_set_disable(self): # Set expected values kwargs = { - 'enabled': False, + 'is_enabled': False, } - self.domains_mock.update.assert_called_with(self.domain.id, **kwargs) + self.identity_sdk_client.update_domain.assert_called_with( + self.domain.id, **kwargs + ) self.assertIsNone(result) def test_domain_set_immutable_option(self): @@ -452,7 +450,9 @@ def test_domain_set_immutable_option(self): kwargs = { 'options': {'immutable': True}, } - self.domains_mock.update.assert_called_with(self.domain.id, **kwargs) + self.identity_sdk_client.update_domain.assert_called_with( + self.domain.id, **kwargs + ) self.assertIsNone(result) def test_domain_set_no_immutable_option(self): @@ -461,7 +461,7 @@ def test_domain_set_no_immutable_option(self): self.domain.id, ] verifylist = [ - ('no_immutable', True), + ('immutable', False), ('domain', self.domain.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -472,16 +472,34 @@ def test_domain_set_no_immutable_option(self): kwargs = { 'options': {'immutable': False}, } - self.domains_mock.update.assert_called_with(self.domain.id, **kwargs) + self.identity_sdk_client.update_domain.assert_called_with( + self.domain.id, **kwargs + ) self.assertIsNone(result) -class TestDomainShow(TestDomain): +class TestDomainShow(identity_fakes.TestIdentity): + columns = ( + 'id', + 'name', + 'enabled', + 'description', + 'options', + ) + def setUp(self): super().setUp() - self.domain = identity_fakes.FakeDomain.create_one_domain() - self.domains_mock.get.return_value = self.domain + self.domain = sdk_fakes.generate_fake_resource(_domain.Domain) + self.identity_sdk_client.find_domain.return_value = self.domain + self.datalist = ( + self.domain.id, + self.domain.name, + self.domain.is_enabled, + self.domain.description, + self.domain.options, + ) + # Get the command object to test self.cmd = domain.ShowDomain(self.app, None) @@ -501,17 +519,9 @@ def test_domain_show(self): # returns a two-part tuple with a tuple of column names and a tuple of # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.domains_mock.get.assert_called_with( - self.domain.id, + self.identity_sdk_client.find_domain.assert_called_with( + self.domain.id, ignore_missing=False ) - collist = ('description', 'enabled', 'id', 'name', 'tags') - self.assertEqual(collist, columns) - datalist = ( - self.domain.description, - True, - self.domain.id, - self.domain.name, - self.domain.tags, - ) - self.assertEqual(datalist, data) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/test_endpoint.py b/openstackclient/tests/unit/identity/v3/test_endpoint.py index 84e10e55ad..eb201fd4a0 100644 --- a/openstackclient/tests/unit/identity/v3/test_endpoint.py +++ b/openstackclient/tests/unit/identity/v3/test_endpoint.py @@ -21,30 +21,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestEndpoint(identity_fakes.TestIdentityv3): - def setUp(self): - super().setUp() - - # Get a shortcut to the EndpointManager Mock - self.endpoints_mock = self.identity_client.endpoints - self.endpoints_mock.reset_mock() - self.ep_filter_mock = self.identity_client.endpoint_filter - self.ep_filter_mock.reset_mock() - - # Get a shortcut to the ServiceManager Mock - self.services_mock = self.identity_client.services - self.services_mock.reset_mock() - - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - self.domains_mock.reset_mock() - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() - - -class TestEndpointCreate(identity_fakes.TestIdentityv3): +class TestEndpointCreate(identity_fakes.TestIdentity): columns = ( 'enabled', 'id', @@ -265,7 +242,7 @@ def test_endpoint_create_disable(self): self.assertEqual(datalist, data) -class TestEndpointDelete(identity_fakes.TestIdentityv3): +class TestEndpointDelete(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -294,7 +271,7 @@ def test_endpoint_delete(self): self.assertIsNone(result) -class TestEndpointList(identity_fakes.TestIdentityv3): +class TestEndpointList(identity_fakes.TestIdentity): columns = ( 'ID', 'Region', @@ -494,7 +471,7 @@ def test_endpoint_list_project_with_project_domain(self): self.assertEqual(datalist, tuple(data)) -class TestEndpointSet(identity_fakes.TestIdentityv3): +class TestEndpointSet(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -643,7 +620,7 @@ def test_endpoint_set_disable(self): self.assertIsNone(result) -class TestEndpointShow(identity_fakes.TestIdentityv3): +class TestEndpointShow(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -678,7 +655,7 @@ def test_endpoint_show(self): # data to be shown. columns, data = self.cmd.take_action(parsed_args) self.identity_sdk_client.find_endpoint.assert_called_with( - self.endpoint.id, + self.endpoint.id, ignore_missing=False ) collist = ( @@ -767,28 +744,21 @@ def setUp(self): self.cmd = endpoint.ShowEndpoint(self.app, None) -class TestAddProjectToEndpoint(TestEndpoint): - project = identity_fakes.FakeProject.create_one_project() - domain = identity_fakes.FakeDomain.create_one_domain() - service = identity_fakes.FakeService.create_one_service() - endpoint = identity_fakes.FakeEndpoint.create_one_endpoint( - attrs={'service_id': service.id} - ) - - new_ep_filter = identity_fakes.FakeEndpoint.create_one_endpoint_filter( - attrs={'endpoint': endpoint.id, 'project': project.id} - ) - +class TestAddProjectToEndpoint(identity_fakes.TestIdentity): def setUp(self): super().setUp() - # This is the return value for utils.find_resource() - self.endpoints_mock.get.return_value = self.endpoint + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.domain = sdk_fakes.generate_fake_resource(_domain.Domain) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.endpoint = sdk_fakes.generate_fake_resource( + _endpoint.Endpoint, service_id=self.service.id + ) + + self.identity_sdk_client.find_project.return_value = self.project + self.identity_sdk_client.find_domain.return_value = self.domain + self.identity_sdk_client.find_endpoint.return_value = self.endpoint - # Update the image_id in the MEMBER dict - self.ep_filter_mock.create.return_value = self.new_ep_filter - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain # Get the command object to test self.cmd = endpoint.AddProjectToEndpoint(self.app, None) @@ -804,7 +774,7 @@ def test_add_project_to_endpoint_no_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.ep_filter_mock.add_endpoint_to_project.assert_called_with( + self.identity_sdk_client.associate_endpoint_with_project.assert_called_with( project=self.project.id, endpoint=self.endpoint.id ) self.assertIsNone(result) @@ -824,29 +794,26 @@ def test_add_project_to_endpoint_with_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.ep_filter_mock.add_endpoint_to_project.assert_called_with( + self.identity_sdk_client.associate_endpoint_with_project.assert_called_with( project=self.project.id, endpoint=self.endpoint.id ) self.assertIsNone(result) -class TestRemoveProjectEndpoint(TestEndpoint): - project = identity_fakes.FakeProject.create_one_project() - domain = identity_fakes.FakeDomain.create_one_domain() - service = identity_fakes.FakeService.create_one_service() - endpoint = identity_fakes.FakeEndpoint.create_one_endpoint( - attrs={'service_id': service.id} - ) - +class TestRemoveProjectEndpoint(identity_fakes.TestIdentity): def setUp(self): super().setUp() - # This is the return value for utils.find_resource() - self.endpoints_mock.get.return_value = self.endpoint + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.domain = sdk_fakes.generate_fake_resource(_domain.Domain) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.endpoint = sdk_fakes.generate_fake_resource( + _endpoint.Endpoint, service_id=self.service.id + ) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain - self.ep_filter_mock.delete.return_value = None + self.identity_sdk_client.find_project.return_value = self.project + self.identity_sdk_client.find_domain.return_value = self.domain + self.identity_sdk_client.find_endpoint.return_value = self.endpoint # Get the command object to test self.cmd = endpoint.RemoveProjectFromEndpoint(self.app, None) @@ -863,10 +830,8 @@ def test_remove_project_endpoint_no_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - - self.ep_filter_mock.delete_endpoint_from_project.assert_called_with( - project=self.project.id, - endpoint=self.endpoint.id, + self.identity_sdk_client.disassociate_endpoint_from_project.assert_called_with( + project=self.project.id, endpoint=self.endpoint.id ) self.assertIsNone(result) @@ -885,9 +850,7 @@ def test_remove_project_endpoint_with_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - - self.ep_filter_mock.delete_endpoint_from_project.assert_called_with( - project=self.project.id, - endpoint=self.endpoint.id, + self.identity_sdk_client.disassociate_endpoint_from_project.assert_called_with( + project=self.project.id, endpoint=self.endpoint.id ) self.assertIsNone(result) diff --git a/openstackclient/tests/unit/identity/v3/test_endpoint_group.py b/openstackclient/tests/unit/identity/v3/test_endpoint_group.py index e739a7d92a..7c5b123b17 100644 --- a/openstackclient/tests/unit/identity/v3/test_endpoint_group.py +++ b/openstackclient/tests/unit/identity/v3/test_endpoint_group.py @@ -17,7 +17,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestEndpointGroup(identity_fakes.TestIdentityv3): +class TestEndpointGroup(identity_fakes.TestIdentity): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/identity/v3/test_group.py b/openstackclient/tests/unit/identity/v3/test_group.py index bd983c6180..d6867272c8 100644 --- a/openstackclient/tests/unit/identity/v3/test_group.py +++ b/openstackclient/tests/unit/identity/v3/test_group.py @@ -14,45 +14,33 @@ from unittest import mock from unittest.mock import call -from keystoneauth1 import exceptions as ks_exc +from openstack import exceptions as sdk_exc +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import group as _group +from openstack.identity.v3 import user as _user +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions -from osc_lib import utils from openstackclient.identity.v3 import group from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestGroup(identity_fakes.TestIdentityv3): +class TestGroupAddUser(identity_fakes.TestIdentity): def setUp(self): super().setUp() - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - self.domains_mock.reset_mock() - - # Get a shortcut to the GroupManager Mock - self.groups_mock = self.identity_client.groups - self.groups_mock.reset_mock() - - # Get a shortcut to the UserManager Mock - self.users_mock = self.identity_client.users - self.users_mock.reset_mock() - - -class TestGroupAddUser(TestGroup): - _group = identity_fakes.FakeGroup.create_one_group() - users = identity_fakes.FakeUser.create_users(count=2) - - def setUp(self): - super().setUp() + self._group = sdk_fakes.generate_fake_resource(_group.Group) + self.users = tuple( + sdk_fakes.generate_fake_resources(_user.User, count=2) + ) - self.groups_mock.get.return_value = self._group - self.users_mock.get = identity_fakes.FakeUser.get_users(self.users) - self.users_mock.add_to_group.return_value = None + self.identity_sdk_client.find_group.return_value = self._group + self.identity_sdk_client.add_user_to_group.return_value = None self.cmd = group.AddUserToGroup(self.app, None) def test_group_add_user(self): + self.identity_sdk_client.find_user.return_value = self.users[0] arglist = [ self._group.name, self.users[0].name, @@ -64,12 +52,16 @@ def test_group_add_user(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.users_mock.add_to_group.assert_called_once_with( + self.identity_sdk_client.add_user_to_group.assert_called_once_with( self.users[0].id, self._group.id ) self.assertIsNone(result) def test_group_add_multi_users(self): + self.identity_sdk_client.find_user.side_effect = [ + self.users[0], + self.users[1], + ] arglist = [ self._group.name, self.users[0].name, @@ -86,13 +78,13 @@ def test_group_add_multi_users(self): call(self.users[0].id, self._group.id), call(self.users[1].id, self._group.id), ] - self.users_mock.add_to_group.assert_has_calls(calls) + self.identity_sdk_client.add_user_to_group.assert_has_calls(calls) self.assertIsNone(result) @mock.patch.object(group.LOG, 'error') def test_group_add_user_with_error(self, mock_error): - self.users_mock.add_to_group.side_effect = [ - exceptions.CommandError(), + self.identity_sdk_client.add_user_to_group.side_effect = [ + sdk_exc.ResourceNotFound, None, ] arglist = [ @@ -111,20 +103,20 @@ def test_group_add_user_with_error(self, mock_error): except exceptions.CommandError as e: msg = f"1 of 2 users not added to group {self._group.name}." self.assertEqual(msg, str(e)) - msg = f"{self.users[0].name} not added to group {self._group.name}: " + msg = f"{self.users[0].name} not added to group {self._group.name}: {sdk_exc.ResourceNotFound()!s}" mock_error.assert_called_once_with(msg) -class TestGroupCheckUser(TestGroup): - group = identity_fakes.FakeGroup.create_one_group() - user = identity_fakes.FakeUser.create_one_user() - +class TestGroupCheckUser(identity_fakes.TestIdentity): def setUp(self): super().setUp() - self.groups_mock.get.return_value = self.group - self.users_mock.get.return_value = self.user - self.users_mock.check_in_group.return_value = None + self.group = sdk_fakes.generate_fake_resource(_group.Group) + self.user = sdk_fakes.generate_fake_resource(_user.User) + + self.identity_sdk_client.find_group.return_value = self.group + self.identity_sdk_client.find_user.return_value = self.user + self.identity_sdk_client.check_user_in_group.return_value = True self.cmd = group.CheckUserInGroup(self.app, None) @@ -140,16 +132,15 @@ def test_group_check_user(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.users_mock.check_in_group.assert_called_once_with( + self.identity_sdk_client.check_user_in_group.assert_called_once_with( self.user.id, self.group.id ) self.assertIsNone(result) def test_group_check_user_server_error(self): - def server_error(*args): - raise ks_exc.http.InternalServerError - - self.users_mock.check_in_group.side_effect = server_error + self.identity_sdk_client.check_user_in_group.side_effect = ( + sdk_exc.SDKException + ) arglist = [ self.group.name, self.user.name, @@ -161,12 +152,12 @@ def server_error(*args): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.assertRaises( - ks_exc.http.InternalServerError, self.cmd.take_action, parsed_args + sdk_exc.SDKException, self.cmd.take_action, parsed_args ) -class TestGroupCreate(TestGroup): - domain = identity_fakes.FakeDomain.create_one_domain() +class TestGroupCreate(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) columns = ( 'description', @@ -177,23 +168,20 @@ class TestGroupCreate(TestGroup): def setUp(self): super().setUp() - self.group = identity_fakes.FakeGroup.create_one_group( - attrs={'domain_id': self.domain.id} + self.group = sdk_fakes.generate_fake_resource( + _group.Group, description=None, domain_id=None ) - self.data = ( - self.group.description, - self.group.domain_id, - self.group.id, - self.group.name, + self.group_with_options = sdk_fakes.generate_fake_resource( + _group.Group, domain_id=self.domain.id ) - self.groups_mock.create.return_value = self.group - self.groups_mock.get.return_value = self.group - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_group.return_value = self.group + self.identity_sdk_client.find_domain.return_value = self.domain self.cmd = group.CreateGroup(self.app, None) def test_group_create(self): + self.identity_sdk_client.create_group.return_value = self.group arglist = [ self.group.name, ] @@ -203,40 +191,56 @@ def test_group_create(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.groups_mock.create.assert_called_once_with( + self.identity_sdk_client.create_group.assert_called_once_with( name=self.group.name, - domain=None, - description=None, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + datalist = ( + self.group.description, + None, + self.group.id, + self.group.name, + ) + self.assertEqual(datalist, data) def test_group_create_with_options(self): + self.identity_sdk_client.create_group.return_value = ( + self.group_with_options + ) arglist = [ '--domain', self.domain.name, '--description', - self.group.description, - self.group.name, + self.group_with_options.description, + self.group_with_options.name, ] verifylist = [ ('domain', self.domain.name), - ('description', self.group.description), - ('name', self.group.name), + ('description', self.group_with_options.description), + ('name', self.group_with_options.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.groups_mock.create.assert_called_once_with( - name=self.group.name, - domain=self.domain.id, - description=self.group.description, + self.identity_sdk_client.create_group.assert_called_once_with( + name=self.group_with_options.name, + domain_id=self.domain.id, + description=self.group_with_options.description, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + datalist = ( + self.group_with_options.description, + self.domain.id, + self.group_with_options.id, + self.group_with_options.name, + ) + self.assertEqual(datalist, data) def test_group_create_or_show(self): - self.groups_mock.create.side_effect = ks_exc.Conflict() + self.identity_sdk_client.find_group.return_value = self.group + self.identity_sdk_client.create_group.side_effect = ( + sdk_exc.ConflictException + ) arglist = [ '--or-show', self.group.name, @@ -248,46 +252,99 @@ def test_group_create_or_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.groups_mock.get.assert_called_once_with(self.group.name) + self.identity_sdk_client.find_group.assert_called_once_with( + self.group.name, ignore_missing=False + ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + datalist = ( + self.group.description, + None, + self.group.id, + self.group.name, + ) + self.assertEqual(datalist, data) + def test_group_create_or_show_with_domain(self): + self.identity_sdk_client.find_group.return_value = ( + self.group_with_options + ) + self.identity_sdk_client.create_group.side_effect = ( + sdk_exc.ConflictException + ) + arglist = [ + '--or-show', + self.group_with_options.name, + '--domain', + self.domain.id, + ] + verifylist = [ + ('or_show', True), + ('name', self.group_with_options.name), + ('domain', self.domain.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.identity_sdk_client.find_group.assert_called_once_with( + self.group_with_options.name, + domain_id=self.domain.id, + ignore_missing=False, + ) + self.assertEqual(self.columns, columns) + datalist = ( + self.group_with_options.description, + self.domain.id, + self.group_with_options.id, + self.group_with_options.name, + ) + self.assertEqual(datalist, data) -class TestGroupDelete(TestGroup): - domain = identity_fakes.FakeDomain.create_one_domain() - groups = identity_fakes.FakeGroup.create_groups( - attrs={'domain_id': domain.id}, count=2 - ) + +class TestGroupDelete(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): super().setUp() - self.groups_mock.get = identity_fakes.FakeGroup.get_groups(self.groups) - self.groups_mock.delete.return_value = None - self.domains_mock.get.return_value = self.domain + self.group = sdk_fakes.generate_fake_resource( + _group.Group, + domain_id=None, + ) + self.group_with_domain = sdk_fakes.generate_fake_resource( + _group.Group, + name=self.group.name, + domain_id=self.domain.id, + ) + self.identity_sdk_client.delete_group.return_value = None + self.identity_sdk_client.find_domain.return_value = self.domain self.cmd = group.DeleteGroup(self.app, None) def test_group_delete(self): + self.identity_sdk_client.find_group.return_value = self.group arglist = [ - self.groups[0].id, + self.group.id, ] verifylist = [ - ('groups', [self.groups[0].id]), + ('groups', [self.group.id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.groups_mock.get.assert_called_once_with(self.groups[0].id) - self.groups_mock.delete.assert_called_once_with(self.groups[0].id) + self.identity_sdk_client.find_group.assert_called_once_with( + name_or_id=self.group.id, ignore_missing=False + ) + self.identity_sdk_client.delete_group.assert_called_once_with( + self.group.id + ) self.assertIsNone(result) def test_group_multi_delete(self): - arglist = [] - verifylist = [] - - for g in self.groups: - arglist.append(g.id) + self.identity_sdk_client.find_group.side_effect = [ + self.group, + self.group_with_domain, + ] + arglist = [self.group.id, self.group_with_domain.id] verifylist = [ ('groups', arglist), ] @@ -295,39 +352,50 @@ def test_group_multi_delete(self): result = self.cmd.take_action(parsed_args) - calls = [] - for g in self.groups: - calls.append(call(g.id)) - self.groups_mock.delete.assert_has_calls(calls) + self.identity_sdk_client.delete_group.assert_has_calls( + [mock.call(self.group.id), mock.call(self.group_with_domain.id)] + ) self.assertIsNone(result) def test_group_delete_with_domain(self): - get_mock_result = [exceptions.CommandError, self.groups[0]] - self.groups_mock.get = mock.Mock(side_effect=get_mock_result) + self.identity_sdk_client.find_domain.side_effect = [ + sdk_exc.ForbiddenException + ] + self.identity_sdk_client.find_group.return_value = ( + self.group_with_domain + ) arglist = [ '--domain', - self.domain.id, - self.groups[0].id, + self.group_with_domain.domain_id, + self.group_with_domain.name, ] verifylist = [ - ('domain', self.groups[0].domain_id), - ('groups', [self.groups[0].id]), + ('domain', self.domain.id), + ('groups', [self.group_with_domain.name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.groups_mock.get.assert_any_call( - self.groups[0].id, domain_id=self.domain.id + self.identity_sdk_client.find_group.assert_called_with( + name_or_id=self.group_with_domain.name, + ignore_missing=False, + domain_id=self.domain.id, + ) + self.identity_sdk_client.delete_group.assert_called_once_with( + self.group_with_domain.id ) - self.groups_mock.delete.assert_called_once_with(self.groups[0].id) self.assertIsNone(result) - @mock.patch.object(utils, 'find_resource') - def test_delete_multi_groups_with_exception(self, find_mock): - find_mock.side_effect = [self.groups[0], exceptions.CommandError] + def test_delete_multi_groups_with_exception(self): + self.identity_sdk_client.find_group.side_effect = [ + self.group, + self.group_with_domain, + exceptions.CommandError, + ] arglist = [ - self.groups[0].id, + self.group.id, + self.group_with_domain.id, 'unexist_group', ] verifylist = [ @@ -339,45 +407,57 @@ def test_delete_multi_groups_with_exception(self, find_mock): self.cmd.take_action(parsed_args) self.fail('CommandError should be raised.') except exceptions.CommandError as e: - self.assertEqual('1 of 2 groups failed to delete.', str(e)) - - find_mock.assert_any_call(self.groups_mock, self.groups[0].id) - find_mock.assert_any_call(self.groups_mock, 'unexist_group') + self.assertEqual('1 of 3 groups failed to delete.', str(e)) + + self.identity_sdk_client.find_group.assert_has_calls( + [ + mock.call(name_or_id=self.group.id, ignore_missing=False), + mock.call( + name_or_id=self.group_with_domain.id, ignore_missing=False + ), + mock.call(name_or_id='unexist_group', ignore_missing=False), + ] + ) - self.assertEqual(2, find_mock.call_count) - self.groups_mock.delete.assert_called_once_with(self.groups[0].id) + self.assertEqual(3, self.identity_sdk_client.find_group.call_count) + self.identity_sdk_client.delete_group.assert_has_calls( + [ + mock.call(self.group.id), + mock.call(self.group_with_domain.id), + ] + ) -class TestGroupList(TestGroup): - domain = identity_fakes.FakeDomain.create_one_domain() - group = identity_fakes.FakeGroup.create_one_group() - user = identity_fakes.FakeUser.create_one_user() +class TestGroupList(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) columns = ( 'ID', 'Name', ) - datalist = ( - ( - group.id, - group.name, - ), - ) def setUp(self): super().setUp() - self.groups_mock.get.return_value = self.group - self.groups_mock.list.return_value = [self.group] - - self.domains_mock.get.return_value = self.domain + self.group = sdk_fakes.generate_fake_resource( + _group.Group, description=None, domain_id=None + ) + self.group_with_domain = sdk_fakes.generate_fake_resource( + _group.Group, domain_id=self.domain.id + ) + self.user = sdk_fakes.generate_fake_resource(_user.User) - self.users_mock.get.return_value = self.user + self.identity_sdk_client.find_user.return_value = self.user + self.identity_sdk_client.find_domain.return_value = self.domain # Get the command object to test self.cmd = group.ListGroup(self.app, None) def test_group_list_no_options(self): + self.identity_sdk_client.groups.return_value = [ + self.group, + self.group_with_domain, + ] arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -387,18 +467,23 @@ def test_group_list_no_options(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'domain': None, - 'user': None, - } - - self.groups_mock.list.assert_called_with(**kwargs) + self.identity_sdk_client.groups.assert_called_with() self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + datalist = ( + ( + self.group.id, + self.group.name, + ), + ( + self.group_with_domain.id, + self.group_with_domain.name, + ), + ) + self.assertEqual(datalist, tuple(data)) def test_group_list_domain(self): + self.identity_sdk_client.groups.return_value = [self.group_with_domain] arglist = [ '--domain', self.domain.id, @@ -415,16 +500,17 @@ def test_group_list_domain(self): # Set expected values kwargs = { - 'domain': self.domain.id, - 'user': None, + 'domain_id': self.domain.id, } - self.groups_mock.list.assert_called_with(**kwargs) + self.identity_sdk_client.groups.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + datalist = ((self.group_with_domain.id, self.group_with_domain.name),) + self.assertEqual(datalist, tuple(data)) def test_group_list_user(self): + self.identity_sdk_client.user_groups.return_value = [self.group] arglist = [ '--user', self.user.name, @@ -439,18 +525,46 @@ def test_group_list_user(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'domain': None, - 'user': self.user.id, - } + self.identity_sdk_client.user_groups.assert_called_with(self.user.id) + + self.assertEqual(self.columns, columns) + + datalist = ((self.group.id, self.group.name),) + self.assertEqual(datalist, tuple(data)) + + def test_group_list_user_domain(self): + self.identity_sdk_client.user_groups.return_value = [ + self.group_with_domain + ] + arglist = [ + '--user', + self.user.name, + '--domain', + self.domain.name, + ] + verifylist = [ + ('user', self.user.name), + ('domain', self.domain.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) - self.groups_mock.list.assert_called_with(**kwargs) + self.identity_sdk_client.user_groups.assert_called_with(self.user.id) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, tuple(data)) + + datalist = ((self.group_with_domain.id, self.group_with_domain.name),) + self.assertEqual(datalist, tuple(data)) def test_group_list_long(self): + self.identity_sdk_client.groups.return_value = [ + self.group, + self.group_with_domain, + ] arglist = [ '--long', ] @@ -464,18 +578,9 @@ def test_group_list_long(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'domain': None, - 'user': None, - } + self.identity_sdk_client.groups.assert_called_with() - self.groups_mock.list.assert_called_with(**kwargs) - - columns = self.columns + ( - 'Domain ID', - 'Description', - ) + long_columns = (*self.columns, 'Domain ID', 'Description') datalist = ( ( self.group.id, @@ -483,25 +588,33 @@ def test_group_list_long(self): self.group.domain_id, self.group.description, ), + ( + self.group_with_domain.id, + self.group_with_domain.name, + self.group_with_domain.domain_id, + self.group_with_domain.description, + ), ) - self.assertEqual(columns, columns) + self.assertEqual(long_columns, columns) self.assertEqual(datalist, tuple(data)) -class TestGroupRemoveUser(TestGroup): - _group = identity_fakes.FakeGroup.create_one_group() - users = identity_fakes.FakeUser.create_users(count=2) - +class TestGroupRemoveUser(identity_fakes.TestIdentity): def setUp(self): super().setUp() - self.groups_mock.get.return_value = self._group - self.users_mock.get = identity_fakes.FakeUser.get_users(self.users) - self.users_mock.remove_from_group.return_value = None + self._group = sdk_fakes.generate_fake_resource(_group.Group) + self.users = tuple( + sdk_fakes.generate_fake_resources(_user.User, count=2) + ) + + self.identity_sdk_client.find_group.return_value = self._group + self.identity_sdk_client.remove_user_from_group.return_value = None self.cmd = group.RemoveUserFromGroup(self.app, None) def test_group_remove_user(self): + self.identity_sdk_client.find_user.return_value = self.users[0] arglist = [ self._group.id, self.users[0].id, @@ -513,12 +626,16 @@ def test_group_remove_user(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.users_mock.remove_from_group.assert_called_once_with( + self.identity_sdk_client.remove_user_from_group.assert_called_once_with( self.users[0].id, self._group.id ) self.assertIsNone(result) def test_group_remove_multi_users(self): + self.identity_sdk_client.find_user.side_effect = [ + self.users[0], + self.users[1], + ] arglist = [ self._group.name, self.users[0].name, @@ -535,13 +652,13 @@ def test_group_remove_multi_users(self): call(self.users[0].id, self._group.id), call(self.users[1].id, self._group.id), ] - self.users_mock.remove_from_group.assert_has_calls(calls) + self.identity_sdk_client.remove_user_from_group.assert_has_calls(calls) self.assertIsNone(result) @mock.patch.object(group.LOG, 'error') def test_group_remove_user_with_error(self, mock_error): - self.users_mock.remove_from_group.side_effect = [ - exceptions.CommandError(), + self.identity_sdk_client.remove_user_from_group.side_effect = [ + sdk_exc.ResourceNotFound(), None, ] arglist = [ @@ -560,26 +677,29 @@ def test_group_remove_user_with_error(self, mock_error): except exceptions.CommandError as e: msg = f"1 of 2 users not removed from group {self._group.id}." self.assertEqual(msg, str(e)) - msg = f"{self.users[0].id} not removed from group {self._group.id}: " + msg = f"{self.users[0].id} not removed from group {self._group.id}: {sdk_exc.ResourceNotFound()!s}" mock_error.assert_called_once_with(msg) -class TestGroupSet(TestGroup): - domain = identity_fakes.FakeDomain.create_one_domain() - group = identity_fakes.FakeGroup.create_one_group( - attrs={'domain_id': domain.id} - ) +class TestGroupSet(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): super().setUp() + self.group = sdk_fakes.generate_fake_resource( + _group.Group, domain_id=self.domain.id + ) + self.group_with_domain = sdk_fakes.generate_fake_resource( + _group.Group, name=self.group.name, domain_id=self.domain.id + ) - self.groups_mock.get.return_value = self.group - self.domains_mock.get.return_value = self.domain - self.groups_mock.update.return_value = None + self.identity_sdk_client.find_group.return_value = self.group + self.identity_sdk_client.find_domain.return_value = self.domain self.cmd = group.SetGroup(self.app, None) def test_group_set_nothing(self): + self.identity_sdk_client.update_group.return_value = self.group arglist = [ self.group.id, ] @@ -589,10 +709,13 @@ def test_group_set_nothing(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.groups_mock.update.assert_called_once_with(self.group.id) + self.identity_sdk_client.update_group.assert_called_once_with( + self.group.id + ) self.assertIsNone(result) def test_group_set_name_and_description(self): + self.identity_sdk_client.update_group.return_value = self.group arglist = [ '--name', 'new_name', @@ -612,36 +735,43 @@ def test_group_set_name_and_description(self): 'name': 'new_name', 'description': 'new_description', } - self.groups_mock.update.assert_called_once_with( + self.identity_sdk_client.update_group.assert_called_once_with( self.group.id, **kwargs ) self.assertIsNone(result) def test_group_set_with_domain(self): - get_mock_result = [exceptions.CommandError, self.group] - self.groups_mock.get = mock.Mock(side_effect=get_mock_result) - + self.identity_sdk_client.find_domain.side_effect = [ + sdk_exc.ForbiddenException + ] + self.identity_sdk_client.find_group.return_value = ( + self.group_with_domain + ) arglist = [ '--domain', self.domain.id, - self.group.id, + self.group_with_domain.name, ] verifylist = [ ('domain', self.domain.id), - ('group', self.group.id), + ('group', self.group_with_domain.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.groups_mock.get.assert_any_call( - self.group.id, domain_id=self.domain.id + self.identity_sdk_client.find_group.assert_called_once_with( + name_or_id=self.group_with_domain.name, + ignore_missing=False, + domain_id=self.domain.id, + ) + self.identity_sdk_client.update_group.assert_called_once_with( + self.group_with_domain.id ) - self.groups_mock.update.assert_called_once_with(self.group.id) self.assertIsNone(result) -class TestGroupShow(TestGroup): - domain = identity_fakes.FakeDomain.create_one_domain() +class TestGroupShow(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) columns = ( 'description', @@ -652,22 +782,19 @@ class TestGroupShow(TestGroup): def setUp(self): super().setUp() - self.group = identity_fakes.FakeGroup.create_one_group( - attrs={'domain_id': self.domain.id} + self.group = sdk_fakes.generate_fake_resource( + _group.Group, description=None, domain_id=None ) - self.data = ( - self.group.description, - self.group.domain_id, - self.group.id, - self.group.name, + self.group_with_domain = sdk_fakes.generate_fake_resource( + _group.Group, name=self.group.name, domain_id=self.domain.id ) - self.groups_mock.get.return_value = self.group - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_domain.return_value = self.domain self.cmd = group.ShowGroup(self.app, None) def test_group_show(self): + self.identity_sdk_client.find_group.return_value = self.group arglist = [ self.group.id, ] @@ -677,28 +804,44 @@ def test_group_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.groups_mock.get.assert_called_once_with(self.group.id) + self.identity_sdk_client.find_group.assert_called_once_with( + self.group.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + datalist = ( + None, + None, + self.group.id, + self.group.name, + ) + self.assertEqual(datalist, data) def test_group_show_with_domain(self): - get_mock_result = [exceptions.CommandError, self.group] - self.groups_mock.get = mock.Mock(side_effect=get_mock_result) - + self.identity_sdk_client.find_group.return_value = ( + self.group_with_domain + ) arglist = [ '--domain', self.domain.id, - self.group.id, + self.group_with_domain.name, ] verifylist = [ ('domain', self.domain.id), - ('group', self.group.id), + ('group', self.group_with_domain.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.groups_mock.get.assert_any_call( - self.group.id, domain_id=self.domain.id + self.identity_sdk_client.find_group.assert_called_once_with( + self.group_with_domain.name, + domain_id=self.domain.id, + ignore_missing=False, ) self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + datalist = ( + self.group_with_domain.description, + self.domain.id, + self.group_with_domain.id, + self.group_with_domain.name, + ) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/test_identity_provider.py b/openstackclient/tests/unit/identity/v3/test_identity_provider.py index 20e7f497ae..cc89a14875 100644 --- a/openstackclient/tests/unit/identity/v3/test_identity_provider.py +++ b/openstackclient/tests/unit/identity/v3/test_identity_provider.py @@ -12,108 +12,98 @@ # License for the specific language governing permissions and limitations # under the License. -import copy from unittest import mock +from osc_lib.cli import format_columns from osc_lib import exceptions +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import identity_provider as _identity_provider +from openstack.test import fakes as sdk_fakes from openstackclient.identity.v3 import identity_provider -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit import utils as test_utils -class TestIdentityProvider(identity_fakes.TestFederatedIdentity): - def setUp(self): - super().setUp() - - # Identity Provider mocks - federation_lib = self.identity_client.federation - self.identity_providers_mock = federation_lib.identity_providers - self.identity_providers_mock.reset_mock() - - # Domain mocks - self.domains_mock = self.identity_client.domains - self.domains_mock.reset_mock() - self.domain = identity_fakes.FakeDomain.create_one_domain( - identity_fakes.DOMAIN - ) - self.domains_mock.list.return_value = [self.domain] - self.domains_mock.get.return_value = self.domain - - -class TestIdentityProviderCreate(TestIdentityProvider): +class TestIdentityProviderCreate(identity_fakes.TestFederatedIdentity): columns = ( + 'authorization_ttl', 'description', 'domain_id', 'enabled', 'id', 'remote_ids', ) - datalist = ( - identity_fakes.idp_description, - identity_fakes.domain_id, - True, - identity_fakes.idp_id, - identity_fakes.formatted_idp_remote_ids, - ) def setUp(self): super().setUp() - copied_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) - resource = fakes.FakeResource(None, copied_idp, loaded=True) - self.identity_providers_mock.create.return_value = resource + self.domain = sdk_fakes.generate_fake_resource(_domain.Domain) + self.idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + domain_id=self.domain.id, + remote_ids=['entity1', 'entity2'], + ) + + self.identity_sdk_client.create_identity_provider.return_value = ( + self.idp + ) + self.identity_sdk_client.find_domain.return_value = self.domain + + self.datalist = ( + self.idp.authorization_ttl, + self.idp.description, + self.domain.id, + self.idp.is_enabled, + self.idp.id, + format_columns.ListColumn(self.idp.remote_ids), + ) self.cmd = identity_provider.CreateIdentityProvider(self.app, None) def test_create_identity_provider_no_options(self): arglist = [ - identity_fakes.idp_id, + self.idp.id, ] verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), + ('identity_provider_id', self.idp.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'remote_ids': None, - 'enabled': True, - 'description': None, - 'domain_id': None, + 'is_enabled': True, } - self.identity_providers_mock.create.assert_called_with( - id=identity_fakes.idp_id, **kwargs + self.identity_sdk_client.create_identity_provider.assert_called_with( + id=self.idp.id, **kwargs ) self.assertEqual(self.columns, columns) + self.assertCountEqual(self.datalist, data) def test_create_identity_provider_description(self): arglist = [ '--description', - identity_fakes.idp_description, - identity_fakes.idp_id, + self.idp.description, + self.idp.id, ] verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), - ('description', identity_fakes.idp_description), + ('identity_provider_id', self.idp.id), + ('description', self.idp.description), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'remote_ids': None, - 'description': identity_fakes.idp_description, - 'domain_id': None, - 'enabled': True, + 'description': self.idp.description, + 'is_enabled': True, } - self.identity_providers_mock.create.assert_called_with( - id=identity_fakes.idp_id, **kwargs + self.identity_sdk_client.create_identity_provider.assert_called_with( + id=self.idp.id, **kwargs ) self.assertEqual(self.columns, columns) @@ -121,27 +111,25 @@ def test_create_identity_provider_description(self): def test_create_identity_provider_remote_id(self): arglist = [ - identity_fakes.idp_id, + self.idp.id, '--remote-id', - identity_fakes.idp_remote_ids[0], + self.idp.remote_ids[0], ] verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), - ('remote_id', identity_fakes.idp_remote_ids[:1]), + ('identity_provider_id', self.idp.id), + ('remote_ids', self.idp.remote_ids[:1]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'remote_ids': identity_fakes.idp_remote_ids[:1], - 'description': None, - 'domain_id': None, - 'enabled': True, + 'remote_ids': self.idp.remote_ids[:1], + 'is_enabled': True, } - self.identity_providers_mock.create.assert_called_with( - id=identity_fakes.idp_id, **kwargs + self.identity_sdk_client.create_identity_provider.assert_called_with( + id=self.idp.id, **kwargs ) self.assertEqual(self.columns, columns) @@ -150,28 +138,26 @@ def test_create_identity_provider_remote_id(self): def test_create_identity_provider_remote_ids_multiple(self): arglist = [ '--remote-id', - identity_fakes.idp_remote_ids[0], + self.idp.remote_ids[0], '--remote-id', - identity_fakes.idp_remote_ids[1], - identity_fakes.idp_id, + self.idp.remote_ids[1], + self.idp.id, ] verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), - ('remote_id', identity_fakes.idp_remote_ids), + ('identity_provider_id', self.idp.id), + ('remote_ids', self.idp.remote_ids), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'remote_ids': identity_fakes.idp_remote_ids, - 'description': None, - 'domain_id': None, - 'enabled': True, + 'remote_ids': self.idp.remote_ids, + 'is_enabled': True, } - self.identity_providers_mock.create.assert_called_with( - id=identity_fakes.idp_id, **kwargs + self.identity_sdk_client.create_identity_provider.assert_called_with( + id=self.idp.id, **kwargs ) self.assertEqual(self.columns, columns) @@ -181,16 +167,16 @@ def test_create_identity_provider_remote_ids_file(self): arglist = [ '--remote-id-file', '/tmp/file_name', - identity_fakes.idp_id, + self.idp.id, ] verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), + ('identity_provider_id', self.idp.id), ('remote_id_file', '/tmp/file_name'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) mocker = mock.Mock() - mocker.return_value = "\n".join(identity_fakes.idp_remote_ids) + mocker.return_value = "\n".join(self.idp.remote_ids) with mock.patch( "openstackclient.identity.v3.identity_provider." "utils.read_blob_file_contents", @@ -200,83 +186,80 @@ def test_create_identity_provider_remote_ids_file(self): # Set expected values kwargs = { - 'remote_ids': identity_fakes.idp_remote_ids, - 'description': None, - 'domain_id': None, - 'enabled': True, + 'remote_ids': self.idp.remote_ids, + 'is_enabled': True, } - self.identity_providers_mock.create.assert_called_with( - id=identity_fakes.idp_id, **kwargs + self.identity_sdk_client.create_identity_provider.assert_called_with( + id=self.idp.id, **kwargs ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.datalist, data) def test_create_identity_provider_disabled(self): - # Prepare FakeResource object - IDENTITY_PROVIDER = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) - IDENTITY_PROVIDER['enabled'] = False - IDENTITY_PROVIDER['description'] = None - - resource = fakes.FakeResource(None, IDENTITY_PROVIDER, loaded=True) - self.identity_providers_mock.create.return_value = resource + idp_disabled = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + domain_id=self.domain.id, + remote_ids=['entity1', 'entity2'], + is_enabled=False, + ) + self.identity_sdk_client.create_identity_provider.return_value = ( + idp_disabled + ) + self.cmd = identity_provider.CreateIdentityProvider(self.app, None) arglist = [ '--disable', - identity_fakes.idp_id, + idp_disabled.id, ] verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), + ('identity_provider_id', idp_disabled.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'remote_ids': None, - 'enabled': False, - 'description': None, - 'domain_id': None, + 'is_enabled': False, } - self.identity_providers_mock.create.assert_called_with( - id=identity_fakes.idp_id, **kwargs + self.identity_sdk_client.create_identity_provider.assert_called_with( + id=idp_disabled.id, **kwargs ) self.assertEqual(self.columns, columns) datalist = ( - None, - identity_fakes.domain_id, + idp_disabled.authorization_ttl, + idp_disabled.description, + self.domain.id, False, - identity_fakes.idp_id, - identity_fakes.formatted_idp_remote_ids, + idp_disabled.id, + format_columns.ListColumn(self.idp.remote_ids), ) self.assertCountEqual(datalist, data) def test_create_identity_provider_domain_name(self): arglist = [ '--domain', - identity_fakes.domain_name, - identity_fakes.idp_id, + self.domain.name, + self.idp.id, ] verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), - ('domain', identity_fakes.domain_name), + ('identity_provider_id', self.idp.id), + ('domain', self.domain.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'remote_ids': None, - 'description': None, - 'domain_id': identity_fakes.domain_id, - 'enabled': True, + 'domain_id': self.domain.id, + 'is_enabled': True, } - self.identity_providers_mock.create.assert_called_with( - id=identity_fakes.idp_id, **kwargs + self.identity_sdk_client.create_identity_provider.assert_called_with( + id=self.idp.id, **kwargs ) self.assertEqual(self.columns, columns) @@ -285,26 +268,24 @@ def test_create_identity_provider_domain_name(self): def test_create_identity_provider_domain_id(self): arglist = [ '--domain', - identity_fakes.domain_id, - identity_fakes.idp_id, + self.domain.id, + self.idp.id, ] verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), - ('domain', identity_fakes.domain_id), + ('identity_provider_id', self.idp.id), + ('domain', self.domain.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'remote_ids': None, - 'description': None, - 'domain_id': identity_fakes.domain_id, - 'enabled': True, + 'domain_id': self.domain.id, + 'is_enabled': True, } - self.identity_providers_mock.create.assert_called_with( - id=identity_fakes.idp_id, **kwargs + self.identity_sdk_client.create_identity_provider.assert_called_with( + id=self.idp.id, **kwargs ) self.assertEqual(self.columns, columns) @@ -314,10 +295,10 @@ def test_create_identity_provider_authttl_positive(self): arglist = [ '--authorization-ttl', '60', - identity_fakes.idp_id, + self.idp.id, ] verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), + ('identity_provider_id', self.idp.id), ('authorization_ttl', 60), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -325,15 +306,12 @@ def test_create_identity_provider_authttl_positive(self): # Set expected values kwargs = { - 'remote_ids': None, - 'description': None, - 'domain_id': None, - 'enabled': True, + 'is_enabled': True, 'authorization_ttl': 60, } - self.identity_providers_mock.create.assert_called_with( - id=identity_fakes.idp_id, **kwargs + self.identity_sdk_client.create_identity_provider.assert_called_with( + id=self.idp.id, **kwargs ) self.assertEqual(self.columns, columns) @@ -343,10 +321,10 @@ def test_create_identity_provider_authttl_zero(self): arglist = [ '--authorization-ttl', '0', - identity_fakes.idp_id, + self.idp.id, ] verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), + ('identity_provider_id', self.idp.id), ('authorization_ttl', 0), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -354,15 +332,12 @@ def test_create_identity_provider_authttl_zero(self): # Set expected values kwargs = { - 'remote_ids': None, - 'description': None, - 'domain_id': None, - 'enabled': True, + 'is_enabled': True, 'authorization_ttl': 0, } - self.identity_providers_mock.create.assert_called_with( - id=identity_fakes.idp_id, **kwargs + self.identity_sdk_client.create_identity_provider.assert_called_with( + id=self.idp.id, **kwargs ) self.assertEqual(self.columns, columns) @@ -372,10 +347,10 @@ def test_create_identity_provider_authttl_negative(self): arglist = [ '--authorization-ttl', '-60', - identity_fakes.idp_id, + self.idp.id, ] verifylist = [ - ('identity_provider_id', identity_fakes.idp_id), + ('identity_provider_id', self.idp.id), ('authorization_ttl', -60), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -387,7 +362,7 @@ def test_create_identity_provider_authttl_not_int(self): arglist = [ '--authorization-ttl', 'spam', - identity_fakes.idp_id, + self.idp.id, ] verifylist = [] self.assertRaises( @@ -399,53 +374,46 @@ def test_create_identity_provider_authttl_not_int(self): ) -class TestIdentityProviderDelete(TestIdentityProvider): +class TestIdentityProviderDelete(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - # This is the return value for utils.find_resource() - self.identity_providers_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True, + self.idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider ) - self.identity_providers_mock.delete.return_value = None + self.identity_sdk_client.delete_identity_provider.return_value = None self.cmd = identity_provider.DeleteIdentityProvider(self.app, None) def test_delete_identity_provider(self): arglist = [ - identity_fakes.idp_id, + self.idp.id, ] verifylist = [ - ('identity_provider', [identity_fakes.idp_id]), + ('identity_provider', [self.idp.id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.identity_providers_mock.delete.assert_called_with( - identity_fakes.idp_id, + self.identity_sdk_client.delete_identity_provider.assert_called_with( + self.idp.id, ) self.assertIsNone(result) -class TestIdentityProviderList(TestIdentityProvider): +class TestIdentityProviderList(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - self.identity_providers_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True, + self.domain = sdk_fakes.generate_fake_resource(_domain.Domain) + self.idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + domain_id=self.domain.id, + remote_ids=['entity1', 'entity2'], ) - self.identity_providers_mock.list.return_value = [ - fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True, - ), - ] + + self.identity_sdk_client.identity_providers.return_value = [self.idp] # Get the command object to test self.cmd = identity_provider.ListIdentityProvider(self.app, None) @@ -460,23 +428,23 @@ def test_identity_provider_list_no_options(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.identity_providers_mock.list.assert_called_with() + self.identity_sdk_client.identity_providers.assert_called_with() collist = ('ID', 'Enabled', 'Domain ID', 'Description') self.assertEqual(collist, columns) datalist = ( ( - identity_fakes.idp_id, + self.idp.id, True, - identity_fakes.domain_id, - identity_fakes.idp_description, + self.domain.id, + self.idp.description, ), ) self.assertCountEqual(datalist, tuple(data)) def test_identity_provider_list_ID_option(self): - arglist = ['--id', identity_fakes.idp_id] - verifylist = [('id', identity_fakes.idp_id)] + arglist = ['--id', self.idp.id] + verifylist = [('id', self.idp.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) # In base command class Lister in cliff, abstract method take_action() @@ -484,17 +452,19 @@ def test_identity_provider_list_ID_option(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - kwargs = {'id': identity_fakes.idp_id} - self.identity_providers_mock.list.assert_called_with(**kwargs) + kwargs = {'id': self.idp.id} + self.identity_sdk_client.identity_providers.assert_called_with( + **kwargs + ) collist = ('ID', 'Enabled', 'Domain ID', 'Description') self.assertEqual(collist, columns) datalist = ( ( - identity_fakes.idp_id, + self.idp.id, True, - identity_fakes.domain_id, - identity_fakes.idp_description, + self.domain.id, + self.idp.description, ), ) self.assertCountEqual(datalist, tuple(data)) @@ -509,38 +479,35 @@ def test_identity_provider_list_enabled_option(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - kwargs = {'enabled': True} - self.identity_providers_mock.list.assert_called_with(**kwargs) + kwargs = {'is_enabled': True} + self.identity_sdk_client.identity_providers.assert_called_with( + **kwargs + ) collist = ('ID', 'Enabled', 'Domain ID', 'Description') self.assertEqual(collist, columns) datalist = ( ( - identity_fakes.idp_id, + self.idp.id, True, - identity_fakes.domain_id, - identity_fakes.idp_description, + self.domain.id, + self.idp.description, ), ) self.assertCountEqual(datalist, tuple(data)) -class TestIdentityProviderSet(TestIdentityProvider): - columns = ( - 'description', - 'enabled', - 'id', - 'remote_ids', - ) - datalist = ( - identity_fakes.idp_description, - True, - identity_fakes.idp_id, - identity_fakes.idp_remote_ids, - ) - +class TestIdentityProviderSet(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() + + self.domain = sdk_fakes.generate_fake_resource(_domain.Domain) + self.idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + domain_id=self.domain.id, + remote_ids=['entity1', 'entity2'], + ) + self.cmd = identity_provider.SetIdentityProvider(self.app, None) def test_identity_provider_set_description(self): @@ -548,26 +515,33 @@ def test_identity_provider_set_description(self): def prepare(self): """Prepare fake return objects before the test is executed""" - updated_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) - updated_idp['enabled'] = False - resources = fakes.FakeResource(None, updated_idp, loaded=True) - self.identity_providers_mock.update.return_value = resources + new_description = 'new desc' + + self.updated_idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + id=self.idp.id, + description=new_description, + domain_id=self.domain.id, + remote_ids=self.idp.remote_ids, + ) + self.identity_sdk_client.update_identity_provider.return_value = ( + self.updated_idp + ) prepare(self) - new_description = 'new desc' - arglist = ['--description', new_description, identity_fakes.idp_id] + arglist = ['--description', self.updated_idp.description, self.idp.id] verifylist = [ - ('identity_provider', identity_fakes.idp_id), - ('description', new_description), + ('identity_provider', self.idp.id), + ('description', self.updated_idp.description), ('enable', False), ('disable', False), - ('remote_id', None), + ('remote_ids', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.identity_providers_mock.update.assert_called_with( - identity_fakes.idp_id, - description=new_description, + self.identity_sdk_client.update_identity_provider.assert_called_with( + self.idp.id, + description=self.updated_idp.description, ) def test_identity_provider_disable(self): @@ -578,34 +552,40 @@ def test_identity_provider_disable(self): def prepare(self): """Prepare fake return objects before the test is executed""" - updated_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) - updated_idp['enabled'] = False - resources = fakes.FakeResource(None, updated_idp, loaded=True) - self.identity_providers_mock.update.return_value = resources + self.updated_idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + id=self.idp.id, + enabled=False, + domain_id=self.domain.id, + remote_ids=self.idp.remote_ids, + ) + self.identity_sdk_client.update_identity_provider.return_value = ( + self.updated_idp + ) prepare(self) arglist = [ '--disable', - identity_fakes.idp_id, + self.idp.id, '--remote-id', - identity_fakes.idp_remote_ids[0], + self.idp.remote_ids[0], '--remote-id', - identity_fakes.idp_remote_ids[1], + self.idp.remote_ids[1], ] verifylist = [ - ('identity_provider', identity_fakes.idp_id), + ('identity_provider', self.idp.id), ('description', None), ('enable', False), ('disable', True), - ('remote_id', identity_fakes.idp_remote_ids), + ('remote_ids', self.idp.remote_ids), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.identity_providers_mock.update.assert_called_with( - identity_fakes.idp_id, - enabled=False, - remote_ids=identity_fakes.idp_remote_ids, + self.identity_sdk_client.update_identity_provider.assert_called_with( + self.idp.id, + is_enabled=False, + remote_ids=self.idp.remote_ids, ) def test_identity_provider_enable(self): @@ -616,36 +596,40 @@ def test_identity_provider_enable(self): def prepare(self): """Prepare fake return objects before the test is executed""" - resources = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True, + self.updated_idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + id=self.idp.id, + is_enabled=True, + domain_id=self.domain.id, + remote_ids=self.idp.remote_ids, + ) + self.identity_sdk_client.update_identity_provider.return_value = ( + self.updated_idp ) - self.identity_providers_mock.update.return_value = resources prepare(self) arglist = [ '--enable', - identity_fakes.idp_id, + self.idp.id, '--remote-id', - identity_fakes.idp_remote_ids[0], + self.idp.remote_ids[0], '--remote-id', - identity_fakes.idp_remote_ids[1], + self.idp.remote_ids[1], ] verifylist = [ - ('identity_provider', identity_fakes.idp_id), + ('identity_provider', self.idp.id), ('description', None), ('enable', True), ('disable', False), - ('remote_id', identity_fakes.idp_remote_ids), + ('remote_ids', self.idp.remote_ids), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.identity_providers_mock.update.assert_called_with( - identity_fakes.idp_id, - enabled=True, - remote_ids=identity_fakes.idp_remote_ids, + self.identity_sdk_client.update_identity_provider.assert_called_with( + self.idp.id, + is_enabled=True, + remote_ids=self.idp.remote_ids, ) def test_identity_provider_replace_remote_ids(self): @@ -658,31 +642,36 @@ def prepare(self): """Prepare fake return objects before the test is executed""" self.new_remote_id = 'new_entity' - updated_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) - updated_idp['remote_ids'] = [self.new_remote_id] - resources = fakes.FakeResource(None, updated_idp, loaded=True) - self.identity_providers_mock.update.return_value = resources + self.updated_idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + id=self.idp.id, + domain_id=self.domain.id, + remote_ids=[self.new_remote_id], + ) + self.identity_sdk_client.update_identity_provider.return_value = ( + self.updated_idp + ) prepare(self) arglist = [ '--enable', - identity_fakes.idp_id, + self.idp.id, '--remote-id', self.new_remote_id, ] verifylist = [ - ('identity_provider', identity_fakes.idp_id), + ('identity_provider', self.idp.id), ('description', None), ('enable', True), ('disable', False), - ('remote_id', [self.new_remote_id]), + ('remote_ids', [self.new_remote_id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.identity_providers_mock.update.assert_called_with( - identity_fakes.idp_id, - enabled=True, + self.identity_sdk_client.update_identity_provider.assert_called_with( + self.idp.id, + is_enabled=True, remote_ids=[self.new_remote_id], ) @@ -696,20 +685,25 @@ def prepare(self): """Prepare fake return objects before the test is executed""" self.new_remote_id = 'new_entity' - updated_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) - updated_idp['remote_ids'] = [self.new_remote_id] - resources = fakes.FakeResource(None, updated_idp, loaded=True) - self.identity_providers_mock.update.return_value = resources + self.updated_idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + id=self.idp.id, + domain_id=self.domain.id, + remote_ids=[self.new_remote_id], + ) + self.identity_sdk_client.update_identity_provider.return_value = ( + self.updated_idp + ) prepare(self) arglist = [ '--enable', - identity_fakes.idp_id, + self.idp.id, '--remote-id-file', self.new_remote_id, ] verifylist = [ - ('identity_provider', identity_fakes.idp_id), + ('identity_provider', self.idp.id), ('description', None), ('enable', True), ('disable', False), @@ -725,38 +719,34 @@ def prepare(self): mocker, ): self.cmd.take_action(parsed_args) - self.identity_providers_mock.update.assert_called_with( - identity_fakes.idp_id, - enabled=True, + self.identity_sdk_client.update_identity_provider.assert_called_with( + self.idp.id, + is_enabled=True, remote_ids=[self.new_remote_id], ) def test_identity_provider_no_options(self): def prepare(self): """Prepare fake return objects before the test is executed""" - resources = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True, + self.updated_idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + id=self.idp.id, + domain_id=self.domain.id, + remote_ids=self.idp.remote_ids, ) - self.identity_providers_mock.get.return_value = resources - - resources = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True, + self.identity_sdk_client.update_identity_provider.return_value = ( + self.updated_idp ) - self.identity_providers_mock.update.return_value = resources prepare(self) arglist = [ - identity_fakes.idp_id, + self.idp.id, ] verifylist = [ - ('identity_provider', identity_fakes.idp_id), + ('identity_provider', self.idp.id), ('enable', False), ('disable', False), - ('remote_id', None), + ('remote_ids', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -765,58 +755,70 @@ def prepare(self): def test_identity_provider_set_authttl_positive(self): def prepare(self): """Prepare fake return objects before the test is executed""" - updated_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) - updated_idp['authorization_ttl'] = 60 - resources = fakes.FakeResource(None, updated_idp, loaded=True) - self.identity_providers_mock.update.return_value = resources + self.updated_idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + id=self.idp.id, + authorization_ttl=60, + domain_id=self.domain.id, + remote_ids=self.idp.remote_ids, + ) + self.identity_sdk_client.update_identity_provider.return_value = ( + self.updated_idp + ) prepare(self) - arglist = ['--authorization-ttl', '60', identity_fakes.idp_id] + arglist = ['--authorization-ttl', '60', self.idp.id] verifylist = [ - ('identity_provider', identity_fakes.idp_id), + ('identity_provider', self.idp.id), ('enable', False), ('disable', False), - ('remote_id', None), + ('remote_ids', None), ('authorization_ttl', 60), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.identity_providers_mock.update.assert_called_with( - identity_fakes.idp_id, + self.identity_sdk_client.update_identity_provider.assert_called_with( + self.idp.id, authorization_ttl=60, ) def test_identity_provider_set_authttl_zero(self): def prepare(self): """Prepare fake return objects before the test is executed""" - updated_idp = copy.deepcopy(identity_fakes.IDENTITY_PROVIDER) - updated_idp['authorization_ttl'] = 0 - resources = fakes.FakeResource(None, updated_idp, loaded=True) - self.identity_providers_mock.update.return_value = resources + self.updated_idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + id=self.idp.id, + authorization_ttl=0, + domain_id=self.domain.id, + remote_ids=self.idp.remote_ids, + ) + self.identity_sdk_client.update_identity_provider.return_value = ( + self.updated_idp + ) prepare(self) - arglist = ['--authorization-ttl', '0', identity_fakes.idp_id] + arglist = ['--authorization-ttl', '0', self.idp.id] verifylist = [ - ('identity_provider', identity_fakes.idp_id), + ('identity_provider', self.idp.id), ('enable', False), ('disable', False), - ('remote_id', None), + ('remote_ids', None), ('authorization_ttl', 0), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.identity_providers_mock.update.assert_called_with( - identity_fakes.idp_id, + self.identity_sdk_client.update_identity_provider.assert_called_with( + self.idp.id, authorization_ttl=0, ) def test_identity_provider_set_authttl_negative(self): - arglist = ['--authorization-ttl', '-1', identity_fakes.idp_id] + arglist = ['--authorization-ttl', '-1', self.idp.id] verifylist = [ - ('identity_provider', identity_fakes.idp_id), + ('identity_provider', self.idp.id), ('enable', False), ('disable', False), - ('remote_id', None), + ('remote_ids', None), ('authorization_ttl', -1), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -825,7 +827,7 @@ def test_identity_provider_set_authttl_negative(self): ) def test_identity_provider_set_authttl_not_int(self): - arglist = ['--authorization-ttl', 'spam', identity_fakes.idp_id] + arglist = ['--authorization-ttl', 'spam', self.idp.id] verifylist = [] self.assertRaises( test_utils.ParserException, @@ -836,47 +838,49 @@ def test_identity_provider_set_authttl_not_int(self): ) -class TestIdentityProviderShow(TestIdentityProvider): +class TestIdentityProviderShow(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - ret = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.IDENTITY_PROVIDER), - loaded=True, + self.idp = sdk_fakes.generate_fake_resource( + _identity_provider.IdentityProvider, + remote_ids=['entity1', 'entity2'], ) - self.identity_providers_mock.get.side_effect = [ - Exception("Not found"), - ret, - ] - self.identity_providers_mock.get.return_value = ret + self.identity_sdk_client.get_identity_provider.return_value = self.idp - # Get the command object to test self.cmd = identity_provider.ShowIdentityProvider(self.app, None) def test_identity_provider_show(self): arglist = [ - identity_fakes.idp_id, + self.idp.id, ] verifylist = [ - ('identity_provider', identity_fakes.idp_id), + ('identity_provider', self.idp.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.identity_providers_mock.get.assert_called_with( - identity_fakes.idp_id, id='test_idp' + self.identity_sdk_client.get_identity_provider.assert_called_with( + self.idp.id, ) - collist = ('description', 'domain_id', 'enabled', 'id', 'remote_ids') + collist = ( + 'authorization_ttl', + 'description', + 'domain_id', + 'enabled', + 'id', + 'remote_ids', + ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.idp_description, - identity_fakes.domain_id, + self.idp.authorization_ttl, + self.idp.description, + self.idp.domain_id, True, - identity_fakes.idp_id, - identity_fakes.formatted_idp_remote_ids, + self.idp.id, + format_columns.ListColumn(self.idp.remote_ids), ) self.assertCountEqual(datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/test_implied_role.py b/openstackclient/tests/unit/identity/v3/test_implied_role.py index f6bc6063f6..3f81b047c3 100644 --- a/openstackclient/tests/unit/identity/v3/test_implied_role.py +++ b/openstackclient/tests/unit/identity/v3/test_implied_role.py @@ -20,7 +20,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestRole(identity_fakes.TestIdentityv3): +class TestRole(identity_fakes.TestIdentity): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/identity/v3/test_limit.py b/openstackclient/tests/unit/identity/v3/test_limit.py index a1d180b4ba..f790e1d4ba 100644 --- a/openstackclient/tests/unit/identity/v3/test_limit.py +++ b/openstackclient/tests/unit/identity/v3/test_limit.py @@ -10,87 +10,85 @@ # License for the specific language governing permissions and limitations # under the License. -import copy - -from keystoneauth1.exceptions import http as ksa_exceptions +from openstack import exceptions as sdk_exc +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import limit as _limit +from openstack.identity.v3 import project as _project +from openstack.identity.v3 import region as _region +from openstack.identity.v3 import service as _service +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.identity.v3 import limit -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestLimit(identity_fakes.TestIdentityv3): +class TestLimitCreate(identity_fakes.TestIdentity): def setUp(self): super().setUp() - identity_manager = self.identity_client - - self.limit_mock = identity_manager.limits - - self.services_mock = identity_manager.services - self.services_mock.reset_mock() - - self.projects_mock = identity_manager.projects - self.projects_mock.reset_mock() - - self.regions_mock = identity_manager.regions - self.regions_mock.reset_mock() + self.domain = sdk_fakes.generate_fake_resource(_domain.Domain) + self.project = sdk_fakes.generate_fake_resource( + _project.Project, domain_id=self.domain.id + ) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.resource_limit = 15 -class TestLimitCreate(TestLimit): - def setUp(self): - super().setUp() - - self.service = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.SERVICE), loaded=True - ) - self.services_mock.get.return_value = self.service + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + self.identity_sdk_client.find_project.return_value = self.project + self.identity_sdk_client.find_domain.return_value = self.domain - self.project = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.PROJECT), loaded=True + self.limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_name='foobars', + description=None, + resource_limit=self.resource_limit, + region_id=None, ) - self.projects_mock.get.return_value = self.project - - self.region = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.REGION), loaded=True + self.limit_with_options = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_limit=self.resource_limit, + resource_name='foobars', + description='test description', + region_id=self.region.id, ) - self.regions_mock.get.return_value = self.region self.cmd = limit.CreateLimit(self.app, None) def test_limit_create_without_options(self): - self.limit_mock.create.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.LIMIT), loaded=True - ) + self.identity_sdk_client.create_limit.return_value = self.limit - resource_limit = 15 arglist = [ '--project', - identity_fakes.project_id, + self.project.id, '--service', - identity_fakes.service_id, + self.service.id, '--resource-limit', - str(resource_limit), - identity_fakes.limit_resource_name, + str(self.resource_limit), + self.limit.resource_name, ] verifylist = [ - ('project', identity_fakes.project_id), - ('service', identity_fakes.service_id), - ('resource_name', identity_fakes.limit_resource_name), - ('resource_limit', resource_limit), + ('project', self.project.id), + ('service', self.service.id), + ('resource_name', self.limit.resource_name), + ('resource_limit', self.resource_limit), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - kwargs = {'description': None, 'region': None} - self.limit_mock.create.assert_called_with( - self.project, - self.service, - identity_fakes.limit_resource_name, - resource_limit, - **kwargs, + self.identity_sdk_client.create_limit.assert_called_with( + project_id=self.project.id, + service_id=self.service.id, + resource_name=self.limit.resource_name, + resource_limit=self.resource_limit, ) collist = ( @@ -105,55 +103,58 @@ def test_limit_create_without_options(self): self.assertEqual(collist, columns) datalist = ( None, - identity_fakes.limit_id, - identity_fakes.project_id, + self.limit.id, + self.project.id, None, - resource_limit, - identity_fakes.limit_resource_name, - identity_fakes.service_id, + self.resource_limit, + self.limit.resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_limit_create_with_options(self): - self.limit_mock.create.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.LIMIT_OPTIONS), loaded=True + self.identity_sdk_client.create_limit.return_value = ( + self.limit_with_options ) resource_limit = 15 arglist = [ '--project', - identity_fakes.project_id, + self.project.id, + '--project-domain', + self.domain.name, '--service', - identity_fakes.service_id, + self.service.id, '--resource-limit', str(resource_limit), '--region', - identity_fakes.region_id, + self.region.id, '--description', - identity_fakes.limit_description, - identity_fakes.limit_resource_name, + self.limit_with_options.description, + self.limit_with_options.resource_name, ] verifylist = [ - ('project', identity_fakes.project_id), - ('service', identity_fakes.service_id), - ('resource_name', identity_fakes.limit_resource_name), + ('project', self.project.id), + ('project_domain', self.domain.name), + ('service', self.service.id), + ('resource_name', self.limit_with_options.resource_name), ('resource_limit', resource_limit), - ('region', identity_fakes.region_id), - ('description', identity_fakes.limit_description), + ('region', self.region.id), + ('description', self.limit_with_options.description), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) kwargs = { - 'description': identity_fakes.limit_description, - 'region': self.region, + 'project_id': self.project.id, + 'service_id': self.service.id, + 'region_id': self.region.id, + 'resource_name': self.limit_with_options.resource_name, + 'resource_limit': resource_limit, + 'description': self.limit_with_options.description, } - self.limit_mock.create.assert_called_with( - self.project, - self.service, - identity_fakes.limit_resource_name, - resource_limit, + self.identity_sdk_client.create_limit.assert_called_with( **kwargs, ) @@ -168,37 +169,41 @@ def test_limit_create_with_options(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.limit_description, - identity_fakes.limit_id, - identity_fakes.project_id, - identity_fakes.region_id, + self.limit_with_options.description, + self.limit_with_options.id, + self.project.id, + self.region.id, resource_limit, - identity_fakes.limit_resource_name, - identity_fakes.service_id, + self.limit_with_options.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestLimitDelete(TestLimit): +class TestLimitDelete(identity_fakes.TestIdentity): def setUp(self): super().setUp() self.cmd = limit.DeleteLimit(self.app, None) def test_limit_delete(self): - self.limit_mock.delete.return_value = None + self.limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit + ) + self.identity_sdk_client.delete_limit.return_value = None - arglist = [identity_fakes.limit_id] - verifylist = [('limit_id', [identity_fakes.limit_id])] + arglist = [self.limit.id] + verifylist = [('limit_id', [self.limit.id])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.limit_mock.delete.assert_called_with(identity_fakes.limit_id) + self.identity_sdk_client.delete_limit.assert_called_with(self.limit.id) self.assertIsNone(result) def test_limit_delete_with_exception(self): - return_value = ksa_exceptions.NotFound() - self.limit_mock.delete.side_effect = return_value + self.identity_sdk_client.delete_limit.side_effect = ( + sdk_exc.ResourceNotFound + ) arglist = ['fake-limit-id'] verifylist = [('limit_id', ['fake-limit-id'])] @@ -211,24 +216,42 @@ def test_limit_delete_with_exception(self): self.assertEqual('1 of 1 limits failed to delete.', str(e)) -class TestLimitShow(TestLimit): +class TestLimitShow(identity_fakes.TestIdentity): def setUp(self): super().setUp() - self.limit_mock.get.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.LIMIT), loaded=True + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + + self.resource_limit = 15 + + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + self.identity_sdk_client.find_project.return_value = self.project + + self.limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_name='foobars', + description=None, + resource_limit=self.resource_limit, + region_id=None, ) + self.identity_sdk_client.get_limit.return_value = self.limit + self.cmd = limit.ShowLimit(self.app, None) def test_limit_show(self): - arglist = [identity_fakes.limit_id] - verifylist = [('limit_id', identity_fakes.limit_id)] + arglist = [self.limit.id] + verifylist = [('limit_id', self.limit.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.limit_mock.get.assert_called_with(identity_fakes.limit_id) + self.identity_sdk_client.get_limit.assert_called_with(self.limit.id) collist = ( 'description', @@ -242,45 +265,61 @@ def test_limit_show(self): self.assertEqual(collist, columns) datalist = ( None, - identity_fakes.limit_id, - identity_fakes.project_id, + self.limit.id, + self.project.id, None, - identity_fakes.limit_resource_limit, - identity_fakes.limit_resource_name, - identity_fakes.service_id, + self.resource_limit, + self.limit.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestLimitSet(TestLimit): +class TestLimitSet(identity_fakes.TestIdentity): def setUp(self): super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + + self.resource_limit = 15 + + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + self.identity_sdk_client.find_project.return_value = self.project + self.cmd = limit.SetLimit(self.app, None) def test_limit_set_description(self): - limit = copy.deepcopy(identity_fakes.LIMIT) - limit['description'] = identity_fakes.limit_description - self.limit_mock.update.return_value = fakes.FakeResource( - None, limit, loaded=True + description = 'limit of foobars' + limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_name='foobars', + description=description, + resource_limit=self.resource_limit, + region_id=None, ) + self.identity_sdk_client.update_limit.return_value = limit arglist = [ '--description', - identity_fakes.limit_description, - identity_fakes.limit_id, + description, + limit.id, ] verifylist = [ - ('description', identity_fakes.limit_description), - ('limit_id', identity_fakes.limit_id), + ('description', description), + ('limit_id', limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.limit_mock.update.assert_called_with( - identity_fakes.limit_id, - description=identity_fakes.limit_description, - resource_limit=None, + self.identity_sdk_client.update_limit.assert_called_with( + limit.id, + description=description, ) collist = ( @@ -294,40 +333,44 @@ def test_limit_set_description(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.limit_description, - identity_fakes.limit_id, - identity_fakes.project_id, + description, + limit.id, + self.project.id, None, - identity_fakes.limit_resource_limit, - identity_fakes.limit_resource_name, - identity_fakes.service_id, + limit.resource_limit, + limit.resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_limit_set_resource_limit(self): resource_limit = 20 - limit = copy.deepcopy(identity_fakes.LIMIT) - limit['resource_limit'] = resource_limit - self.limit_mock.update.return_value = fakes.FakeResource( - None, limit, loaded=True + limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_name='foobars', + description=None, + resource_limit=resource_limit, + region_id=None, ) + self.identity_sdk_client.update_limit.return_value = limit arglist = [ '--resource-limit', str(resource_limit), - identity_fakes.limit_id, + limit.id, ] verifylist = [ ('resource_limit', resource_limit), - ('limit_id', identity_fakes.limit_id), + ('limit_id', limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.limit_mock.update.assert_called_with( - identity_fakes.limit_id, - description=None, + self.identity_sdk_client.update_limit.assert_called_with( + limit.id, resource_limit=resource_limit, ) @@ -343,25 +386,36 @@ def test_limit_set_resource_limit(self): self.assertEqual(collist, columns) datalist = ( None, - identity_fakes.limit_id, - identity_fakes.project_id, + limit.id, + self.project.id, None, resource_limit, - identity_fakes.limit_resource_name, - identity_fakes.service_id, + limit.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestLimitList(TestLimit): +class TestLimitList(identity_fakes.TestIdentity): def setUp(self): super().setUp() - self.limit_mock.list.return_value = [ - fakes.FakeResource( - None, copy.deepcopy(identity_fakes.LIMIT), loaded=True - ) - ] + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + + self.resource_limit = 15 + + self.limit = sdk_fakes.generate_fake_resource( + resource_type=_limit.Limit, + project_id=self.project.id, + service_id=self.service.id, + resource_name='foobars', + description=None, + resource_limit=self.resource_limit, + region_id=None, + ) + self.identity_sdk_client.limits.return_value = [self.limit] self.cmd = limit.ListLimit(self.app, None) @@ -372,9 +426,7 @@ def test_limit_list(self): columns, data = self.cmd.take_action(parsed_args) - self.limit_mock.list.assert_called_with( - service=None, resource_name=None, region=None, project=None - ) + self.identity_sdk_client.limits.assert_called_with() collist = ( 'ID', @@ -388,11 +440,11 @@ def test_limit_list(self): self.assertEqual(collist, columns) datalist = ( ( - identity_fakes.limit_id, - identity_fakes.project_id, - identity_fakes.service_id, - identity_fakes.limit_resource_name, - identity_fakes.limit_resource_limit, + self.limit.id, + self.project.id, + self.service.id, + self.limit.resource_name, + self.limit.resource_limit, None, None, ), diff --git a/openstackclient/tests/unit/identity/v3/test_mappings.py b/openstackclient/tests/unit/identity/v3/test_mappings.py index 5a3fad97a8..daf663e3f6 100644 --- a/openstackclient/tests/unit/identity/v3/test_mappings.py +++ b/openstackclient/tests/unit/identity/v3/test_mappings.py @@ -12,109 +12,189 @@ # License for the specific language governing permissions and limitations # under the License. -import copy from unittest import mock from osc_lib import exceptions +from openstack.identity.v3 import mapping as _mapping +from openstack.test import fakes as sdk_fakes from openstackclient.identity.v3 import mapping -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestMapping(identity_fakes.TestFederatedIdentity): +MAPPING_RULES_FILE_PATH = '/tmp/path/to/file' +# Copied from +# https://opendev.org/openstack/keystone/src/branch/stable/2026.1/keystone/tests/unit/mapping_fixtures.py +EMPLOYEE_GROUP_ID = "0cd5e9" +CONTRACTOR_GROUP_ID = "85a868" +TESTER_GROUP_ID = "123" +DEVELOPER_GROUP_ID = "xyz" +MAPPING_RULES = [ + { + "local": [ + {"group": {"id": EMPLOYEE_GROUP_ID}}, + {"user": {"name": "{0}"}}, + ], + "remote": [ + {"type": "UserName"}, + { + "type": "orgPersonType", + "not_any_of": ["Contractor", "SubContractor"], + }, + {"type": "LastName", "any_one_of": ["Bo"]}, + ], + }, + { + "local": [ + {"group": {"id": CONTRACTOR_GROUP_ID}}, + {"user": {"name": "{0}"}}, + ], + "remote": [ + {"type": "UserName"}, + { + "type": "orgPersonType", + "any_one_of": ["Contractor", "SubContractor"], + }, + {"type": "FirstName", "any_one_of": ["Jill"]}, + ], + }, +] +MAPPING_RULES_2 = { + "rules": [ + { + "local": [ + { + "user": {"name": "{0} {1}", "email": "{2}"}, + "group": {"id": EMPLOYEE_GROUP_ID}, + } + ], + "remote": [ + {"type": "FirstName"}, + {"type": "LastName"}, + {"type": "Email"}, + { + "type": "orgPersonType", + "any_one_of": ["Admin", "Big Cheese"], + }, + ], + }, + { + "local": [{"user": {"name": "{0}", "email": "{1}"}}], + "remote": [ + {"type": "UserName"}, + {"type": "Email"}, + { + "type": "orgPersonType", + "not_any_of": [ + "Admin", + "Employee", + "Contractor", + "Tester", + ], + }, + ], + }, + { + "local": [ + {"group": {"id": TESTER_GROUP_ID}}, + {"group": {"id": DEVELOPER_GROUP_ID}}, + {"user": {"name": "{0}"}}, + ], + "remote": [ + {"type": "UserName"}, + {"type": "orgPersonType", "any_one_of": ["Tester"]}, + { + "type": "Email", + "any_one_of": [".*@example.com$"], + "regex": True, + }, + ], + }, + ] +} + + +class TestMappingCreate(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - federation_lib = self.identity_client.federation - self.mapping_mock = federation_lib.mappings - self.mapping_mock.reset_mock() - - -class TestMappingCreate(TestMapping): - def setUp(self): - super().setUp() - self.mapping_mock.create.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.MAPPING_RESPONSE), loaded=True + self.mapping = sdk_fakes.generate_fake_resource( + _mapping.Mapping, rules=MAPPING_RULES, schema_version=None ) + self.identity_sdk_client.create_mapping.return_value = self.mapping + self.cmd = mapping.CreateMapping(self.app, None) def test_create_mapping(self): arglist = [ '--rules', - identity_fakes.mapping_rules_file_path, - identity_fakes.mapping_id, + MAPPING_RULES_FILE_PATH, + self.mapping.id, ] verifylist = [ - ('mapping', identity_fakes.mapping_id), - ('rules', identity_fakes.mapping_rules_file_path), + ('mapping', self.mapping.id), + ('rules', MAPPING_RULES_FILE_PATH), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) mocker = mock.Mock() - mocker.return_value = identity_fakes.MAPPING_RULES + mocker.return_value = MAPPING_RULES with mock.patch( "openstackclient.identity.v3.mapping.CreateMapping._read_rules", mocker, ): columns, data = self.cmd.take_action(parsed_args) - self.mapping_mock.create.assert_called_with( - mapping_id=identity_fakes.mapping_id, - rules=identity_fakes.MAPPING_RULES, - schema_version=None, + self.identity_sdk_client.create_mapping.assert_called_with( + id=self.mapping.id, rules=MAPPING_RULES, schema_version=None ) - collist = ('id', 'rules') + collist = ('id', 'rules', 'schema_version') self.assertEqual(collist, columns) - datalist = (identity_fakes.mapping_id, identity_fakes.MAPPING_RULES) + datalist = (self.mapping.id, MAPPING_RULES, None) self.assertEqual(datalist, data) -class TestMappingDelete(TestMapping): +class TestMappingDelete(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - self.mapping_mock.get.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.MAPPING_RESPONSE), loaded=True - ) - self.mapping_mock.delete.return_value = None + self.mapping = sdk_fakes.generate_fake_resource( + _mapping.Mapping, rules=MAPPING_RULES, schema_version=None + ) + self.identity_sdk_client.delete_mapping.return_value = None self.cmd = mapping.DeleteMapping(self.app, None) def test_delete_mapping(self): - arglist = [identity_fakes.mapping_id] - verifylist = [('mapping', [identity_fakes.mapping_id])] + arglist = [self.mapping.id] + verifylist = [('mapping', [self.mapping.id])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.mapping_mock.delete.assert_called_with(identity_fakes.mapping_id) + self.identity_sdk_client.delete_mapping.assert_called_with( + self.mapping.id, ignore_missing=False + ) self.assertIsNone(result) -class TestMappingList(TestMapping): +class TestMappingList(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - self.mapping_mock.get.return_value = fakes.FakeResource( - None, {'id': identity_fakes.mapping_id}, loaded=True + + self.mapping = sdk_fakes.generate_fake_resource( + _mapping.Mapping, rules=MAPPING_RULES, schema_version='1.0' + ) + + self.extra_mapping = sdk_fakes.generate_fake_resource( + _mapping.Mapping, rules=MAPPING_RULES_2, schema_version='2.0' ) - # Pretend list command returns list of two mappings. - # NOTE(marek-denis): We are returning FakeResources with mapping id - # only as ShowMapping class is implemented in a way where rules will - # not be displayed, only mapping ids. - self.mapping_mock.list.return_value = [ - fakes.FakeResource( - None, - {'id': identity_fakes.mapping_id, 'schema_version': '1.0'}, - loaded=True, - ), - fakes.FakeResource( - None, - {'id': 'extra_mapping', 'schema_version': '2.0'}, - loaded=True, - ), + + self.identity_sdk_client.mappings.return_value = [ + self.mapping, + self.extra_mapping, ] # Get the command object to test @@ -127,29 +207,30 @@ def test_mapping_list(self): columns, data = self.cmd.take_action(parsed_args) - self.mapping_mock.list.assert_called_with() + self.identity_sdk_client.mappings.assert_called_with() collist = ('ID', 'schema_version') self.assertEqual(collist, columns) datalist = [ - (identity_fakes.mapping_id, '1.0'), - ('extra_mapping', '2.0'), + (self.mapping.id, '1.0'), + (self.extra_mapping.id, '2.0'), ] self.assertEqual(datalist, data) -class TestMappingSet(TestMapping): +class TestMappingSet(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - self.mapping_mock.get.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.MAPPING_RESPONSE), loaded=True + self.mapping = sdk_fakes.generate_fake_resource( + _mapping.Mapping, rules=MAPPING_RULES, schema_version='1.0' ) - - self.mapping_mock.update.return_value = fakes.FakeResource( - None, identity_fakes.MAPPING_RESPONSE_2, loaded=True + self.mapping_2 = sdk_fakes.generate_fake_resource( + _mapping.Mapping, rules=MAPPING_RULES_2, schema_version='2.0' ) + self.identity_sdk_client.get_mapping.return_value = self.mapping + self.identity_sdk_client.update_mapping.return_value = self.mapping_2 # Get the command object to test self.cmd = mapping.SetMapping(self.app, None) @@ -157,26 +238,26 @@ def setUp(self): def test_set_new_rules(self): arglist = [ '--rules', - identity_fakes.mapping_rules_file_path, - identity_fakes.mapping_id, + MAPPING_RULES_FILE_PATH, + self.mapping.id, ] verifylist = [ - ('mapping', identity_fakes.mapping_id), - ('rules', identity_fakes.mapping_rules_file_path), + ('mapping', self.mapping.id), + ('rules', MAPPING_RULES_FILE_PATH), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) mocker = mock.Mock() - mocker.return_value = identity_fakes.MAPPING_RULES_2 + mocker.return_value = MAPPING_RULES_2 with mock.patch( "openstackclient.identity.v3.mapping.SetMapping._read_rules", mocker, ): result = self.cmd.take_action(parsed_args) - self.mapping_mock.update.assert_called_with( - mapping=identity_fakes.mapping_id, - rules=identity_fakes.MAPPING_RULES_2, + self.identity_sdk_client.update_mapping.assert_called_with( + mapping=self.mapping.id, + rules=MAPPING_RULES_2, schema_version=None, ) @@ -185,12 +266,12 @@ def test_set_new_rules(self): def test_set_rules_wrong_file_path(self): arglist = [ '--rules', - identity_fakes.mapping_rules_file_path, - identity_fakes.mapping_id, + MAPPING_RULES_FILE_PATH, + self.mapping.id, ] verifylist = [ - ('mapping', identity_fakes.mapping_id), - ('rules', identity_fakes.mapping_rules_file_path), + ('mapping', self.mapping.id), + ('rules', MAPPING_RULES_FILE_PATH), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -199,27 +280,30 @@ def test_set_rules_wrong_file_path(self): ) -class TestMappingShow(TestMapping): +class TestMappingShow(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - self.mapping_mock.get.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.MAPPING_RESPONSE), loaded=True + self.mapping = sdk_fakes.generate_fake_resource( + _mapping.Mapping, rules=MAPPING_RULES, schema_version=None ) + self.identity_sdk_client.get_mapping.return_value = self.mapping self.cmd = mapping.ShowMapping(self.app, None) def test_mapping_show(self): - arglist = [identity_fakes.mapping_id] - verifylist = [('mapping', identity_fakes.mapping_id)] + arglist = [self.mapping.id] + verifylist = [('mapping', self.mapping.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.mapping_mock.get.assert_called_with(identity_fakes.mapping_id) + self.identity_sdk_client.get_mapping.assert_called_with( + self.mapping.id + ) - collist = ('id', 'rules') + collist = ('id', 'rules', 'schema_version') self.assertEqual(collist, columns) - datalist = (identity_fakes.mapping_id, identity_fakes.MAPPING_RULES) + datalist = (self.mapping.id, MAPPING_RULES, None) self.assertEqual(datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/test_oauth.py b/openstackclient/tests/unit/identity/v3/test_oauth.py index 576f8b20eb..9dcf0be89e 100644 --- a/openstackclient/tests/unit/identity/v3/test_oauth.py +++ b/openstackclient/tests/unit/identity/v3/test_oauth.py @@ -109,7 +109,7 @@ def test_authorize_request_tokens(self): ] verifylist = [ ('request_key', identity_fakes.request_token_id), - ('role', [identity_fakes.role_name]), + ('roles', [identity_fakes.role_name]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) diff --git a/openstackclient/tests/unit/identity/v3/test_policy.py b/openstackclient/tests/unit/identity/v3/test_policy.py new file mode 100644 index 0000000000..e3cc42af04 --- /dev/null +++ b/openstackclient/tests/unit/identity/v3/test_policy.py @@ -0,0 +1,322 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from osc_lib import exceptions + +from openstack.identity.v3 import policy as _policy +from openstack.test import fakes as sdk_fakes +from openstackclient.identity.v3 import policy +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes + + +POLICY_RULES_FILE_PATH = '/tmp/path/to/file' +# Copied from +# https://opendev.org/openstack/keystone/src/branch/stable/2026.1/keystone/tests/unit/mapping_fixtures.py +EMPLOYEE_GROUP_ID = "0cd5e9" +CONTRACTOR_GROUP_ID = "85a868" +TESTER_GROUP_ID = "123" +DEVELOPER_GROUP_ID = "xyz" +POLICY_RULES = [ + { + "local": [ + {"group": {"id": EMPLOYEE_GROUP_ID}}, + {"user": {"name": "{0}"}}, + ], + "remote": [ + {"type": "UserName"}, + { + "type": "orgPersonType", + "not_any_of": ["Contractor", "SubContractor"], + }, + {"type": "LastName", "any_one_of": ["Bo"]}, + ], + }, + { + "local": [ + {"group": {"id": CONTRACTOR_GROUP_ID}}, + {"user": {"name": "{0}"}}, + ], + "remote": [ + {"type": "UserName"}, + { + "type": "orgPersonType", + "any_one_of": ["Contractor", "SubContractor"], + }, + {"type": "FirstName", "any_one_of": ["Jill"]}, + ], + }, +] +POLICY_RULES_2 = [ + { + "local": [ + { + "user": {"name": "{0} {1}", "email": "{2}"}, + "group": {"id": EMPLOYEE_GROUP_ID}, + } + ], + "remote": [ + {"type": "FirstName"}, + {"type": "LastName"}, + {"type": "Email"}, + { + "type": "orgPersonType", + "any_one_of": ["Admin", "Big Cheese"], + }, + ], + }, + { + "local": [{"user": {"name": "{0}", "email": "{1}"}}], + "remote": [ + {"type": "UserName"}, + {"type": "Email"}, + { + "type": "orgPersonType", + "not_any_of": [ + "Admin", + "Employee", + "Contractor", + "Tester", + ], + }, + ], + }, + { + "local": [ + {"group": {"id": TESTER_GROUP_ID}}, + {"group": {"id": DEVELOPER_GROUP_ID}}, + {"user": {"name": "{0}"}}, + ], + "remote": [ + {"type": "UserName"}, + {"type": "orgPersonType", "any_one_of": ["Tester"]}, + { + "type": "Email", + "any_one_of": [".*@example.com$"], + "regex": True, + }, + ], + }, +] + + +class TestPolicyCreate(identity_fakes.TestFederatedIdentity): + def setUp(self): + super().setUp() + + self.policy = sdk_fakes.generate_fake_resource( + _policy.Policy, blob=POLICY_RULES, type='application/json' + ) + self.identity_sdk_client.create_policy.return_value = self.policy + + self.cmd = policy.CreatePolicy(self.app, None) + + def test_create_policy(self): + arglist = [POLICY_RULES_FILE_PATH] + verifylist = [ + ('rules', POLICY_RULES_FILE_PATH), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = POLICY_RULES + with mock.patch( + "osc_lib.utils.read_blob_file_contents", + mocker, + ): + columns, data = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.create_policy.assert_called_with( + blob=POLICY_RULES, type='application/json' + ) + + collist = ('id', 'rules', 'type') + self.assertEqual(collist, columns) + + datalist = (self.policy.id, POLICY_RULES, 'application/json') + self.assertEqual(datalist, data) + + +class TestPolicyDelete(identity_fakes.TestFederatedIdentity): + def setUp(self): + super().setUp() + + self.policy = sdk_fakes.generate_fake_resource( + _policy.Policy, blob=POLICY_RULES, type='application/json' + ) + self.identity_sdk_client.delete_policy.return_value = None + self.cmd = policy.DeletePolicy(self.app, None) + + def test_delete_policy(self): + arglist = [self.policy.id] + verifylist = [('policy', [self.policy.id])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.delete_policy.assert_called_with( + self.policy.id, ignore_missing=False + ) + self.assertIsNone(result) + + +class TestPolicyList(identity_fakes.TestFederatedIdentity): + def setUp(self): + super().setUp() + + self.policy = sdk_fakes.generate_fake_resource( + _policy.Policy, blob=POLICY_RULES, type='application/json' + ) + + self.extra_policy = sdk_fakes.generate_fake_resource( + _policy.Policy, blob=POLICY_RULES_2, type='text/json' + ) + + self.identity_sdk_client.policies.return_value = [ + self.policy, + self.extra_policy, + ] + + # Get the command object to test + self.cmd = policy.ListPolicy(self.app, None) + + def test_policy_list(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.policies.assert_called_with() + + collist = ('ID', 'Type') + self.assertEqual(collist, columns) + + datalist = [ + (self.policy.id, 'application/json'), + (self.extra_policy.id, 'text/json'), + ] + self.assertEqual(datalist, data) + + def test_policy_list_long(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.policies.assert_called_with() + + collist = ('ID', 'Type', 'Rules') + self.assertEqual(collist, columns) + + datalist = [ + (self.policy.id, 'application/json', POLICY_RULES), + (self.extra_policy.id, 'text/json', POLICY_RULES_2), + ] + self.assertEqual(datalist, data) + + +class TestPolicySet(identity_fakes.TestFederatedIdentity): + def setUp(self): + super().setUp() + + self.policy = sdk_fakes.generate_fake_resource( + _policy.Policy, blob=POLICY_RULES, type='application/json' + ) + self.policy_2 = sdk_fakes.generate_fake_resource( + _policy.Policy, + id=self.policy.id, + blob=POLICY_RULES_2, + type='text/json', + ) + self.identity_sdk_client.get_policy.return_value = self.policy + self.identity_sdk_client.update_policy.return_value = self.policy_2 + + # Get the command object to test + self.cmd = policy.SetPolicy(self.app, None) + + def test_policy_set_new_rules(self): + arglist = [ + self.policy.id, + '--rules', + POLICY_RULES_FILE_PATH, + '--type', + 'text/json', + ] + verifylist = [ + ('policy', self.policy.id), + ('rules', POLICY_RULES_FILE_PATH), + ('type', 'text/json'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + mocker = mock.Mock() + mocker.return_value = POLICY_RULES_2 + with mock.patch( + "osc_lib.utils.read_blob_file_contents", + mocker, + ): + result = self.cmd.take_action(parsed_args) + self.identity_sdk_client.update_policy.assert_called_with( + self.policy.id, + blob=POLICY_RULES_2, + type='text/json', + ) + + self.assertIsNone(result) + + def test_policy_set_rules_wrong_file_path(self): + arglist = [ + '--rules', + POLICY_RULES_FILE_PATH, + self.policy.id, + ] + verifylist = [ + ('policy', self.policy.id), + ('rules', POLICY_RULES_FILE_PATH), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestPolicyShow(identity_fakes.TestFederatedIdentity): + def setUp(self): + super().setUp() + + self.policy = sdk_fakes.generate_fake_resource( + _policy.Policy, blob=POLICY_RULES, type='application/json' + ) + self.identity_sdk_client.get_policy.return_value = self.policy + + self.cmd = policy.ShowPolicy(self.app, None) + + def test_policy_show(self): + arglist = [self.policy.id] + verifylist = [('policy', self.policy.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.get_policy.assert_called_with(self.policy.id) + + collist = ('id', 'rules', 'type') + self.assertEqual(collist, columns) + + datalist = (self.policy.id, POLICY_RULES, 'application/json') + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/test_project.py b/openstackclient/tests/unit/identity/v3/test_project.py index 9085498e31..3e968ccf85 100644 --- a/openstackclient/tests/unit/identity/v3/test_project.py +++ b/openstackclient/tests/unit/identity/v3/test_project.py @@ -13,31 +13,20 @@ # under the License. from unittest import mock -from unittest.mock import call +from openstack import exceptions as sdk_exc +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.identity.v3 import user as _user +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions -from osc_lib import utils -from openstackclient.identity import common from openstackclient.identity.v3 import project from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestProject(identity_fakes.TestIdentityv3): - def setUp(self): - super().setUp() - - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - self.domains_mock.reset_mock() - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() - - -class TestProjectCreate(TestProject): - domain = identity_fakes.FakeDomain.create_one_domain() +class TestProjectCreate(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) columns = ( 'description', @@ -46,40 +35,41 @@ class TestProjectCreate(TestProject): 'id', 'is_domain', 'name', + 'options', 'parent_id', 'tags', ) + project_kwargs_no_options = { + 'description': None, + 'domain_id': None, + 'enabled': True, + 'is_domain': False, + 'parent_id': None, + 'tags': [], + } + def setUp(self): super().setUp() - self.project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': self.domain.id} - ) - self.domains_mock.get.return_value = self.domain - self.projects_mock.create.return_value = self.project - self.datalist = ( - self.project.description, - self.project.domain_id, - True, - self.project.id, - False, - self.project.name, - self.project.parent_id, - self.project.tags, - ) + self.identity_sdk_client.find_domain.return_value = self.domain + # Get the command object to test self.cmd = project.CreateProject(self.app, None) def test_project_create_no_options(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, **self.project_kwargs_no_options + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ - self.project.name, + project.name, ] verifylist = [ ('parent', None), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('enabled', True), + ('name', project.name), ('tags', []), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -89,54 +79,43 @@ def test_project_create_no_options(self): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {}, + 'name': project.name, + 'is_enabled': True, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) + + self.assertEqual(self.columns, columns) - collist = ( - 'description', - 'domain_id', - 'enabled', - 'id', - 'is_domain', - 'name', - 'parent_id', - 'tags', - ) - self.assertEqual(collist, columns) datalist = ( - self.project.description, - self.project.domain_id, + None, + None, True, - self.project.id, + project.id, False, - self.project.name, - self.project.parent_id, - self.project.tags, + project.name, + {}, + None, + [], ) self.assertEqual(datalist, data) def test_project_create_description(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, description='new desc'), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--description', 'new desc', - self.project.name, + project.name, ] verifylist = [ ('description', 'new desc'), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('enabled', True), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -147,34 +126,43 @@ def test_project_create_description(self): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, + 'name': project.name, 'description': 'new desc', - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {}, + 'is_enabled': True, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + 'new desc', + None, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_domain(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, domain_id=self.domain.id), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--domain', - self.project.domain_id, - self.project.name, + project.domain_id, + project.name, ] verifylist = [ - ('domain', self.project.domain_id), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('domain', project.domain_id), + ('enabled', True), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -187,65 +175,90 @@ def test_project_create_domain(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': self.project.domain_id, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {}, + 'name': project.name, + 'domain_id': project.domain_id, + 'is_enabled': True, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + self.domain.id, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_domain_no_perms(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, domain_id=self.domain.id), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--domain', - self.project.domain_id, - self.project.name, + project.domain_id, + project.name, ] verifylist = [ - ('domain', self.project.domain_id), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('domain', project.domain_id), + ('enabled', True), + ('name', project.name), ('parent', None), ('tags', []), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - mocker = mock.Mock() - mocker.return_value = None - with mock.patch("osc_lib.utils.find_resource", mocker): - columns, data = self.cmd.take_action(parsed_args) + self.identity_sdk_client.find_domain.side_effect = ( + sdk_exc.ForbiddenException + ) + self.identity_sdk_client.find_domain.return_value = None + + columns, data = self.cmd.take_action(parsed_args) # Set expected values kwargs = { - 'name': self.project.name, - 'domain': self.project.domain_id, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {}, + 'name': project.name, + 'domain_id': project.domain_id, + 'is_enabled': True, } - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) + self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + self.domain.id, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_enable(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, enabled=True), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--enable', - self.project.name, + project.name, ] verifylist = [ - ('enable', True), - ('disable', False), - ('name', self.project.name), + ('enabled', True), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -258,30 +271,39 @@ def test_project_create_enable(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {}, + 'name': project.name, + 'is_enabled': True, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_disable(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, enabled=False), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--disable', - self.project.name, + project.name, ] verifylist = [ - ('enable', False), - ('disable', True), - ('name', self.project.name), + ('enabled', False), + ('name', project.name), ('parent', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -293,32 +315,42 @@ def test_project_create_disable(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': False, - 'parent': None, - 'tags': [], - 'options': {}, + 'name': project.name, + 'is_enabled': False, } - # ProjectManager.create(name=, domain=, - # description=, enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + False, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_property(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, fee='fi', fo='fum'), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--property', 'fee=fi', '--property', 'fo=fum', - self.project.name, + project.name, ] verifylist = [ - ('property', {'fee': 'fi', 'fo': 'fum'}), - ('name', self.project.name), + ('name', project.name), + ('properties', {'fee': 'fi', 'fo': 'fum'}), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -329,37 +361,62 @@ def test_project_create_property(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, + 'name': project.name, + 'is_enabled': True, 'fee': 'fi', 'fo': 'fum', - 'tags': [], - 'options': {}, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) - self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertEqual( + ( + 'description', + 'domain_id', + 'enabled', + 'fee', + 'fo', + 'id', + 'is_domain', + 'name', + 'options', + 'parent_id', + 'tags', + ), + columns, + ) + datalist = ( + None, + None, + True, + 'fi', + 'fum', + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_is_domain_false_property(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, is_domain=False), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--property', 'is_domain=false', - self.project.name, + project.name, ] verifylist = [ ('parent', None), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('enabled', True), + ('name', project.name), ('tags', []), - ('property', {'is_domain': 'false'}), - ('name', self.project.name), + ('properties', {'is_domain': 'false'}), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -371,34 +428,44 @@ def test_project_create_is_domain_false_property(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, + 'name': project.name, + 'is_enabled': True, 'is_domain': False, - 'tags': [], - 'options': {}, } - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_is_domain_true_property(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, is_domain=True), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--property', 'is_domain=true', - self.project.name, + project.name, ] verifylist = [ ('parent', None), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('enabled', True), + ('name', project.name), ('tags', []), - ('property', {'is_domain': 'true'}), - ('name', self.project.name), + ('properties', {'is_domain': 'true'}), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -410,34 +477,44 @@ def test_project_create_is_domain_true_property(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, + 'name': project.name, + 'is_enabled': True, 'is_domain': True, - 'tags': [], - 'options': {}, } - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + True, + project.id, + True, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_is_domain_none_property(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, is_domain=None), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--property', 'is_domain=none', - self.project.name, + project.name, ] verifylist = [ ('parent', None), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('enabled', True), + ('name', project.name), ('tags', []), - ('property', {'is_domain': 'none'}), - ('name', self.project.name), + ('properties', {'is_domain': 'none'}), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -449,41 +526,51 @@ def test_project_create_is_domain_none_property(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, + 'name': project.name, + 'is_enabled': True, 'is_domain': None, - 'tags': [], - 'options': {}, } - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + True, + project.id, + None, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) def test_project_create_parent(self): - self.parent = identity_fakes.FakeProject.create_one_project() - self.project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': self.domain.id, 'parent_id': self.parent.id} + parent = sdk_fakes.generate_fake_resource(_project.Project) + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, + domain_id=self.domain.id, + parent_id=parent.id, + ), ) - self.projects_mock.get.return_value = self.parent - self.projects_mock.create.return_value = self.project + self.identity_sdk_client.find_project.return_value = parent + self.identity_sdk_client.create_project.return_value = project arglist = [ '--domain', - self.project.domain_id, + project.domain_id, '--parent', - self.parent.name, - self.project.name, + parent.name, + project.name, ] verifylist = [ - ('domain', self.project.domain_id), - ('parent', self.parent.name), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('domain', project.domain_id), + ('parent', parent.name), + ('enabled', True), + ('name', project.name), ('tags', []), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -491,62 +578,52 @@ def test_project_create_parent(self): columns, data = self.cmd.take_action(parsed_args) kwargs = { - 'name': self.project.name, - 'domain': self.project.domain_id, - 'parent': self.parent.id, - 'description': None, - 'enabled': True, - 'tags': [], - 'options': {}, + 'name': project.name, + 'domain_id': project.domain_id, + 'parent_id': parent.id, + 'is_enabled': True, } + self.identity_sdk_client.create_project.assert_called_with(**kwargs) - self.projects_mock.create.assert_called_with(**kwargs) - - collist = ( - 'description', - 'domain_id', - 'enabled', - 'id', - 'is_domain', - 'name', - 'parent_id', - 'tags', - ) - self.assertEqual(columns, collist) + self.assertEqual(self.columns, columns) datalist = ( - self.project.description, - self.project.domain_id, - self.project.enabled, - self.project.id, - self.project.is_domain, - self.project.name, - self.parent.id, - self.project.tags, + None, + self.domain.id, + True, + project.id, + False, + project.name, + {}, + parent.id, + [], ) self.assertEqual(data, datalist) def test_project_create_invalid_parent(self): - self.projects_mock.resource_class.__name__ = 'Project' - self.projects_mock.get.side_effect = exceptions.NotFound( - 'Invalid parent' + self.identity_sdk_client.find_project.side_effect = ( + sdk_exc.ResourceNotFound ) - self.projects_mock.find.side_effect = exceptions.NotFound( - 'Invalid parent' + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, + domain_id=self.domain.id, + parent_id='invalid', + ), ) arglist = [ '--domain', - self.project.domain_id, + project.domain_id, '--parent', 'invalid', - self.project.name, + project.name, ] verifylist = [ - ('domain', self.project.domain_id), + ('domain', project.domain_id), ('parent', 'invalid'), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('enabled', True), + ('name', project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -557,18 +634,27 @@ def test_project_create_invalid_parent(self): ) def test_project_create_with_tags(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, + domain_id=self.domain.id, + tags=['foo'], + ), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--domain', - self.project.domain_id, + project.domain_id, '--tag', 'foo', - self.project.name, + project.name, ] verifylist = [ - ('domain', self.project.domain_id), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('domain', project.domain_id), + ('enabled', True), + ('name', project.name), ('parent', None), ('tags', ['foo']), ] @@ -581,30 +667,45 @@ def test_project_create_with_tags(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': self.project.domain_id, - 'description': None, - 'enabled': True, - 'parent': None, + 'name': project.name, + 'domain_id': project.domain_id, + 'is_enabled': True, 'tags': ['foo'], - 'options': {}, } - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + self.domain.id, + True, + project.id, + False, + project.name, + {}, + None, + ['foo'], + ) + self.assertEqual(datalist, data) def test_project_create_with_immutable_option(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, options={'immutable': True} + ), + ) + self.identity_sdk_client.create_project.return_value = project + arglist = [ '--immutable', - self.project.name, + project.name, ] verifylist = [ ('immutable', True), ('description', None), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('enabled', True), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -615,76 +716,173 @@ def test_project_create_with_immutable_option(self): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # Set expected values + # Set expected values + kwargs = { + 'name': project.name, + 'is_enabled': True, + 'options': {'immutable': True}, + } + self.identity_sdk_client.create_project.assert_called_with(**kwargs) + + self.assertEqual(self.columns, columns) + datalist = ( + None, + None, + True, + project.id, + False, + project.name, + {'immutable': True}, + None, + [], + ) + self.assertEqual(datalist, data) + + def test_project_create_with_no_immutable_option(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, options={'immutable': False} + ), + ) + self.identity_sdk_client.create_project.return_value = project + + arglist = [ + '--no-immutable', + project.name, + ] + verifylist = [ + ('immutable', False), + ('description', None), + ('enabled', True), + ('name', project.name), + ('parent', None), + ('tags', []), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class ShowOne in cliff, abstract method take_action() + # returns a two-part tuple with a tuple of column names and a tuple of + # data to be shown. + columns, data = self.cmd.take_action(parsed_args) + + # Set expected values + kwargs = { + 'name': project.name, + 'is_enabled': True, + 'options': {'immutable': False}, + } + self.identity_sdk_client.create_project.assert_called_with(**kwargs) + + self.assertEqual(self.columns, columns) + datalist = ( + None, + None, + True, + project.id, + False, + project.name, + {'immutable': False}, + None, + [], + ) + self.assertEqual(datalist, data) + + def test_project_create_conflict_with_or_show(self): + project = sdk_fakes.generate_fake_resource( + _project.Project, **self.project_kwargs_no_options + ) + self.identity_sdk_client.create_project.side_effect = ( + sdk_exc.ConflictException + ) + self.identity_sdk_client.find_project.return_value = project + + arglist = [ + '--or-show', + project.name, + ] + verifylist = [ + ('or_show', True), + ('description', None), + ('enabled', True), + ('name', project.name), + ('parent', None), + ('tags', []), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {'immutable': True}, + 'name': project.name, + 'is_enabled': True, } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + self.identity_sdk_client.create_project.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + datalist = ( + None, + None, + True, + project.id, + False, + project.name, + {}, + None, + [], + ) + self.assertEqual(datalist, data) + + def test_project_create_conflict_without_or_show(self): + self.identity_sdk_client.create_project.side_effect = ( + sdk_exc.ConflictException + ) + project = sdk_fakes.generate_fake_resource( + _project.Project, **self.project_kwargs_no_options + ) - def test_project_create_with_no_immutable_option(self): arglist = [ - '--no-immutable', - self.project.name, + project.name, ] verifylist = [ - ('no_immutable', True), + ('or_show', False), ('description', None), - ('enable', False), - ('disable', False), - ('name', self.project.name), + ('enabled', True), + ('name', project.name), ('parent', None), ('tags', []), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. - columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], - 'options': {'immutable': False}, - } - # ProjectManager.create(name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.create.assert_called_with(**kwargs) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertEqual(self.columns, columns) - self.assertEqual(self.datalist, data) + self.assertRaises( + sdk_exc.ConflictException, + self.cmd.take_action, + parsed_args, + ) -class TestProjectDelete(TestProject): - project = identity_fakes.FakeProject.create_one_project() +class TestProjectDelete(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): super().setUp() - # This is the return value for utils.find_resource() - self.projects_mock.get.return_value = self.project - self.projects_mock.delete.return_value = None + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.project_with_domain = sdk_fakes.generate_fake_resource( + _project.Project, + name=self.project.name, + domain_id=self.domain.id, + ) + self.identity_sdk_client.delete_project.return_value = None # Get the command object to test self.cmd = project.DeleteProject(self.app, None) def test_project_delete_no_options(self): + self.identity_sdk_client.find_project.return_value = self.project + arglist = [ self.project.id, ] @@ -695,16 +893,72 @@ def test_project_delete_no_options(self): result = self.cmd.take_action(parsed_args) - self.projects_mock.delete.assert_called_with( + self.identity_sdk_client.delete_project.assert_called_with( self.project.id, ) self.assertIsNone(result) - @mock.patch.object(utils, 'find_resource') - def test_delete_multi_projects_with_exception(self, find_mock): - find_mock.side_effect = [self.project, exceptions.CommandError] + def test_project_multi_delete(self): + self.identity_sdk_client.find_project.side_effect = [ + self.project, + self.project_with_domain, + ] + arglist = [self.project.id, self.project_with_domain.id] + verifylist = [ + ('projects', arglist), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.delete_project.assert_has_calls( + [ + mock.call(self.project.id), + mock.call(self.project_with_domain.id), + ] + ) + self.assertIsNone(result) + + def test_project_delete_with_forbidden_domain(self): + self.identity_sdk_client.find_domain.side_effect = [ + sdk_exc.ForbiddenException + ] + self.identity_sdk_client.find_project.return_value = ( + self.project_with_domain + ) + + arglist = [ + '--domain', + self.project_with_domain.domain_id, + self.project_with_domain.name, + ] + verifylist = [ + ('domain', self.domain.id), + ('projects', [self.project_with_domain.name]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.identity_sdk_client.find_project.assert_called_with( + name_or_id=self.project_with_domain.name, + ignore_missing=False, + domain_id=self.domain.id, + ) + self.identity_sdk_client.delete_project.assert_called_once_with( + self.project_with_domain.id + ) + self.assertIsNone(result) + + def test_delete_multi_projects_with_exception(self): + self.identity_sdk_client.find_project.side_effect = [ + self.project, + self.project_with_domain, + sdk_exc.NotFoundException, + ] + arglist = [ self.project.id, + self.project_with_domain.id, 'unexist_project', ] verifylist = [ @@ -716,21 +970,36 @@ def test_delete_multi_projects_with_exception(self, find_mock): self.cmd.take_action(parsed_args) self.fail('CommandError should be raised.') except exceptions.CommandError as e: - self.assertEqual('1 of 2 projects failed to delete.', str(e)) + self.assertEqual('1 of 3 projects failed to delete.', str(e)) - find_mock.assert_any_call(self.projects_mock, self.project.id) - find_mock.assert_any_call(self.projects_mock, 'unexist_project') + self.identity_sdk_client.find_project.assert_has_calls( + [ + mock.call(name_or_id=self.project.id, ignore_missing=False), + mock.call( + name_or_id=self.project_with_domain.id, + ignore_missing=False, + ), + mock.call(name_or_id='unexist_project', ignore_missing=False), + ] + ) - self.assertEqual(2, find_mock.call_count) - self.projects_mock.delete.assert_called_once_with(self.project.id) + self.assertEqual(3, self.identity_sdk_client.find_project.call_count) + self.identity_sdk_client.delete_project.assert_has_calls( + [ + mock.call(self.project.id), + mock.call(self.project_with_domain.id), + ] + ) -class TestProjectList(TestProject): - domain = identity_fakes.FakeDomain.create_one_domain() - project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': domain.id} +class TestProjectList(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) + project = sdk_fakes.generate_fake_resource( + _project.Project, domain_id=domain.id + ) + projects = list( + sdk_fakes.generate_fake_resources(_project.Project, count=2) ) - projects = identity_fakes.FakeProject.create_projects() columns = ( 'ID', @@ -760,12 +1029,12 @@ class TestProjectList(TestProject): def setUp(self): super().setUp() - self.projects_mock.list.return_value = [self.project] - # Get the command object to test self.cmd = project.ListProject(self.app, None) def test_project_list_no_options(self): + self.identity_sdk_client.projects.return_value = [self.project] + arglist = [] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -774,12 +1043,14 @@ def test_project_list_no_options(self): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with() + self.identity_sdk_client.projects.assert_called_with() self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) def test_project_list_long(self): + self.identity_sdk_client.projects.return_value = [self.project] + arglist = [ '--long', ] @@ -792,7 +1063,7 @@ def test_project_list_long(self): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with() + self.identity_sdk_client.projects.assert_called_with() collist = ('ID', 'Name', 'Domain ID', 'Description', 'Enabled') self.assertEqual(collist, columns) @@ -808,6 +1079,8 @@ def test_project_list_long(self): self.assertEqual(datalist, tuple(data)) def test_project_list_domain(self): + self.identity_sdk_client.projects.return_value = [self.project] + arglist = [ '--domain', self.project.domain_id, @@ -816,7 +1089,7 @@ def test_project_list_domain(self): ('domain', self.project.domain_id), ] - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_domain.return_value = self.domain parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -824,14 +1097,16 @@ def test_project_list_domain(self): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with( - domain=self.project.domain_id + self.identity_sdk_client.projects.assert_called_with( + domain_id=self.project.domain_id ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) def test_project_list_domain_no_perms(self): + self.identity_sdk_client.projects.return_value = [self.project] + arglist = [ '--domain', self.project.domain_id, @@ -840,23 +1115,30 @@ def test_project_list_domain_no_perms(self): ('domain', self.project.domain_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - mocker = mock.Mock() - mocker.return_value = None - with mock.patch("osc_lib.utils.find_resource", mocker): - columns, data = self.cmd.take_action(parsed_args) + self.identity_sdk_client.find_project.side_effect = ( + sdk_exc.ResourceNotFound + ) + self.identity_sdk_client.find_domain.return_value = self.domain + + columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with( - domain=self.project.domain_id + self.identity_sdk_client.projects.assert_called_with( + domain_id=self.project.domain_id ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) def test_project_list_parent(self): - self.parent = identity_fakes.FakeProject.create_one_project() - self.project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': self.domain.id, 'parent_id': self.parent.id} + self.parent = sdk_fakes.generate_fake_resource(_project.Project) + self.project = sdk_fakes.generate_fake_resource( + _project.Project, + id=self.project.id, + name=self.project.name, + domain_id=self.domain.id, + parent_id=self.parent.id, ) + self.identity_sdk_client.projects.return_value = [self.project] arglist = [ '--parent', @@ -866,18 +1148,48 @@ def test_project_list_parent(self): ('parent', self.parent.id), ] - self.projects_mock.get.return_value = self.parent + self.identity_sdk_client.find_project.return_value = self.parent + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.identity_sdk_client.projects.assert_called_with( + parent_id=self.parent.id + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_project_list_user(self): + self.user = sdk_fakes.generate_fake_resource(_user.User) + self.project = sdk_fakes.generate_fake_resource( + _project.UserProject, + id=self.project.id, + name=self.project.name, + user_id=self.user.id, + ) + self.identity_sdk_client.user_projects.return_value = [self.project] + + arglist = [ + '--user', + self.user.id, + ] + verifylist = [ + ('user', self.user.id), + ] + + self.identity_sdk_client.find_user.return_value = self.user parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with(parent=self.parent.id) + self.identity_sdk_client.user_projects.assert_called_with(self.user.id) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) def test_project_list_sort(self): - self.projects_mock.list.return_value = self.projects + self.identity_sdk_client.projects.return_value = self.projects arglist = [ '--sort', @@ -891,7 +1203,7 @@ def test_project_list_sort(self): # returns a tuple containing the column names and an iterable # containing the data to be listed. (columns, data) = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with() + self.identity_sdk_client.projects.assert_called_with() collist = ('ID', 'Name') self.assertEqual(collist, columns) @@ -910,6 +1222,8 @@ def test_project_list_sort(self): self.assertEqual(datalists, tuple(data)) def test_project_list_my_projects(self): + self.identity_sdk_client.user_projects.return_value = [self.project] + auth_ref = identity_fakes.fake_auth_ref( identity_fakes.TOKEN_WITH_PROJECT_ID, ) @@ -927,8 +1241,8 @@ def test_project_list_my_projects(self): # returns a tuple containing the column names and an iterable # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.list.assert_called_with( - user=self.app.client_manager.auth_ref.user_id + self.identity_sdk_client.user_projects.assert_called_with( + self.app.client_manager.auth_ref.user_id ) collist = ('ID', 'Name') @@ -941,20 +1255,64 @@ def test_project_list_my_projects(self): ) self.assertEqual(datalist, tuple(data)) + def test_project_list_with_option_enabled(self): + self.identity_sdk_client.projects.return_value = [self.project] + + arglist = ['--enabled'] + verifylist = [('is_enabled', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + kwargs = {'is_enabled': True} + self.identity_sdk_client.projects.assert_called_with(**kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_project_list_with_pagination(self): + self.identity_sdk_client.projects.return_value = [self.project] + + arglist = [ + '--limit', + '2', + '--marker', + 'some-marker', + ] + verifylist = [ + ('limit', 2), + ('marker', 'some-marker'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + kwargs = {'limit': 2, 'marker': 'some-marker'} + self.identity_sdk_client.projects.assert_called_with(**kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + +class TestProjectSet(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) -class TestProjectSet(TestProject): - domain = identity_fakes.FakeDomain.create_one_domain() - project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': domain.id, 'tags': ['tag1', 'tag2', 'tag3']} + project_kwargs_no_options = { + 'domain_id': domain.id, + 'tags': ['tag1', 'tag2', 'tag3'], + } + project = sdk_fakes.generate_fake_resource( + _project.Project, **project_kwargs_no_options ) def setUp(self): super().setUp() - self.domains_mock.get.return_value = self.domain - - self.projects_mock.get.return_value = self.project - self.projects_mock.update.return_value = self.project + self.identity_sdk_client.find_domain.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project # Get the command object to test self.cmd = project.SetProject(self.app, None) @@ -965,8 +1323,7 @@ def test_project_set_no_options(self): ] verifylist = [ ('project', self.project.name), - ('enable', False), - ('disable', False), + ('enabled', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -985,8 +1342,7 @@ def test_project_set_name(self): verifylist = [ ('name', 'qwerty'), ('domain', self.project.domain_id), - ('enable', False), - ('disable', False), + ('enabled', None), ('project', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -997,9 +1353,10 @@ def test_project_set_name(self): kwargs = { 'name': 'qwerty', } - # ProjectManager.update(project, name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_description(self): @@ -1013,8 +1370,7 @@ def test_project_set_description(self): verifylist = [ ('domain', self.project.domain_id), ('description', 'new desc'), - ('enable', False), - ('disable', False), + ('enabled', None), ('project', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1025,7 +1381,9 @@ def test_project_set_description(self): kwargs = { 'description': 'new desc', } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_enable(self): @@ -1037,8 +1395,7 @@ def test_project_set_enable(self): ] verifylist = [ ('domain', self.project.domain_id), - ('enable', True), - ('disable', False), + ('enabled', True), ('project', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1049,7 +1406,9 @@ def test_project_set_enable(self): kwargs = { 'enabled': True, } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_disable(self): @@ -1061,8 +1420,7 @@ def test_project_set_disable(self): ] verifylist = [ ('domain', self.project.domain_id), - ('enable', False), - ('disable', True), + ('enabled', False), ('project', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1073,7 +1431,9 @@ def test_project_set_disable(self): kwargs = { 'enabled': False, } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_property(self): @@ -1088,7 +1448,7 @@ def test_project_set_property(self): ] verifylist = [ ('domain', self.project.domain_id), - ('property', {'fee': 'fi', 'fo': 'fum'}), + ('properties', {'fee': 'fi', 'fo': 'fum'}), ('project', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1100,7 +1460,9 @@ def test_project_set_property(self): 'fee': 'fi', 'fo': 'fum', } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_tags(self): @@ -1115,9 +1477,8 @@ def test_project_set_tags(self): ] verifylist = [ ('name', 'qwerty'), - ('domain', self.project.domain_id), - ('enable', False), - ('disable', False), + ('domain', self.domain.id), + ('enabled', None), ('project', self.project.name), ('tags', ['foo']), ] @@ -1130,9 +1491,9 @@ def test_project_set_tags(self): 'name': 'qwerty', 'tags': sorted({'tag1', 'tag2', 'tag3', 'foo'}), } - # ProjectManager.update(project, name=, domain=, description=, - # enabled=, **kwargs) - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_remove_tags(self): @@ -1144,17 +1505,18 @@ def test_project_remove_tags(self): self.project.name, ] verifylist = [ - ('enable', False), - ('disable', False), + ('enabled', None), ('project', self.project.name), - ('remove_tag', ['tag1', 'tag2']), + ('remove_tags', ['tag1', 'tag2']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) kwargs = {'tags': list({'tag3'})} - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_with_immutable_option(self): @@ -1167,8 +1529,7 @@ def test_project_set_with_immutable_option(self): verifylist = [ ('domain', self.project.domain_id), ('immutable', True), - ('enable', False), - ('disable', False), + ('enabled', None), ('project', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1179,7 +1540,9 @@ def test_project_set_with_immutable_option(self): kwargs = { 'options': {'immutable': True}, } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) def test_project_set_with_no_immutable_option(self): @@ -1191,9 +1554,8 @@ def test_project_set_with_no_immutable_option(self): ] verifylist = [ ('domain', self.project.domain_id), - ('no_immutable', True), - ('enable', False), - ('disable', False), + ('immutable', False), + ('enabled', None), ('project', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1204,114 +1566,108 @@ def test_project_set_with_no_immutable_option(self): kwargs = { 'options': {'immutable': False}, } - self.projects_mock.update.assert_called_with(self.project.id, **kwargs) + self.identity_sdk_client.update_project.assert_called_with( + self.project.id, **kwargs + ) self.assertIsNone(result) -class TestProjectShow(TestProject): - domain = identity_fakes.FakeDomain.create_one_domain() +class TestProjectShow(identity_fakes.TestIdentity): + domain = sdk_fakes.generate_fake_resource(_domain.Domain) + + columns = ( + 'description', + 'domain_id', + 'enabled', + 'id', + 'is_domain', + 'name', + 'options', + 'parent_id', + 'tags', + ) + + project_kwargs_no_options = { + 'description': None, + 'domain_id': None, + 'enabled': True, + 'is_domain': False, + 'parent_id': None, + 'tags': [], + } def setUp(self): super().setUp() - self.project = identity_fakes.FakeProject.create_one_project( - attrs={'domain_id': self.domain.id} - ) - # Get the command object to test self.cmd = project.ShowProject(self.app, None) def test_project_show(self): - self.projects_mock.get.return_value = self.project + project = sdk_fakes.generate_fake_resource( + _project.Project, **self.project_kwargs_no_options + ) + self.identity_sdk_client.find_project.return_value = project arglist = [ - self.project.id, + project.id, ] verifylist = [ - ('project', self.project.id), + ('project', project.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.identity_client.tokens.get_token_data.return_value = { - 'token': { - 'project': { - 'domain': {}, - 'name': parsed_args.project, - 'id': parsed_args.project, - } - } - } - # In base command class ShowOne in cliff, abstract method take_action() # returns a two-part tuple with a tuple of column names and a tuple of # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_called_once_with(self.project.id) - - collist = ( - 'description', - 'domain_id', - 'enabled', - 'id', - 'is_domain', - 'name', - 'parent_id', - 'tags', + self.identity_sdk_client.find_project.assert_called_with( + project.id, ignore_missing=False ) - self.assertEqual(collist, columns) + + self.assertEqual(self.columns, columns) datalist = ( - self.project.description, - self.project.domain_id, + None, + None, True, - self.project.id, + project.id, False, - self.project.name, - self.project.parent_id, - self.project.tags, + project.name, + {}, + None, + [], ) self.assertEqual(datalist, data) def test_project_show_parents(self): - self.project = identity_fakes.FakeProject.create_one_project( - attrs={ - 'parent_id': self.project.parent_id, - 'parents': [{'project': {'id': self.project.parent_id}}], - } + parent = sdk_fakes.generate_fake_resource( + _project.Project, parent_id='default' + ) + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, + parent_id=parent.id, + parents={parent.id: {parent.parent_id: None}}, + ), ) - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = project arglist = [ - self.project.id, + project.id, '--parents', ] verifylist = [ - ('project', self.project.id), + ('project', project.id), ('parents', True), ('children', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.identity_client.tokens.get_token_data.return_value = { - 'token': { - 'project': { - 'domain': {}, - 'name': parsed_args.project, - 'id': parsed_args.project, - } - } - } columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_has_calls( - [ - call(self.project.id), - call( - self.project.id, - parents_as_ids=True, - subtree_as_ids=False, - ), - ] + self.identity_sdk_client.find_project.assert_called_with( + project.id, parents_as_ids=True, ignore_missing=False ) collist = ( @@ -1321,63 +1677,51 @@ def test_project_show_parents(self): 'id', 'is_domain', 'name', + 'options', 'parent_id', 'parents', 'tags', ) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( - self.project.description, - self.project.domain_id, - self.project.enabled, - self.project.id, - self.project.is_domain, - self.project.name, - self.project.parent_id, - [{'project': {'id': self.project.parent_id}}], - self.project.tags, + None, + None, + True, + project.id, + False, + project.name, + {}, + parent.id, + {parent.id: {'default': None}}, + [], ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_project_show_subtree(self): - self.project = identity_fakes.FakeProject.create_one_project( - attrs={ - 'parent_id': self.project.parent_id, - 'subtree': [{'project': {'id': 'children-id'}}], - } + child = sdk_fakes.generate_fake_resource( + _project.Project, subtree=None ) - self.projects_mock.get.return_value = self.project + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, subtree={child.id: None}), + ) + self.identity_sdk_client.find_project.return_value = project arglist = [ - self.project.id, + project.id, '--children', ] verifylist = [ - ('project', self.project.id), + ('project', project.id), ('parents', False), ('children', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.identity_client.tokens.get_token_data.return_value = { - 'token': { - 'project': { - 'domain': {}, - 'name': parsed_args.project, - 'id': parsed_args.project, - } - } - } columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_has_calls( - [ - call(self.project.id), - call( - self.project.id, - parents_as_ids=False, - subtree_as_ids=True, - ), - ] + + self.identity_sdk_client.find_project.assert_called_with( + project.id, subtree_as_ids=True, ignore_missing=False ) collist = ( @@ -1387,65 +1731,63 @@ def test_project_show_subtree(self): 'id', 'is_domain', 'name', + 'options', 'parent_id', 'subtree', 'tags', ) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( - self.project.description, - self.project.domain_id, - self.project.enabled, - self.project.id, - self.project.is_domain, - self.project.name, - self.project.parent_id, - [{'project': {'id': 'children-id'}}], - self.project.tags, + None, + None, + True, + project.id, + False, + project.name, + {}, + None, + {child.id: None}, + [], ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_project_show_parents_and_children(self): - self.project = identity_fakes.FakeProject.create_one_project( - attrs={ - 'parent_id': self.project.parent_id, - 'parents': [{'project': {'id': self.project.parent_id}}], - 'subtree': [{'project': {'id': 'children-id'}}], - } + parent = sdk_fakes.generate_fake_resource( + _project.Project, parent_id='default' + ) + child = sdk_fakes.generate_fake_resource( + _project.Project, subtree=None + ) + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict( + self.project_kwargs_no_options, + parent_id=parent.id, + parents={parent.id: {parent.parent_id: None}}, + subtree={child.id: None}, + ), ) - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = project arglist = [ - self.project.id, + project.id, '--parents', '--children', ] verifylist = [ - ('project', self.project.id), + ('project', project.id), ('parents', True), ('children', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.identity_client.tokens.get_token_data.return_value = { - 'token': { - 'project': { - 'domain': {}, - 'name': parsed_args.project, - 'id': parsed_args.project, - } - } - } columns, data = self.cmd.take_action(parsed_args) - self.projects_mock.get.assert_has_calls( - [ - call(self.project.id), - call( - self.project.id, - parents_as_ids=True, - subtree_as_ids=True, - ), - ] + + self.identity_sdk_client.find_project.assert_called_with( + project.id, + parents_as_ids=True, + subtree_as_ids=True, + ignore_missing=False, ) collist = ( @@ -1455,42 +1797,36 @@ def test_project_show_parents_and_children(self): 'id', 'is_domain', 'name', + 'options', 'parent_id', 'parents', 'subtree', 'tags', ) - self.assertEqual(columns, collist) + self.assertEqual(collist, columns) datalist = ( - self.project.description, - self.project.domain_id, - self.project.enabled, - self.project.id, - self.project.is_domain, - self.project.name, - self.project.parent_id, - [{'project': {'id': self.project.parent_id}}], - [{'project': {'id': 'children-id'}}], - self.project.tags, + None, + None, + True, + project.id, + False, + project.name, + {}, + parent.id, + {parent.id: {'default': None}}, + {child.id: None}, + [], ) - self.assertEqual(data, datalist) + self.assertEqual(datalist, data) def test_project_show_with_domain(self): - project = identity_fakes.FakeProject.create_one_project( - {"name": self.project.name} - ) - - self.identity_client.tokens.get_token_data.return_value = { - 'token': { - 'project': { - 'domain': {"id": self.project.domain_id}, - 'name': self.project.name, - 'id': self.project.id, - } - } - } + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, domain_id=self.domain.id), + ) + self.identity_sdk_client.find_domain.return_value = self.domain + self.identity_sdk_client.find_project.return_value = project - identity_client = self.identity_client arglist = [ "--domain", self.domain.id, @@ -1502,23 +1838,22 @@ def test_project_show_with_domain(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - project_str = common._get_token_resource( - identity_client, 'project', parsed_args.project, parsed_args.domain + columns, data = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.find_project.assert_called_with( + project.id, domain_id=self.domain.id, ignore_missing=False ) - self.assertEqual(self.project.id, project_str) - arglist = [ - "--domain", - project.domain_id, + self.assertEqual(self.columns, columns) + datalist = ( + None, + self.domain.id, + True, + project.id, + False, project.name, - ] - verifylist = [ - ('domain', project.domain_id), - ('project', project.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - project_str = common._get_token_resource( - identity_client, 'project', parsed_args.project, parsed_args.domain + {}, + None, + [], ) - self.assertEqual(project.name, project_str) + self.assertEqual(datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/test_protocol.py b/openstackclient/tests/unit/identity/v3/test_protocol.py index c85699685a..a0351e385e 100644 --- a/openstackclient/tests/unit/identity/v3/test_protocol.py +++ b/openstackclient/tests/unit/identity/v3/test_protocol.py @@ -12,202 +12,211 @@ # License for the specific language governing permissions and limitations # under the License. -import copy - +from openstack.identity.v3 import federation_protocol as _federation_protocol +from openstack.identity.v3 import mapping as _mapping +from openstack.test import fakes as sdk_fakes from openstackclient.identity.v3 import federation_protocol -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestProtocol(identity_fakes.TestFederatedIdentity): - def setUp(self): - super().setUp() - - federation_lib = self.identity_client.federation - self.protocols_mock = federation_lib.protocols - self.protocols_mock.reset_mock() - - -class TestProtocolCreate(TestProtocol): +class TestProtocolCreate(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - proto = copy.deepcopy(identity_fakes.PROTOCOL_OUTPUT) - resource = fakes.FakeResource(None, proto, loaded=True) - self.protocols_mock.create.return_value = resource + self.proto = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol + ) + self.identity_sdk_client.create_federation_protocol.return_value = ( + self.proto + ) self.cmd = federation_protocol.CreateProtocol(self.app, None) def test_create_protocol(self): argslist = [ - identity_fakes.protocol_id, + self.proto.name, '--identity-provider', - identity_fakes.idp_id, + self.proto.idp_id, '--mapping', - identity_fakes.mapping_id, + self.proto.mapping_id, ] verifylist = [ - ('federation_protocol', identity_fakes.protocol_id), - ('identity_provider', identity_fakes.idp_id), - ('mapping', identity_fakes.mapping_id), + ('federation_protocol', self.proto.name), + ('identity_provider', self.proto.idp_id), + ('mapping', self.proto.mapping_id), ] parsed_args = self.check_parser(self.cmd, argslist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.protocols_mock.create.assert_called_with( - protocol_id=identity_fakes.protocol_id, - identity_provider=identity_fakes.idp_id, - mapping=identity_fakes.mapping_id, + self.identity_sdk_client.create_federation_protocol.assert_called_with( + name=self.proto.id, + idp=self.proto.idp_id, + mapping_id=self.proto.mapping_id, ) collist = ('id', 'identity_provider', 'mapping') self.assertEqual(collist, columns) datalist = ( - identity_fakes.protocol_id, - identity_fakes.idp_id, - identity_fakes.mapping_id, + self.proto.id, + self.proto.idp_id, + self.proto.mapping_id, ) self.assertEqual(datalist, data) -class TestProtocolDelete(TestProtocol): +class TestProtocolDelete(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - # This is the return value for utils.find_resource() - self.protocols_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.PROTOCOL_OUTPUT), - loaded=True, + self.proto = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol ) - - self.protocols_mock.delete.return_value = None + self.identity_sdk_client.delete_federation_protocol.return_value = None self.cmd = federation_protocol.DeleteProtocol(self.app, None) - def test_delete_identity_provider(self): + def test_delete_protocol(self): arglist = [ '--identity-provider', - identity_fakes.idp_id, - identity_fakes.protocol_id, + self.proto.idp_id, + self.proto.name, ] verifylist = [ - ('federation_protocol', [identity_fakes.protocol_id]), - ('identity_provider', identity_fakes.idp_id), + ('federation_protocol', [self.proto.id]), + ('identity_provider', self.proto.idp_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.protocols_mock.delete.assert_called_with( - identity_fakes.idp_id, identity_fakes.protocol_id + self.identity_sdk_client.delete_federation_protocol.assert_called_with( + idp=self.proto.idp_id, + protocol=self.proto.id, + ignore_missing=False, ) self.assertIsNone(result) -class TestProtocolList(TestProtocol): +class TestProtocolList(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - self.protocols_mock.get.return_value = fakes.FakeResource( - None, identity_fakes.PROTOCOL_ID_MAPPING, loaded=True + self.proto1 = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol ) - - self.protocols_mock.list.return_value = [ - fakes.FakeResource( - None, identity_fakes.PROTOCOL_ID_MAPPING, loaded=True - ) + self.proto2 = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol, idp_id=self.proto1 + ) + self.identity_sdk_client.federation_protocols.return_value = [ + self.proto1, + self.proto2, ] - self.cmd = federation_protocol.ListProtocols(self.app, None) def test_list_protocols(self): - arglist = ['--identity-provider', identity_fakes.idp_id] - verifylist = [('identity_provider', identity_fakes.idp_id)] + arglist = ['--identity-provider', self.proto1.idp_id] + verifylist = [('identity_provider', self.proto1.idp_id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.protocols_mock.list.assert_called_with(identity_fakes.idp_id) + self.identity_sdk_client.federation_protocols.assert_called_with( + self.proto1.idp_id + ) + self.assertEqual(columns, ('id', 'mapping')) + datalist = ( + ( + self.proto1.name, + self.proto1.mapping_id, + ), + ( + self.proto2.name, + self.proto2.mapping_id, + ), + ) + self.assertEqual(datalist, tuple(data)) -class TestProtocolSet(TestProtocol): + +class TestProtocolSet(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - self.protocols_mock.get.return_value = fakes.FakeResource( - None, identity_fakes.PROTOCOL_OUTPUT, loaded=True + self.proto = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol ) - self.protocols_mock.update.return_value = fakes.FakeResource( - None, identity_fakes.PROTOCOL_OUTPUT_UPDATED, loaded=True + self.mapping = sdk_fakes.generate_fake_resource(_mapping.Mapping) + self.identity_sdk_client.update_federation_protocol.return_value = ( + self.proto ) - self.cmd = federation_protocol.SetProtocol(self.app, None) def test_set_new_mapping(self): arglist = [ - identity_fakes.protocol_id, + self.proto.name, '--identity-provider', - identity_fakes.idp_id, + self.proto.idp_id, '--mapping', - identity_fakes.mapping_id, + self.mapping.name, ] verifylist = [ - ('identity_provider', identity_fakes.idp_id), - ('federation_protocol', identity_fakes.protocol_id), - ('mapping', identity_fakes.mapping_id), + ('identity_provider', self.proto.idp_id), + ('federation_protocol', self.proto.name), + ('mapping', self.mapping.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.protocols_mock.update.assert_called_with( - identity_fakes.idp_id, - identity_fakes.protocol_id, - identity_fakes.mapping_id, + self.identity_sdk_client.update_federation_protocol.assert_called_with( + idp_id=self.proto.idp_id, + name=self.proto.name, + mapping_id=self.mapping.id, ) collist = ('id', 'identity_provider', 'mapping') self.assertEqual(collist, columns) datalist = ( - identity_fakes.protocol_id, - identity_fakes.idp_id, - identity_fakes.mapping_id_updated, + self.proto.name, + self.proto.idp_id, + self.proto.mapping_id, ) self.assertEqual(datalist, data) -class TestProtocolShow(TestProtocol): +class TestProtocolShow(identity_fakes.TestFederatedIdentity): def setUp(self): super().setUp() - self.protocols_mock.get.return_value = fakes.FakeResource( - None, identity_fakes.PROTOCOL_OUTPUT, loaded=False + self.proto = sdk_fakes.generate_fake_resource( + _federation_protocol.FederationProtocol + ) + self.identity_sdk_client.get_federation_protocol.return_value = ( + self.proto ) - self.cmd = federation_protocol.ShowProtocol(self.app, None) def test_show_protocol(self): arglist = [ - identity_fakes.protocol_id, + self.proto.name, '--identity-provider', - identity_fakes.idp_id, + self.proto.idp_id, ] verifylist = [ - ('federation_protocol', identity_fakes.protocol_id), - ('identity_provider', identity_fakes.idp_id), + ('federation_protocol', self.proto.name), + ('identity_provider', self.proto.idp_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.protocols_mock.get.assert_called_with( - identity_fakes.idp_id, identity_fakes.protocol_id + self.identity_sdk_client.get_federation_protocol.assert_called_with( + idp=self.proto.idp_id, protocol=self.proto.name ) collist = ('id', 'identity_provider', 'mapping') self.assertEqual(collist, columns) datalist = ( - identity_fakes.protocol_id, - identity_fakes.idp_id, - identity_fakes.mapping_id, + self.proto.name, + self.proto.idp_id, + self.proto.mapping_id, ) self.assertEqual(datalist, data) diff --git a/openstackclient/tests/unit/identity/v3/test_region.py b/openstackclient/tests/unit/identity/v3/test_region.py index eecb079133..47c5caf1e4 100644 --- a/openstackclient/tests/unit/identity/v3/test_region.py +++ b/openstackclient/tests/unit/identity/v3/test_region.py @@ -19,7 +19,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestRegionCreate(identity_fakes.TestIdentityv3): +class TestRegionCreate(identity_fakes.TestIdentity): region = sdk_fakes.generate_fake_resource(_region.Region) columns = ( 'region', @@ -121,7 +121,7 @@ def test_region_create_parent_region_id(self): self.assertEqual(self.datalist, data) -class TestRegionDelete(identity_fakes.TestIdentityv3): +class TestRegionDelete(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -148,7 +148,7 @@ def test_region_delete_no_options(self): self.assertIsNone(result) -class TestRegionList(identity_fakes.TestIdentityv3): +class TestRegionList(identity_fakes.TestIdentity): region = sdk_fakes.generate_fake_resource(_region.Region) columns = ( 'Region', @@ -207,7 +207,7 @@ def test_region_list_parent_region_id(self): self.assertEqual(self.datalist, tuple(data)) -class TestRegionSet(identity_fakes.TestIdentityv3): +class TestRegionSet(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -280,7 +280,7 @@ def test_region_set_parent_region_id(self): self.assertIsNone(result) -class TestRegionShow(identity_fakes.TestIdentityv3): +class TestRegionShow(identity_fakes.TestIdentity): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/identity/v3/test_registered_limit.py b/openstackclient/tests/unit/identity/v3/test_registered_limit.py index 792096cdeb..226c94b94f 100644 --- a/openstackclient/tests/unit/identity/v3/test_registered_limit.py +++ b/openstackclient/tests/unit/identity/v3/test_registered_limit.py @@ -10,72 +10,79 @@ # License for the specific language governing permissions and limitations # under the License. -import copy - -from keystoneauth1.exceptions import http as ksa_exceptions +from openstack import exceptions as sdk_exc +from openstack.identity.v3 import region as _region +from openstack.identity.v3 import registered_limit as _registered_limit +from openstack.identity.v3 import service as _service +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.identity.v3 import registered_limit -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestRegisteredLimit(identity_fakes.TestIdentityv3): +class TestRegisteredLimitCreate(identity_fakes.TestIdentity): def setUp(self): super().setUp() - self.registered_limit_mock = self.identity_client.registered_limits - - self.services_mock = self.identity_client.services - self.services_mock.reset_mock() - - self.regions_mock = self.identity_client.regions - self.regions_mock.reset_mock() + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + self.description = 'default limit of foobars' + self.default_limit = 10 + self.resource_name = 'foobars' -class TestRegisteredLimitCreate(TestRegisteredLimit): - def setUp(self): - super().setUp() + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region - self.service = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.SERVICE), loaded=True + self.registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, ) - self.services_mock.get.return_value = self.service - - self.region = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.REGION), loaded=True + self.registered_limit_with_options = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=self.description, + region_id=self.region.id, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, ) - self.regions_mock.get.return_value = self.region self.cmd = registered_limit.CreateRegisteredLimit(self.app, None) def test_registered_limit_create_without_options(self): - self.registered_limit_mock.create.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.REGISTERED_LIMIT), loaded=True + self.identity_sdk_client.create_registered_limit.return_value = ( + self.registered_limit ) - resource_name = identity_fakes.registered_limit_resource_name - default_limit = identity_fakes.registered_limit_default_limit arglist = [ '--service', - identity_fakes.service_id, + self.service.id, '--default-limit', - '10', - resource_name, + str(self.default_limit), + self.resource_name, ] verifylist = [ - ('service', identity_fakes.service_id), - ('default_limit', default_limit), - ('resource_name', resource_name), + ('service', self.service.id), + ('default_limit', self.default_limit), + ('resource_name', self.resource_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - kwargs = {'description': None, 'region': None} - self.registered_limit_mock.create.assert_called_with( - self.service, resource_name, default_limit, **kwargs + kwargs = { + 'service_id': self.service.id, + 'default_limit': self.default_limit, + 'resource_name': self.resource_name, + } + self.identity_sdk_client.create_registered_limit.assert_called_with( + **kwargs ) collist = ( @@ -89,51 +96,52 @@ def test_registered_limit_create_without_options(self): self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, + self.default_limit, None, - identity_fakes.registered_limit_id, + self.registered_limit.id, None, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_registered_limit_create_with_options(self): - self.registered_limit_mock.create.return_value = fakes.FakeResource( - None, - copy.deepcopy(identity_fakes.REGISTERED_LIMIT_OPTIONS), - loaded=True, + self.identity_sdk_client.create_registered_limit.return_value = ( + self.registered_limit_with_options ) - resource_name = identity_fakes.registered_limit_resource_name - default_limit = identity_fakes.registered_limit_default_limit - description = identity_fakes.registered_limit_description arglist = [ '--region', - identity_fakes.region_id, + self.region.id, '--description', - description, + self.description, '--service', - identity_fakes.service_id, + self.service.id, '--default-limit', - '10', - resource_name, + str(self.default_limit), + self.resource_name, ] verifylist = [ - ('region', identity_fakes.region_id), - ('description', description), - ('service', identity_fakes.service_id), - ('default_limit', default_limit), - ('resource_name', resource_name), + ('region', self.region.id), + ('description', self.description), + ('service', self.service.id), + ('default_limit', self.default_limit), + ('resource_name', self.resource_name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - kwargs = {'description': description, 'region': self.region} - self.registered_limit_mock.create.assert_called_with( - self.service, resource_name, default_limit, **kwargs + kwargs = { + 'description': self.description, + 'region_id': self.region.id, + 'service_id': self.service.id, + 'default_limit': self.default_limit, + 'resource_name': self.resource_name, + } + self.identity_sdk_client.create_registered_limit.assert_called_with( + **kwargs ) collist = ( @@ -147,44 +155,47 @@ def test_registered_limit_create_with_options(self): self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, - description, - identity_fakes.registered_limit_id, - identity_fakes.region_id, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.default_limit, + self.description, + self.registered_limit_with_options.id, + self.region.id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestRegisteredLimitDelete(TestRegisteredLimit): +class TestRegisteredLimitDelete(identity_fakes.TestIdentity): def setUp(self): super().setUp() self.cmd = registered_limit.DeleteRegisteredLimit(self.app, None) def test_registered_limit_delete(self): - self.registered_limit_mock.delete.return_value = None + self.registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + ) + self.identity_sdk_client.delete_registered_limit.return_value = None - arglist = [identity_fakes.registered_limit_id] - verifylist = [ - ('registered_limit_id', [identity_fakes.registered_limit_id]) - ] + arglist = [self.registered_limit.id] + verifylist = [('registered_limits', [self.registered_limit.id])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.registered_limit_mock.delete.assert_called_with( - identity_fakes.registered_limit_id + self.identity_sdk_client.delete_registered_limit.assert_called_with( + self.registered_limit.id, + ignore_missing=False, ) self.assertIsNone(result) def test_registered_limit_delete_with_exception(self): - return_value = ksa_exceptions.NotFound() - self.registered_limit_mock.delete.side_effect = return_value + self.identity_sdk_client.delete_registered_limit.side_effect = ( + sdk_exc.ResourceNotFound + ) arglist = ['fake-registered-limit-id'] - verifylist = [('registered_limit_id', ['fake-registered-limit-id'])] + verifylist = [('registered_limits', ['fake-registered-limit-id'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) try: @@ -196,27 +207,52 @@ def test_registered_limit_delete_with_exception(self): ) -class TestRegisteredLimitShow(TestRegisteredLimit): +class TestRegisteredLimitShow(identity_fakes.TestIdentity): def setUp(self): super().setUp() - self.registered_limit_mock.get.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.REGISTERED_LIMIT), loaded=True + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + + self.description = 'default limit of foobars' + self.default_limit = 10 + self.resource_name = 'foobars' + + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + + self.registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.registered_limit_with_options = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=self.description, + region_id=self.region.id, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, ) self.cmd = registered_limit.ShowRegisteredLimit(self.app, None) def test_registered_limit_show(self): - arglist = [identity_fakes.registered_limit_id] - verifylist = [ - ('registered_limit_id', identity_fakes.registered_limit_id) - ] + self.identity_sdk_client.get_registered_limit.return_value = ( + self.registered_limit + ) + + arglist = [self.registered_limit.id] + verifylist = [('registered_limit_id', self.registered_limit.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.get.assert_called_with( - identity_fakes.registered_limit_id + self.identity_sdk_client.get_registered_limit.assert_called_with( + self.registered_limit.id ) collist = ( @@ -229,50 +265,107 @@ def test_registered_limit_show(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, + self.default_limit, None, - identity_fakes.registered_limit_id, + self.registered_limit.id, None, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.resource_name, + self.service.id, + ) + self.assertEqual(datalist, data) + + def test_registered_limit_show_with_options(self): + self.identity_sdk_client.get_registered_limit.return_value = ( + self.registered_limit_with_options + ) + + arglist = [self.registered_limit_with_options.id] + verifylist = [ + ('registered_limit_id', self.registered_limit_with_options.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.get_registered_limit.assert_called_with( + self.registered_limit_with_options.id + ) + + collist = ( + 'default_limit', + 'description', + 'id', + 'region_id', + 'resource_name', + 'service_id', + ) + self.assertEqual(collist, columns) + datalist = ( + self.default_limit, + self.description, + self.registered_limit_with_options.id, + self.region.id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestRegisteredLimitSet(TestRegisteredLimit): +class TestRegisteredLimitSet(identity_fakes.TestIdentity): def setUp(self): super().setUp() + + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.region = sdk_fakes.generate_fake_resource(_region.Region) + + self.default_limit = 10 + self.resource_name = 'foobars' + + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + + self.registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.cmd = registered_limit.SetRegisteredLimit(self.app, None) def test_registered_limit_set_description(self): - registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT) - registered_limit['description'] = ( - identity_fakes.registered_limit_description + updated_description = 'default limit of foobars' + updated_registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + id=self.registered_limit.id, + description=updated_description, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, ) - self.registered_limit_mock.update.return_value = fakes.FakeResource( - None, registered_limit, loaded=True + self.identity_sdk_client.update_registered_limit.return_value = ( + updated_registered_limit ) arglist = [ '--description', - identity_fakes.registered_limit_description, - identity_fakes.registered_limit_id, + updated_description, + self.registered_limit.id, ] verifylist = [ - ('description', identity_fakes.registered_limit_description), - ('registered_limit_id', identity_fakes.registered_limit_id), + ('description', updated_description), + ('registered_limit_id', self.registered_limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.update.assert_called_with( - identity_fakes.registered_limit_id, - service=None, - resource_name=None, - default_limit=None, - description=identity_fakes.registered_limit_description, - region=None, + self.identity_sdk_client.update_registered_limit.assert_called_with( + self.registered_limit.id, + description=updated_description, ) collist = ( @@ -285,43 +378,46 @@ def test_registered_limit_set_description(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, - identity_fakes.registered_limit_description, - identity_fakes.registered_limit_id, + self.default_limit, + updated_description, + self.registered_limit.id, None, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_registered_limit_set_default_limit(self): - registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT) - default_limit = 20 - registered_limit['default_limit'] = default_limit - self.registered_limit_mock.update.return_value = fakes.FakeResource( - None, registered_limit, loaded=True + updated_default_limit = 20 + updated_registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + id=self.registered_limit.id, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=updated_default_limit, + resource_name=self.resource_name, + ) + self.identity_sdk_client.update_registered_limit.return_value = ( + updated_registered_limit ) arglist = [ '--default-limit', - str(default_limit), - identity_fakes.registered_limit_id, + str(updated_default_limit), + self.registered_limit.id, ] verifylist = [ - ('default_limit', default_limit), - ('registered_limit_id', identity_fakes.registered_limit_id), + ('default_limit', updated_default_limit), + ('registered_limit_id', self.registered_limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.update.assert_called_with( - identity_fakes.registered_limit_id, - service=None, - resource_name=None, - default_limit=default_limit, - description=None, - region=None, + self.identity_sdk_client.update_registered_limit.assert_called_with( + self.registered_limit.id, + default_limit=updated_default_limit, ) collist = ( @@ -334,43 +430,46 @@ def test_registered_limit_set_default_limit(self): ) self.assertEqual(collist, columns) datalist = ( - default_limit, + updated_default_limit, None, - identity_fakes.registered_limit_id, + self.registered_limit.id, None, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_registered_limit_set_resource_name(self): - registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT) - resource_name = 'volumes' - registered_limit['resource_name'] = resource_name - self.registered_limit_mock.update.return_value = fakes.FakeResource( - None, registered_limit, loaded=True + updated_resource_name = 'volumes' + updated_registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + id=self.registered_limit.id, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=updated_resource_name, + ) + self.identity_sdk_client.update_registered_limit.return_value = ( + updated_registered_limit ) arglist = [ '--resource-name', - resource_name, - identity_fakes.registered_limit_id, + updated_resource_name, + self.registered_limit.id, ] verifylist = [ - ('resource_name', resource_name), - ('registered_limit_id', identity_fakes.registered_limit_id), + ('resource_name', updated_resource_name), + ('registered_limit_id', self.registered_limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.update.assert_called_with( - identity_fakes.registered_limit_id, - service=None, - resource_name=resource_name, - default_limit=None, - description=None, - region=None, + self.identity_sdk_client.update_registered_limit.assert_called_with( + self.registered_limit.id, + resource_name=updated_resource_name, ) collist = ( @@ -383,40 +482,43 @@ def test_registered_limit_set_resource_name(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, + self.default_limit, None, - identity_fakes.registered_limit_id, + self.registered_limit.id, None, - resource_name, - identity_fakes.service_id, + updated_resource_name, + self.service.id, ) self.assertEqual(datalist, data) def test_registered_limit_set_service(self): - registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT) - service = identity_fakes.FakeService.create_one_service() - registered_limit['service_id'] = service.id - self.registered_limit_mock.update.return_value = fakes.FakeResource( - None, registered_limit, loaded=True + updated_service = sdk_fakes.generate_fake_resource(_service.Service) + self.identity_sdk_client.find_service.return_value = updated_service + updated_registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + id=self.registered_limit.id, + description=None, + region_id=None, + service_id=updated_service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.identity_sdk_client.update_registered_limit.return_value = ( + updated_registered_limit ) - self.services_mock.get.return_value = service - arglist = ['--service', service.id, identity_fakes.registered_limit_id] + arglist = ['--service', updated_service.id, self.registered_limit.id] verifylist = [ - ('service', service.id), - ('registered_limit_id', identity_fakes.registered_limit_id), + ('service', updated_service.id), + ('registered_limit_id', self.registered_limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.update.assert_called_with( - identity_fakes.registered_limit_id, - service=service, - resource_name=None, - default_limit=None, - description=None, - region=None, + self.identity_sdk_client.update_registered_limit.assert_called_with( + self.registered_limit.id, + service_id=updated_service.id, ) collist = ( @@ -429,42 +531,43 @@ def test_registered_limit_set_service(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, + self.default_limit, None, - identity_fakes.registered_limit_id, + self.registered_limit.id, None, - identity_fakes.registered_limit_resource_name, - service.id, + self.resource_name, + updated_service.id, ) self.assertEqual(datalist, data) def test_registered_limit_set_region(self): - registered_limit = copy.deepcopy(identity_fakes.REGISTERED_LIMIT) - region = identity_fakes.REGION - region['id'] = 'RegionTwo' - region = fakes.FakeResource(None, copy.deepcopy(region), loaded=True) - registered_limit['region_id'] = region.id - self.registered_limit_mock.update.return_value = fakes.FakeResource( - None, registered_limit, loaded=True + updated_region = sdk_fakes.generate_fake_resource(_region.Region) + self.identity_sdk_client.get_region.return_value = updated_region + updated_registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + id=self.registered_limit.id, + description=None, + region_id=updated_region.id, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.identity_sdk_client.update_registered_limit.return_value = ( + updated_registered_limit ) - self.regions_mock.get.return_value = region - arglist = ['--region', region.id, identity_fakes.registered_limit_id] + arglist = ['--region', updated_region.id, self.registered_limit.id] verifylist = [ - ('region', region.id), - ('registered_limit_id', identity_fakes.registered_limit_id), + ('region', updated_region.id), + ('registered_limit_id', self.registered_limit.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.update.assert_called_with( - identity_fakes.registered_limit_id, - service=None, - resource_name=None, - default_limit=None, - description=None, - region=region, + self.identity_sdk_client.update_registered_limit.assert_called_with( + self.registered_limit.id, + region_id=updated_region.id, ) collist = ( @@ -477,54 +580,86 @@ def test_registered_limit_set_region(self): ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, + self.default_limit, None, - identity_fakes.registered_limit_id, - region.id, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, + self.registered_limit.id, + updated_region.id, + self.resource_name, + self.service.id, ) self.assertEqual(datalist, data) -class TestRegisteredLimitList(TestRegisteredLimit): +class TestRegisteredLimitList(identity_fakes.TestIdentity): def setUp(self): super().setUp() - self.registered_limit_mock.get.return_value = fakes.FakeResource( - None, copy.deepcopy(identity_fakes.REGISTERED_LIMIT), loaded=True - ) + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.region = sdk_fakes.generate_fake_resource(_region.Region) - self.cmd = registered_limit.ShowRegisteredLimit(self.app, None) + self.description = 'default limit of foobars' + self.default_limit = 10 + self.resource_name = 'foobars' - def test_limit_show(self): - arglist = [identity_fakes.registered_limit_id] - verifylist = [ - ('registered_limit_id', identity_fakes.registered_limit_id) + self.identity_sdk_client.find_service.return_value = self.service + self.identity_sdk_client.get_region.return_value = self.region + + self.registered_limit = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=None, + region_id=None, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.registered_limit_with_options = sdk_fakes.generate_fake_resource( + resource_type=_registered_limit.RegisteredLimit, + description=self.description, + region_id=self.region.id, + service_id=self.service.id, + default_limit=self.default_limit, + resource_name=self.resource_name, + ) + self.identity_sdk_client.registered_limits.return_value = [ + self.registered_limit, + self.registered_limit_with_options, ] + + self.cmd = registered_limit.ListRegisteredLimit(self.app, None) + + def test_registered_limit_list(self): + arglist = [] + verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.registered_limit_mock.get.assert_called_with( - identity_fakes.registered_limit_id - ) - + self.identity_sdk_client.registered_limits.assert_called_with() collist = ( - 'default_limit', - 'description', - 'id', - 'region_id', - 'resource_name', - 'service_id', + "ID", + "Service ID", + "Resource Name", + "Default Limit", + "Description", + "Region ID", ) self.assertEqual(collist, columns) datalist = ( - identity_fakes.registered_limit_default_limit, - None, - identity_fakes.registered_limit_id, - None, - identity_fakes.registered_limit_resource_name, - identity_fakes.service_id, - ) - self.assertEqual(datalist, data) + ( + self.registered_limit.id, + self.service.id, + self.resource_name, + self.default_limit, + None, + None, + ), + ( + self.registered_limit_with_options.id, + self.service.id, + self.resource_name, + self.default_limit, + self.description, + self.region.id, + ), + ) + self.assertEqual(datalist, tuple(data)) diff --git a/openstackclient/tests/unit/identity/v3/test_role.py b/openstackclient/tests/unit/identity/v3/test_role.py index e4a27ed113..3c0d0a78e8 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -15,8 +15,6 @@ from unittest import mock -from osc_lib import exceptions - from openstack import exceptions as sdk_exc from openstack.identity.v3 import domain as _domain from openstack.identity.v3 import group as _group @@ -25,97 +23,18 @@ from openstack.identity.v3 import system as _system from openstack.identity.v3 import user as _user from openstack.test import fakes as sdk_fakes +from osc_lib import exceptions from openstackclient.identity.v3 import role from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -from openstackclient.tests.unit import utils as test_utils -class TestRoleInherited(identity_fakes.TestIdentityv3): +class TestRoleInherited(identity_fakes.TestIdentity): def _is_inheritance_testcase(self): return True -class TestFindSDKId(test_utils.TestCase): - def setUp(self): - super().setUp() - self.user = sdk_fakes.generate_fake_resource(_user.User) - self.identity_sdk_client = mock.Mock() - self.identity_sdk_client.find_user = mock.Mock() - - def test_find_sdk_id_validate(self): - self.identity_sdk_client.find_user.side_effect = [self.user] - - result = role._find_sdk_id( - self.identity_sdk_client.find_user, - name_or_id=self.user.id, - validate_actor_existence=True, - ) - self.assertEqual(self.user.id, result) - - def test_find_sdk_id_no_validate(self): - self.identity_sdk_client.find_user.side_effect = [self.user] - - result = role._find_sdk_id( - self.identity_sdk_client.find_user, - name_or_id=self.user.id, - validate_actor_existence=False, - ) - self.assertEqual(self.user.id, result) - - def test_find_sdk_id_not_found_validate(self): - self.identity_sdk_client.find_user.side_effect = [ - sdk_exc.ResourceNotFound, - ] - - self.assertRaises( - exceptions.CommandError, - role._find_sdk_id, - self.identity_sdk_client.find_user, - name_or_id=self.user.id, - validate_actor_existence=True, - ) - - def test_find_sdk_id_not_found_no_validate(self): - self.identity_sdk_client.find_user.side_effect = [ - sdk_exc.ResourceNotFound, - ] - - result = role._find_sdk_id( - self.identity_sdk_client.find_user, - name_or_id=self.user.id, - validate_actor_existence=False, - ) - self.assertEqual(self.user.id, result) - - def test_find_sdk_id_forbidden_validate(self): - self.identity_sdk_client.find_user.side_effect = [ - sdk_exc.ForbiddenException, - ] - - result = role._find_sdk_id( - self.identity_sdk_client.find_user, - name_or_id=self.user.id, - validate_actor_existence=True, - ) - - self.assertEqual(self.user.id, result) - - def test_find_sdk_id_forbidden_no_validate(self): - self.identity_sdk_client.find_user.side_effect = [ - sdk_exc.ForbiddenException, - ] - - result = role._find_sdk_id( - self.identity_sdk_client.find_user, - name_or_id=self.user.id, - validate_actor_existence=False, - ) - - self.assertEqual(self.user.id, result) - - -class TestRoleAdd(identity_fakes.TestIdentityv3): +class TestRoleAdd(identity_fakes.TestIdentity): def _is_inheritance_testcase(self): return False @@ -442,7 +361,7 @@ class TestRoleAddInherited(TestRoleAdd, TestRoleInherited): pass -class TestRoleCreate(identity_fakes.TestIdentityv3): +class TestRoleCreate(identity_fakes.TestIdentity): collist = ('id', 'name', 'domain_id', 'description') domain = sdk_fakes.generate_fake_resource(_domain.Domain) @@ -494,7 +413,6 @@ def test_role_create_no_options(self): # Set expected values kwargs = { 'name': self.role.name, - 'options': {}, } self.identity_sdk_client.create_role.assert_called_with(**kwargs) @@ -533,7 +451,6 @@ def test_role_create_with_domain(self): kwargs = { 'domain_id': self.domain.id, 'name': self.role_with_domain.name, - 'options': {}, } self.identity_sdk_client.create_role.assert_called_with(**kwargs) @@ -572,7 +489,6 @@ def test_role_create_with_description(self): kwargs = { 'name': self.role_with_description.name, 'description': self.role_with_description.description, - 'options': {}, } self.identity_sdk_client.create_role.assert_called_with(**kwargs) @@ -629,7 +545,7 @@ def test_role_create_with_no_immutable_option(self): self.role.name, ] verifylist = [ - ('no_immutable', True), + ('immutable', False), ('name', self.role.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -657,7 +573,7 @@ def test_role_create_with_no_immutable_option(self): self.assertEqual(datalist, data) -class TestRoleDelete(identity_fakes.TestIdentityv3): +class TestRoleDelete(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -764,7 +680,7 @@ def test_delete_multi_roles_with_exception(self): ) -class TestRoleList(identity_fakes.TestIdentityv3): +class TestRoleList(identity_fakes.TestIdentity): columns = ( 'ID', 'Name', @@ -851,7 +767,7 @@ def test_role_list_domain_role(self): self.assertEqual(datalist, tuple(data)) -class TestRoleRemove(identity_fakes.TestIdentityv3): +class TestRoleRemove(identity_fakes.TestIdentity): def _is_inheritance_testcase(self): return False @@ -1397,7 +1313,7 @@ def test_role_remove_with_error(self): ) -class TestRoleSet(identity_fakes.TestIdentityv3): +class TestRoleSet(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -1437,7 +1353,6 @@ def test_role_set_no_options(self): kwargs = { 'name': 'over', 'role': self.role.id, - 'options': {}, } self.identity_sdk_client.update_role.assert_called_with(**kwargs) self.assertIsNone(result) @@ -1472,7 +1387,6 @@ def test_role_set_domain_role(self): 'name': 'over', 'role': self.role_with_domain.id, 'domain_id': self.domain2.id, - 'options': {}, } self.identity_sdk_client.update_role.assert_called_with(**kwargs) self.assertIsNone(result) @@ -1501,7 +1415,6 @@ def test_role_set_description(self): 'name': 'over', 'description': 'role description', 'role': self.role_with_domain.id, - 'options': {}, } self.identity_sdk_client.update_role.assert_called_with(**kwargs) self.assertIsNone(result) @@ -1544,7 +1457,7 @@ def test_role_set_with_no_immutable(self): ] verifylist = [ ('name', 'over'), - ('no_immutable', True), + ('immutable', False), ('role', self.role_with_domain.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1561,7 +1474,7 @@ def test_role_set_with_no_immutable(self): self.assertIsNone(result) -class TestRoleShow(identity_fakes.TestIdentityv3): +class TestRoleShow(identity_fakes.TestIdentity): domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): diff --git a/openstackclient/tests/unit/identity/v3/test_role_assignment.py b/openstackclient/tests/unit/identity/v3/test_role_assignment.py index 6fc66469c2..2f133d4973 100644 --- a/openstackclient/tests/unit/identity/v3/test_role_assignment.py +++ b/openstackclient/tests/unit/identity/v3/test_role_assignment.py @@ -25,7 +25,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestRoleAssignmentList(identity_fakes.TestIdentityv3): +class TestRoleAssignmentList(identity_fakes.TestIdentity): columns = ( 'Role', 'User', diff --git a/openstackclient/tests/unit/identity/v3/test_service.py b/openstackclient/tests/unit/identity/v3/test_service.py index d6dd0287b1..a8390ce55e 100644 --- a/openstackclient/tests/unit/identity/v3/test_service.py +++ b/openstackclient/tests/unit/identity/v3/test_service.py @@ -22,7 +22,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestServiceCreate(identity_fakes.TestIdentityv3): +class TestServiceCreate(identity_fakes.TestIdentity): columns = ( 'id', 'name', @@ -163,7 +163,7 @@ def test_service_create_disable(self): self.assertEqual(self.datalist, data) -class TestServiceDelete(identity_fakes.TestIdentityv3): +class TestServiceDelete(identity_fakes.TestIdentity): service = sdk_fakes.generate_fake_resource(_service.Service) def setUp(self): @@ -195,7 +195,7 @@ def test_service_delete_no_options(self): self.assertIsNone(result) -class TestServiceList(identity_fakes.TestIdentityv3): +class TestServiceList(identity_fakes.TestIdentity): service = sdk_fakes.generate_fake_resource(_service.Service) def setUp(self): @@ -259,7 +259,7 @@ def test_service_list_long(self): self.assertEqual(datalist, tuple(data)) -class TestServiceSet(identity_fakes.TestIdentityv3): +class TestServiceSet(identity_fakes.TestIdentity): service = sdk_fakes.generate_fake_resource(_service.Service) def setUp(self): @@ -420,7 +420,7 @@ def test_service_set_disable(self): self.assertIsNone(result) -class TestServiceShow(identity_fakes.TestIdentityv3): +class TestServiceShow(identity_fakes.TestIdentity): service = sdk_fakes.generate_fake_resource(_service.Service) def setUp(self): diff --git a/openstackclient/tests/unit/identity/v3/test_token.py b/openstackclient/tests/unit/identity/v3/test_token.py index f8d09b72cc..af5b868219 100644 --- a/openstackclient/tests/unit/identity/v3/test_token.py +++ b/openstackclient/tests/unit/identity/v3/test_token.py @@ -16,7 +16,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestTokenIssue(identity_fakes.TestIdentityv3): +class TestTokenIssue(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -99,7 +99,7 @@ def test_token_issue_with_unscoped(self): self.assertEqual(datalist, data) -class TestTokenRevoke(identity_fakes.TestIdentityv3): +class TestTokenRevoke(identity_fakes.TestIdentity): TOKEN = 'fob' def setUp(self): diff --git a/openstackclient/tests/unit/identity/v3/test_trust.py b/openstackclient/tests/unit/identity/v3/test_trust.py index 5c14b7ad98..e67e96e18c 100644 --- a/openstackclient/tests/unit/identity/v3/test_trust.py +++ b/openstackclient/tests/unit/identity/v3/test_trust.py @@ -26,7 +26,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestTrustCreate(identity_fakes.TestIdentityv3): +class TestTrustCreate(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -109,7 +109,7 @@ def test_trust_create_basic(self): self.assertEqual(datalist, data) -class TestTrustDelete(identity_fakes.TestIdentityv3): +class TestTrustDelete(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -166,7 +166,7 @@ def test_delete_multi_trusts_with_exception(self): ) -class TestTrustList(identity_fakes.TestIdentityv3): +class TestTrustList(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -341,7 +341,7 @@ def test_trust_list_trustor(self): self.assertEqual(datalist, tuple(data)) -class TestTrustShow(identity_fakes.TestIdentityv3): +class TestTrustShow(identity_fakes.TestIdentity): def setUp(self): super().setUp() diff --git a/openstackclient/tests/unit/identity/v3/test_user.py b/openstackclient/tests/unit/identity/v3/test_user.py index d9e16f174d..0172bee398 100644 --- a/openstackclient/tests/unit/identity/v3/test_user.py +++ b/openstackclient/tests/unit/identity/v3/test_user.py @@ -31,7 +31,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestUserCreate(identity_fakes.TestIdentityv3): +class TestUserCreate(identity_fakes.TestIdentity): domain = sdk_fakes.generate_fake_resource(_domain.Domain) project = sdk_fakes.generate_fake_resource(_project.Project) @@ -44,6 +44,7 @@ class TestUserCreate(identity_fakes.TestIdentityv3): 'name', 'description', 'password_expires_at', + 'options', ) def setUp(self): @@ -63,6 +64,7 @@ def setUp(self): self.user.name, self.user.description, self.user.password_expires_at, + getattr(self.user, 'options', {}), ) self.identity_sdk_client.find_domain.return_value = self.domain @@ -92,7 +94,6 @@ def test_user_create_no_options(self): kwargs = { 'name': self.user.name, 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -136,7 +137,6 @@ def test_user_create_password_prompt(self): self.user.name, ] verifylist = [ - ('password', None), ('password_prompt', True), ('enable', False), ('disable', False), @@ -163,6 +163,52 @@ def test_user_create_password_prompt(self): self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, data) + def test_user_create_password_prompt_no_warning(self): + arglist = [ + '--password-prompt', + self.user.name, + ] + verifylist = [ + ('password_prompt', True), + ('enable', False), + ('disable', False), + ('name', self.user.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + import logging + + # Mock the password prompt + mocker = mock.Mock() + mocker.return_value = 'abc123' + + # Use assertLogs to verify no warnings are logged + logger = 'openstackclient.identity.v3.user' + with mock.patch("osc_lib.utils.get_password", mocker): + with self.assertLogs(logger, level='WARNING') as log_ctx: + logging.getLogger(logger).warning( + "Dummy warning for test setup" + ) + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(1, len(log_ctx.records)) + self.assertIn( + "Dummy warning for test setup", log_ctx.output[0] + ) + self.assertNotIn( + "No password was supplied", ''.join(log_ctx.output) + ) + + # Set expected values + kwargs = { + 'name': self.user.name, + 'is_enabled': True, + 'password': 'abc123', + } + self.identity_sdk_client.create_user.assert_called_with(**kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + def test_user_create_email(self): arglist = [ '--email', @@ -187,7 +233,6 @@ def test_user_create_email(self): 'name': self.user.name, 'email': 'barney@example.com', 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -218,7 +263,6 @@ def test_user_create_project(self): 'name': self.user.name, 'default_project_id': self.project.id, 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -232,6 +276,7 @@ def test_user_create_project(self): self.user.name, self.user.description, self.user.password_expires_at, + getattr(self.user, 'options', {}), ) self.assertEqual(datalist, data) @@ -262,7 +307,6 @@ def test_user_create_project_domain(self): 'name': self.user.name, 'default_project_id': self.project.id, 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_once_with(**kwargs) self.identity_sdk_client.find_domain.assert_called_once_with( @@ -279,6 +323,7 @@ def test_user_create_project_domain(self): self.user.name, self.user.description, self.user.password_expires_at, + getattr(self.user, 'options', {}), ) self.assertEqual(datalist, data) @@ -306,7 +351,6 @@ def test_user_create_domain(self): 'name': self.user.name, 'domain_id': self.domain.id, 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -334,7 +378,6 @@ def test_user_create_enable(self): kwargs = { 'name': self.user.name, 'is_enabled': True, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -362,7 +405,6 @@ def test_user_create_disable(self): kwargs = { 'name': self.user.name, 'is_enabled': False, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -392,7 +434,6 @@ def test_user_create_ignore_lockout_failure_attempts(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_lockout_failure_attempts': True}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -422,7 +463,6 @@ def test_user_create_no_ignore_lockout_failure_attempts(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_lockout_failure_attempts': False}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -452,7 +492,6 @@ def test_user_create_ignore_password_expiry(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_password_expiry': True}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -482,7 +521,6 @@ def test_user_create_no_ignore_password_expiry(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_password_expiry': False}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -512,7 +550,6 @@ def test_user_create_ignore_change_password_upon_first_use(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_change_password_upon_first_use': True}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -542,7 +579,6 @@ def test_user_create_no_ignore_change_password_upon_first_use(self): 'name': self.user.name, 'is_enabled': True, 'options': {'ignore_change_password_upon_first_use': False}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -572,7 +608,6 @@ def test_user_create_enables_lock_password(self): 'name': self.user.name, 'is_enabled': True, 'options': {'lock_password': True}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -602,7 +637,6 @@ def test_user_create_disables_lock_password(self): 'name': self.user.name, 'is_enabled': True, 'options': {'lock_password': False}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -632,7 +666,6 @@ def test_user_create_enable_multi_factor_auth(self): 'name': self.user.name, 'is_enabled': True, 'options': {'multi_factor_auth_enabled': True}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -662,7 +695,6 @@ def test_user_create_disable_multi_factor_auth(self): 'name': self.user.name, 'is_enabled': True, 'options': {'multi_factor_auth_enabled': False}, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -679,7 +711,7 @@ def test_user_create_option_with_multi_factor_auth_rule(self): ] verifylist = [ ( - 'multi_factor_auth_rule', + 'multi_factor_auth_rules', [identity_fakes.mfa_opt1, identity_fakes.mfa_opt2], ), ('enable', False), @@ -700,7 +732,6 @@ def test_user_create_option_with_multi_factor_auth_rule(self): 'options': { 'multi_factor_auth_rules': [["password", "totp"], ["password"]] }, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -718,7 +749,7 @@ def test_user_create_with_multiple_options(self): verifylist = [ ('ignore_password_expiry', True), ('disable_multi_factor_auth', True), - ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]), + ('multi_factor_auth_rules', [identity_fakes.mfa_opt1]), ('enable', False), ('disable', False), ('name', self.user.name), @@ -739,7 +770,6 @@ def test_user_create_with_multiple_options(self): 'multi_factor_auth_enabled': False, 'multi_factor_auth_rules': [["password", "totp"]], }, - 'password': None, } self.identity_sdk_client.create_user.assert_called_with(**kwargs) @@ -747,7 +777,7 @@ def test_user_create_with_multiple_options(self): self.assertEqual(self.datalist, data) -class TestUserDelete(identity_fakes.TestIdentityv3): +class TestUserDelete(identity_fakes.TestIdentity): user = sdk_fakes.generate_fake_resource(_user.User) def setUp(self): @@ -810,7 +840,7 @@ def test_delete_multi_users_with_exception(self, find_mock): ) -class TestUserList(identity_fakes.TestIdentityv3): +class TestUserList(identity_fakes.TestIdentity): domain = sdk_fakes.generate_fake_resource(_domain.Domain) project = sdk_fakes.generate_fake_resource(_project.Project) user = sdk_fakes.generate_fake_resource( @@ -840,7 +870,7 @@ def setUp(self): self.identity_sdk_client.find_domain.return_value = self.domain self.identity_sdk_client.find_group.return_value = self.group self.identity_sdk_client.find_project.return_value = self.project - self.identity_sdk_client.role_assignments_filter.return_value = [ + self.identity_sdk_client.role_assignments.return_value = [ self.role_assignment ] @@ -978,18 +1008,101 @@ def test_user_list_project(self): columns, data = self.cmd.take_action(parsed_args) kwargs = { - 'project': self.project.id, + 'scope_project_id': self.project.id, } - self.identity_sdk_client.role_assignments_filter.assert_called_with( - **kwargs - ) + self.identity_sdk_client.role_assignments.assert_called_with(**kwargs) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) + def test_user_list_with_option_enabled(self): + arglist = ['--enabled'] + verifylist = [('is_enabled', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + kwargs = {'domain_id': None, 'is_enabled': True} + self.identity_sdk_client.users.assert_called_with(**kwargs) + self.identity_sdk_client.find_user.assert_not_called() + self.identity_sdk_client.group_users.assert_not_called() + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_user_list_with_pagination(self): + arglist = [ + '--limit', + '2', + '--marker', + 'some-marker', + ] + verifylist = [ + ('limit', 2), + ('marker', 'some-marker'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) -class TestUserSet(identity_fakes.TestIdentityv3): + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'domain_id': None, + 'limit': 2, + 'marker': 'some-marker', + } + self.identity_sdk_client.users.assert_called_with(**kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_user_list_with_pagination_and_group(self): + arglist = [ + '--group', + self.group.name, + '--limit', + '5', + ] + verifylist = [ + ('group', self.group.name), + ('limit', 5), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + kwargs = { + 'domain_id': None, + 'group': self.group.id, + 'limit': 5, + } + self.identity_sdk_client.group_users.assert_called_with(**kwargs) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, tuple(data)) + + def test_user_list_pagination_with_project_fails(self): + arglist = [ + '--project', + self.project.name, + '--limit', + '2', + ] + verifylist = [ + ('project', self.project.name), + ('limit', 2), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestUserSet(identity_fakes.TestIdentity): project = sdk_fakes.generate_fake_resource(_project.Project) domain = sdk_fakes.generate_fake_resource(_domain.Domain) user = sdk_fakes.generate_fake_resource( @@ -1017,7 +1130,6 @@ def test_user_set_no_options(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('project', None), ('enable', False), @@ -1038,7 +1150,6 @@ def test_user_set_name(self): ] verifylist = [ ('name', 'qwerty'), - ('password', None), ('email', None), ('project', None), ('enable', False), @@ -1069,7 +1180,6 @@ def test_user_set_specify_domain(self): ] verifylist = [ ('name', 'qwerty'), - ('password', None), ('domain', self.domain.id), ('email', None), ('project', None), @@ -1125,7 +1235,6 @@ def test_user_set_password_prompt(self): ] verifylist = [ ('name', None), - ('password', None), ('password_prompt', True), ('email', None), ('project', None), @@ -1158,7 +1267,6 @@ def test_user_set_email(self): ] verifylist = [ ('name', None), - ('password', None), ('email', 'barney@example.com'), ('project', None), ('enable', False), @@ -1187,7 +1295,6 @@ def test_user_set_project(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('project', self.project.id), ('enable', False), @@ -1229,7 +1336,6 @@ def test_user_set_project_domain(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('project', self.project.id), ('project_domain', self.project.domain_id), @@ -1263,7 +1369,6 @@ def test_user_set_enable(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('project', None), ('enable', True), @@ -1290,7 +1395,6 @@ def test_user_set_disable(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('project', None), ('enable', False), @@ -1317,7 +1421,6 @@ def test_user_set_ignore_lockout_failure_attempts(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('ignore_lockout_failure_attempts', True), ('project', None), @@ -1345,7 +1448,6 @@ def test_user_set_no_ignore_lockout_failure_attempts(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('no_ignore_lockout_failure_attempts', True), ('project', None), @@ -1373,7 +1475,6 @@ def test_user_set_ignore_password_expiry(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('ignore_password_expiry', True), ('project', None), @@ -1401,7 +1502,6 @@ def test_user_set_no_ignore_password_expiry(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('no_ignore_password_expiry', True), ('project', None), @@ -1429,7 +1529,6 @@ def test_user_set_ignore_change_password_upon_first_use(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('ignore_change_password_upon_first_use', True), ('project', None), @@ -1457,7 +1556,6 @@ def test_user_set_no_ignore_change_password_upon_first_use(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('no_ignore_change_password_upon_first_use', True), ('project', None), @@ -1485,7 +1583,6 @@ def test_user_set_enable_lock_password(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('enable_lock_password', True), ('project', None), @@ -1513,7 +1610,6 @@ def test_user_set_disable_lock_password(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('disable_lock_password', True), ('project', None), @@ -1541,7 +1637,6 @@ def test_user_set_enable_multi_factor_auth(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('enable_multi_factor_auth', True), ('project', None), @@ -1569,7 +1664,6 @@ def test_user_set_disable_multi_factor_auth(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('disable_multi_factor_auth', True), ('project', None), @@ -1598,9 +1692,8 @@ def test_user_set_option_multi_factor_auth_rule(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), - ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]), + ('multi_factor_auth_rules', [identity_fakes.mfa_opt1]), ('project', None), ('enable', False), ('disable', False), @@ -1630,11 +1723,10 @@ def test_user_set_with_multiple_options(self): ] verifylist = [ ('name', None), - ('password', None), ('email', None), ('ignore_password_expiry', True), ('enable_multi_factor_auth', True), - ('multi_factor_auth_rule', [identity_fakes.mfa_opt1]), + ('multi_factor_auth_rules', [identity_fakes.mfa_opt1]), ('project', None), ('enable', False), ('disable', False), @@ -1659,7 +1751,7 @@ def test_user_set_with_multiple_options(self): self.assertIsNone(result) -class TestUserSetPassword(identity_fakes.TestIdentityv3): +class TestUserSetPassword(identity_fakes.TestIdentity): def setUp(self): super().setUp() self.cmd = user.SetPasswordUser(self.app, None) @@ -1691,7 +1783,7 @@ def test_user_password_change(self): conn = self.app.client_manager.sdk_connection user_id = conn.config.get_auth().get_user_id(conn.identity) - self.identity_sdk_client.update_user.assert_called_with( + self.identity_sdk_client.update_password.assert_called_with( user=user_id, current_password=current_pass, password=new_pass ) @@ -1708,7 +1800,7 @@ def test_user_create_password_prompt(self): conn = self.app.client_manager.sdk_connection user_id = conn.config.get_auth().get_user_id(conn.identity) - self.identity_sdk_client.update_user.assert_called_with( + self.identity_sdk_client.update_password.assert_called_with( user=user_id, current_password=current_pass, password=new_pass ) @@ -1733,12 +1825,12 @@ def test_user_password_change_no_prompt(self): conn = self.app.client_manager.sdk_connection user_id = conn.config.get_auth().get_user_id(conn.identity) - self.identity_sdk_client.update_user.assert_called_with( + self.identity_sdk_client.update_password.assert_called_with( user=user_id, current_password=current_pass, password=new_pass ) -class TestUserShow(identity_fakes.TestIdentityv3): +class TestUserShow(identity_fakes.TestIdentity): user = sdk_fakes.generate_fake_resource(_user.User) def setUp(self): @@ -1748,7 +1840,7 @@ def setUp(self): # Get the command object to test self.cmd = user.ShowUser(self.app, None) - self.identity_client.auth.client.get_user_id.return_value = ( # noqa: E501 + self.identity_client.auth.client.get_user_id.return_value = ( self.user.id ) self.identity_client.tokens.get_token_data.return_value = { @@ -1788,6 +1880,7 @@ def test_user_show(self): 'name', 'description', 'password_expires_at', + 'options', ) self.assertEqual(collist, columns) datalist = ( @@ -1799,6 +1892,7 @@ def test_user_show(self): self.user.name, self.user.description, self.user.password_expires_at, + getattr(self.user, 'options', {}), ) self.assertEqual(datalist, data) diff --git a/openstackclient/tests/unit/image/v1/fakes.py b/openstackclient/tests/unit/image/v1/fakes.py index 30503e1f02..0b9edc2503 100644 --- a/openstackclient/tests/unit/image/v1/fakes.py +++ b/openstackclient/tests/unit/image/v1/fakes.py @@ -15,19 +15,21 @@ from unittest import mock import uuid -from openstack.image.v1 import _proxy +from openstack.block_storage import v2 as block_storage_v2 +from openstack.image import v1 as image_v1 from openstack.image.v1 import image -from openstackclient.tests.unit import fakes from openstackclient.tests.unit import utils -from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes class FakeClientMixin: def setUp(self): super().setUp() - self.app.client_manager.image = mock.Mock(spec=_proxy.Proxy) + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.app.client_manager.image = mock.Mock(spec=image_v1.Proxy) self.image_client = self.app.client_manager.image @@ -35,10 +37,13 @@ class TestImagev1(FakeClientMixin, utils.TestCommand): def setUp(self): super().setUp() - self.app.client_manager.volume = volume_fakes.FakeVolumeClient( - endpoint=fakes.AUTH_URL, - token=fakes.AUTH_TOKEN, - ) + # avoid circular imports by defining this manually rather than using + # openstackclient.tests.unit.volume.v2.fakes.FakeClientMixin + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.app.client_manager.volume = mock.Mock(spec=block_storage_v2.Proxy) + self.app.client_manager.volume.api_version = '2' self.volume_client = self.app.client_manager.volume diff --git a/openstackclient/tests/unit/image/v1/test_image.py b/openstackclient/tests/unit/image/v1/test_image.py index 971e969c7b..6b55acb7f4 100644 --- a/openstackclient/tests/unit/image/v1/test_image.py +++ b/openstackclient/tests/unit/image/v1/test_image.py @@ -15,10 +15,11 @@ import copy from unittest import mock +from openstack.block_storage.v2 import volume as _volume +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from openstackclient.image.v1 import image -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.image.v1 import fakes as image_fakes @@ -54,9 +55,9 @@ class TestImageCreate(image_fakes.TestImagev1): def setUp(self): super().setUp() - self.image_client.create_image = mock.Mock(return_value=self.new_image) - self.image_client.find_image = mock.Mock(return_value=self.new_image) - self.image_client.update_image = mock.Mock(return_image=self.new_image) + self.image_client.create_image.return_value = self.new_image + self.image_client.find_image.return_value = self.new_image + self.image_client.update_image.return_value = self.new_image # Get the command object to test self.cmd = image.CreateImage(self.app, None) @@ -212,8 +213,8 @@ def setUp(self): super().setUp() # This is the return value for utils.find_resource() - self.image_client.find_image = mock.Mock(return_value=self._image) - self.image_client.delete_image = mock.Mock(return_value=None) + self.image_client.find_image.return_value = self._image + self.image_client.delete_image.return_value = None # Get the command object to test self.cmd = image.DeleteImage(self.app, None) @@ -261,7 +262,6 @@ class TestImageList(image_fakes.TestImagev1): def setUp(self): super().setUp() - self.image_client.images = mock.Mock() self.image_client.images.side_effect = [ [self._image], [], @@ -441,8 +441,8 @@ def setUp(self): super().setUp() # This is the return value for utils.find_resource() - self.image_client.find_image = mock.Mock(return_value=self._image) - self.image_client.update_image = mock.Mock(return_value=self._image) + self.image_client.find_image.return_value = self._image + self.image_client.update_image.return_value = self._image # Get the command object to test self.cmd = image.SetImage(self.app, None) @@ -592,27 +592,8 @@ def test_image_set_properties(self): self.assertIsNone(result) def test_image_update_volume(self): - # Set up VolumeManager Mock - volumes_mock = self.volume_client.volumes - volumes_mock.reset_mock() - volumes_mock.get.return_value = fakes.FakeResource( - None, - copy.deepcopy({'id': 'vol1', 'name': 'volly'}), - loaded=True, - ) - response = { - "id": 'volume_id', - "updated_at": 'updated_at', - "status": 'uploading', - "display_description": 'desc', - "size": 'size', - "volume_type": 'volume_type', - "container_format": image.DEFAULT_CONTAINER_FORMAT, - "disk_format": image.DEFAULT_DISK_FORMAT, - "image": self._image.name, - } - full_response = {"os-volume_upload_image": response} - volumes_mock.upload_to_image.return_value = (201, full_response) + source_volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = source_volume arglist = [ '--volume', @@ -633,18 +614,20 @@ def test_image_update_volume(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + source_volume.upload_to_image = mock.Mock() + result = self.cmd.take_action(parsed_args) - # VolumeManager.upload_to_image(volume, force, image_name, - # container_format, disk_format) - volumes_mock.upload_to_image.assert_called_with( - 'vol1', - False, + self.volume_client.find_volume.assert_called_once_with( + 'volly', ignore_missing=False + ) + source_volume.upload_to_image.assert_called_once_with( + self.volume_client, self._image.name, - '', - '', + force=False, + container_format=self._image.container_format, + disk_format=self._image.disk_format, ) - # ImageManager.update(image_id, remove_props=, **) self.image_client.update_image.assert_called_with( self._image.id, name='updated_image', @@ -712,7 +695,7 @@ class TestImageShow(image_fakes.TestImagev1): def setUp(self): super().setUp() - self.image_client.find_image = mock.Mock(return_value=self._image) + self.image_client.find_image.return_value = self._image # Get the command object to test self.cmd = image.ShowImage(self.app, None) @@ -758,3 +741,50 @@ def test_image_show_human_readable(self): size_index = columns.index('size') self.assertEqual(data[size_index].human_readable(), '2K') + + +class TestImageSave(image_fakes.TestImagev1): + image = image_fakes.create_one_image({}) + + def setUp(self): + super().setUp() + + self.image_client.find_image.return_value = self.image + self.image_client.download_image.return_value = self.image + + # Get the command object to test + self.cmd = image.SaveImage(self.app, None) + + def test_save_data(self): + arglist = ['--file', '/path/to/file', self.image.id] + + verifylist = [('file', '/path/to/file'), ('image', self.image.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.image_client.download_image.assert_called_once_with( + self.image.id, output='/path/to/file', stream=True, chunk_size=1024 + ) + + def test_save_data_with_chunk_size(self): + arglist = [ + '--file', + '/path/to/file', + '--chunk-size', + '2048', + self.image.id, + ] + + verifylist = [ + ('file', '/path/to/file'), + ('chunk_size', 2048), + ('image', self.image.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.image_client.download_image.assert_called_once_with( + self.image.id, output='/path/to/file', stream=True, chunk_size=2048 + ) diff --git a/openstackclient/tests/unit/image/v2/fakes.py b/openstackclient/tests/unit/image/v2/fakes.py index 27f4777bc8..5fe9622cac 100644 --- a/openstackclient/tests/unit/image/v2/fakes.py +++ b/openstackclient/tests/unit/image/v2/fakes.py @@ -16,7 +16,7 @@ from unittest import mock import uuid -from openstack.image.v2 import _proxy +from openstack.image import v2 as image_v2 from openstack.image.v2 import cache from openstack.image.v2 import image from openstack.image.v2 import member @@ -35,7 +35,10 @@ class FakeClientMixin: def setUp(self): super().setUp() - self.app.client_manager.image = mock.Mock(spec=_proxy.Proxy) + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.app.client_manager.image = mock.Mock(spec=image_v2.Proxy) self.image_client = self.app.client_manager.image diff --git a/openstackclient/tests/unit/image/v2/test_image.py b/openstackclient/tests/unit/image/v2/test_image.py index 9b44c1578c..91dadc5622 100644 --- a/openstackclient/tests/unit/image/v2/test_image.py +++ b/openstackclient/tests/unit/image/v2/test_image.py @@ -19,31 +19,18 @@ from openstack.block_storage.v2 import volume as _volume from openstack import exceptions as sdk_exceptions +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from openstackclient.image.v2 import image as _image -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.image.v2 import fakes as image_fakes from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes -class TestImage(image_fakes.TestImagev2, volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - # Get shortcut to the Mocks in identity client - self.project_mock = self.identity_client.projects - self.project_mock.reset_mock() - self.domain_mock = self.identity_client.domains - self.domain_mock.reset_mock() - - -class TestImageCreate(TestImage): - project = identity_fakes.FakeProject.create_one_project() - domain = identity_fakes.FakeDomain.create_one_domain() - +class TestImageCreate(image_fakes.TestImagev2, volume_fakes.TestVolume): def setUp(self): super().setUp() @@ -52,11 +39,12 @@ def setUp(self): self.image_client.update_image.return_value = self.new_image self.image_client.get_image.return_value = self.new_image - self.project_mock.get.return_value = self.project + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project - self.domain_mock.get.return_value = self.domain + self.domain = sdk_fakes.generate_fake_resource(_domain.Domain) - (self.expected_columns, self.expected_data) = zip( + self.expected_columns, self.expected_data = zip( *sorted(_image._format_image(self.new_image).items()) ) @@ -149,8 +137,9 @@ def test_image_reserve_options(self, raw_input): self.assertCountEqual(self.expected_data, data) def test_image_create_with_unexist_project(self): - self.project_mock.get.side_effect = exceptions.NotFound(None) - self.project_mock.find.side_effect = exceptions.NotFound(None) + self.identity_sdk_client.find_project.side_effect = ( + sdk_exceptions.ResourceNotFound() + ) arglist = [ '--container-format', @@ -305,7 +294,7 @@ def test_image_create_import(self, raw_input): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.create_image.assert_called_with( name=self.new_image.name, @@ -321,8 +310,8 @@ def test_image_create_from_volume(self, mock_get_data_f): mock_get_data_f.return_value = None volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.find_volume.return_value = volume - self.volume_sdk_client.upload_volume_to_image.return_value = { + self.volume_client.find_volume.return_value = volume + self.volume_client.upload_volume_to_image.return_value = { 'volume_type': {'name': 'fake_type'} } @@ -336,9 +325,9 @@ def test_image_create_from_volume(self, mock_get_data_f): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) - self.volume_sdk_client.upload_volume_to_image.assert_called_once_with( + self.volume_client.upload_volume_to_image.assert_called_once_with( volume.id, self.new_image.name, force=False, @@ -353,8 +342,8 @@ def test_image_create_from_volume_pre_v31(self, mock_get_data_f): mock_get_data_f.return_value = None volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.find_volume.return_value = volume - self.volume_sdk_client.upload_volume_to_image.return_value = { + self.volume_client.find_volume.return_value = volume + self.volume_client.upload_volume_to_image.return_value = { 'volume_type': {'name': 'fake_type'} } @@ -381,8 +370,8 @@ def test_image_create_from_volume_v31(self, mock_get_data_f): mock_get_data_f.return_value = None volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.find_volume.return_value = volume - self.volume_sdk_client.upload_volume_to_image.return_value = { + self.volume_client.find_volume.return_value = volume + self.volume_client.upload_volume_to_image.return_value = { 'volume_type': {'name': 'fake_type'} } @@ -397,9 +386,9 @@ def test_image_create_from_volume_v31(self, mock_get_data_f): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) - self.volume_sdk_client.upload_volume_to_image.assert_called_once_with( + self.volume_client.upload_volume_to_image.assert_called_once_with( volume.id, self.new_image.name, force=False, @@ -410,9 +399,9 @@ def test_image_create_from_volume_v31(self, mock_get_data_f): ) -class TestAddProjectToImage(TestImage): - project = identity_fakes.FakeProject.create_one_project() - domain = identity_fakes.FakeDomain.create_one_domain() +class TestAddProjectToImage(image_fakes.TestImagev2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) _image = image_fakes.create_one_image() new_member = image_fakes.create_one_image_member( attrs={'image_id': _image.id, 'member_id': project.id} @@ -444,8 +433,7 @@ def setUp(self): # Update the image_id in the MEMBER dict self.image_client.add_member.return_value = self.new_member - self.project_mock.get.return_value = self.project - self.domain_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project # Get the command object to test self.cmd = _image.AddProjectToImage(self.app, None) @@ -497,7 +485,7 @@ def test_add_project_to_image_with_option(self): self.assertEqual(self.datalist, data) -class TestImageDelete(TestImage): +class TestImageDelete(image_fakes.TestImagev2): def setUp(self): super().setUp() @@ -617,7 +605,7 @@ def test_image_delete_multi_images_exception(self): self.image_client.delete_image.assert_has_calls(calls) -class TestImageList(TestImage): +class TestImageList(image_fakes.TestImagev2): _image = image_fakes.create_one_image() columns = ( @@ -822,6 +810,8 @@ def test_image_list_long_option(self): 'Visibility', 'Protected', 'Project', + 'Hash Algorithm', + 'Hash Value', 'Tags', ) @@ -830,14 +820,16 @@ def test_image_list_long_option(self): ( self._image.id, self._image.name, - None, - None, - None, - None, - None, + self._image.disk_format, + self._image.container_format, + self._image.size, + self._image.checksum, + self._image.status, self._image.visibility, self._image.is_protected, self._image.owner_id, + self._image.hash_algo, + self._image.hash_value, format_columns.ListColumn(self._image.tags), ), ) @@ -914,7 +906,7 @@ def test_image_list_limit_option(self): self.assertEqual(ret_limit, len(tuple(data))) def test_image_list_project_option(self): - self.image_client.find_image = mock.Mock(return_value=self._image) + self.image_client.find_image.return_value = self._image arglist = [ '--project', 'nova', @@ -931,7 +923,7 @@ def test_image_list_project_option(self): @mock.patch('osc_lib.utils.find_resource') def test_image_list_marker_option(self, fr_mock): - self.image_client.find_image = mock.Mock(return_value=self._image) + self.image_client.find_image.return_value = self._image arglist = [ '--marker', @@ -942,7 +934,7 @@ def test_image_list_marker_option(self, fr_mock): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.images.assert_called_with( marker=self._image.id, ) @@ -962,7 +954,7 @@ def test_image_list_name_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.images.assert_called_with( name='abc', # marker=self._image.id @@ -978,7 +970,7 @@ def test_image_list_status_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.images.assert_called_with(status='active') def test_image_list_hidden_option(self): @@ -990,7 +982,7 @@ def test_image_list_hidden_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.images.assert_called_with(is_hidden=True) def test_image_list_tag_option(self): @@ -1000,12 +992,12 @@ def test_image_list_tag_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + self.cmd.take_action(parsed_args) self.image_client.images.assert_called_with(tag=['abc', 'cba']) -class TestListImageProjects(TestImage): - project = identity_fakes.FakeProject.create_one_project() +class TestListImageProjects(image_fakes.TestImagev2): + project = sdk_fakes.generate_fake_resource(_project.Project) _image = image_fakes.create_one_image() member = image_fakes.create_one_image_member( attrs={'image_id': _image.id, 'member_id': project.id} @@ -1042,9 +1034,9 @@ def test_image_member_list(self): self.assertEqual(self.datalist, list(data)) -class TestRemoveProjectImage(TestImage): - project = identity_fakes.FakeProject.create_one_project() - domain = identity_fakes.FakeDomain.create_one_domain() +class TestRemoveProjectImage(image_fakes.TestImagev2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): super().setUp() @@ -1053,8 +1045,7 @@ def setUp(self): # This is the return value for utils.find_resource() self.image_client.find_image.return_value = self._image - self.project_mock.get.return_value = self.project - self.domain_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project self.image_client.remove_member.return_value = None # Get the command object to test self.cmd = _image.RemoveProjectImage(self.app, None) @@ -1105,7 +1096,7 @@ def test_remove_project_image_with_options(self): self.assertIsNone(result) -class TestShowProjectImage(TestImage): +class TestShowProjectImage(image_fakes.TestImagev2): _image = image_fakes.create_one_image() new_member = image_fakes.create_one_image_member( attrs={'image_id': _image.id, 'member_id': 'member1'} @@ -1165,17 +1156,15 @@ def test_show_project_image(self): self.assertEqual(self.datalist, data) -class TestImageSet(TestImage): - project = identity_fakes.FakeProject.create_one_project() - domain = identity_fakes.FakeDomain.create_one_domain() +class TestImageSet(image_fakes.TestImagev2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) _image = image_fakes.create_one_image({'tags': []}) def setUp(self): super().setUp() - self.project_mock.get.return_value = self.project - - self.domain_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project self.image_client.find_image.return_value = self._image @@ -1291,6 +1280,117 @@ def test_image_set_membership_option_pending(self): # the 'update membership' route. self.image_client.update_image.assert_called_with(self._image.id) + def test_image_set_membership_accept_with_project_no_owner_change(self): + """Test that --project with --accept doesn't change image owner.""" + membership = image_fakes.create_one_image_member( + attrs={ + 'image_id': '0f41529e-7c12-4de8-be2d-181abb825b3c', + 'member_id': self.project.id, + } + ) + self.image_client.update_member.return_value = membership + + arglist = [ + '--project', + self.project.name, + '--accept', + self._image.id, + ] + verifylist = [ + ('project', self.project.name), + ('membership', 'accepted'), + ('image', self._image.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.image_client.update_member.assert_called_once_with( + image=self._image.id, + member=self.project.id, + status='accepted', + ) + + self.image_client.update_image.assert_called() + call_args = self.image_client.update_image.call_args + if call_args: + _args, kwargs = call_args + self.assertNotIn('owner_id', kwargs) + + def test_image_set_membership_reject_with_project_no_owner_change(self): + """Test that --project with --reject doesn't change image owner.""" + membership = image_fakes.create_one_image_member( + attrs={ + 'image_id': '0f41529e-7c12-4de8-be2d-181abb825b3c', + 'member_id': self.project.id, + } + ) + self.image_client.update_member.return_value = membership + + arglist = [ + '--project', + self.project.name, + '--reject', + self._image.id, + ] + verifylist = [ + ('project', self.project.name), + ('membership', 'rejected'), + ('image', self._image.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.image_client.update_member.assert_called_once_with( + image=self._image.id, + member=self.project.id, + status='rejected', + ) + + self.image_client.update_image.assert_called() + call_args = self.image_client.update_image.call_args + if call_args: + _args, kwargs = call_args + self.assertNotIn('owner_id', kwargs) + + def test_image_set_membership_pending_with_project_no_owner_change(self): + """Test that --project with --pending doesn't change image owner.""" + membership = image_fakes.create_one_image_member( + attrs={ + 'image_id': '0f41529e-7c12-4de8-be2d-181abb825b3c', + 'member_id': self.project.id, + } + ) + self.image_client.update_member.return_value = membership + + arglist = [ + '--project', + self.project.name, + '--pending', + self._image.id, + ] + verifylist = [ + ('project', self.project.name), + ('membership', 'pending'), + ('image', self._image.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + self.image_client.update_member.assert_called_once_with( + image=self._image.id, + member=self.project.id, + status='pending', + ) + + self.image_client.update_image.assert_called() + call_args = self.image_client.update_image.call_args + if call_args: + _args, kwargs = call_args + self.assertNotIn('owner_id', kwargs) + def test_image_set_options(self): arglist = [ '--name', @@ -1338,8 +1438,9 @@ def test_image_set_options(self): self.assertIsNone(result) def test_image_set_with_unexist_project(self): - self.project_mock.get.side_effect = exceptions.NotFound(None) - self.project_mock.find.side_effect = exceptions.NotFound(None) + self.identity_sdk_client.find_project.side_effect = ( + sdk_exceptions.ResourceNotFound() + ) arglist = [ '--project', @@ -1356,15 +1457,17 @@ def test_image_set_with_unexist_project(self): exceptions.CommandError, self.cmd.take_action, parsed_args ) - def test_image_set_bools1(self): + def test_image_set_bools_true(self): arglist = [ '--protected', '--private', + '--hidden', 'graven', ] verifylist = [ ('is_protected', True), ('visibility', 'private'), + ('is_hidden', True), ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1374,6 +1477,7 @@ def test_image_set_bools1(self): kwargs = { 'is_protected': True, 'visibility': 'private', + 'is_hidden': True, } # ImageManager.update(image, **kwargs) self.image_client.update_image.assert_called_with( @@ -1381,15 +1485,17 @@ def test_image_set_bools1(self): ) self.assertIsNone(result) - def test_image_set_bools2(self): + def test_image_set_bools_false(self): arglist = [ '--unprotected', '--public', + '--unhidden', 'graven', ] verifylist = [ ('is_protected', False), ('visibility', 'public'), + ('is_hidden', False), ('image', 'graven'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1399,6 +1505,7 @@ def test_image_set_bools2(self): kwargs = { 'is_protected': False, 'visibility': 'public', + 'is_hidden': False, } # ImageManager.update(image, **kwargs) self.image_client.update_image.assert_called_with( @@ -1702,7 +1809,7 @@ def test_image_set_unhidden(self): self.assertIsNone(result) -class TestImageShow(TestImage): +class TestImageShow(image_fakes.TestImagev2): new_image = image_fakes.create_one_image(attrs={'size': 1000}) _data = image_fakes.create_one_image() @@ -1721,7 +1828,7 @@ class TestImageShow(TestImage): def setUp(self): super().setUp() - self.image_client.find_image = mock.Mock(return_value=self._data) + self.image_client.find_image.return_value = self._data # Get the command object to test self.cmd = _image.ShowImage(self.app, None) @@ -1770,7 +1877,7 @@ def test_image_show_human_readable(self): self.assertEqual(data[size_index], '1K') -class TestImageUnset(TestImage): +class TestImageUnset(image_fakes.TestImagev2): def setUp(self): super().setUp() @@ -1779,6 +1886,7 @@ def setUp(self): attrs['hw_rng_model'] = 'virtio' attrs['prop'] = 'test' attrs['prop2'] = 'fake' + attrs['os_secure_boot'] = 'required' self.image = image_fakes.create_one_image(attrs) self.image_client.find_image.return_value = self.image @@ -1822,11 +1930,18 @@ def test_image_unset_property_option(self): 'hw_rng_model', '--property', 'prop', + '--property', + 'os_secure_boot', self.image.id, ] + # openstacksdk translates 'os_secure_boot' property to + # 'needs_secure_boot' Image attribute. This is true for + # all IMAGE_ATTRIBUTES_CUSTOM_NAMES keys + self.assertEqual(self.image.needs_secure_boot, 'required') + self.assertFalse(hasattr(self.image, 'os_secure_boot')) verifylist = [ - ('properties', ['hw_rng_model', 'prop']), + ('properties', ['hw_rng_model', 'prop', 'os_secure_boot']), ('image', self.image.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1865,7 +1980,7 @@ def test_image_unset_mixed_option(self): self.assertIsNone(result) -class TestImageStage(TestImage): +class TestImageStage(image_fakes.TestImagev2): image = image_fakes.create_one_image({}) def setUp(self): @@ -1919,7 +2034,7 @@ def test_stage_image__from_stdin(self, mock_get_data_from_stdin): ) -class TestImageImport(TestImage): +class TestImageImport(image_fakes.TestImagev2): image = image_fakes.create_one_image( { 'container_format': 'bare', @@ -1956,7 +2071,7 @@ def test_import_image__glance_direct(self): remote_image_id=None, remote_service_interface=None, stores=None, - all_stores=None, + all_stores=False, all_stores_must_succeed=False, ) @@ -1986,7 +2101,7 @@ def test_import_image__web_download(self): remote_image_id=None, remote_service_interface=None, stores=None, - all_stores=None, + all_stores=False, all_stores_must_succeed=False, ) @@ -2039,6 +2154,36 @@ def test_import_image__web_download_invalid_options(self): self.image_client.import_image.assert_not_called() + def test_import_image__web_download_invalid_url(self): + arglist = [ + self.image.name, + '--method', + 'web-download', + '--uri', + 'invalid:1234', + ] + + verifylist = [ + ('image', self.image.name), + ('import_method', 'web-download'), + ('uri', 'invalid:1234'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + self.assertIn( + "'invalid:1234' is not a valid url", + str(exc), + ) + + self.image_client.import_image.assert_not_called() + def test_import_image__web_download_invalid_image_state(self): self.image.status = 'uploading' # != 'queued' arglist = [ @@ -2094,7 +2239,7 @@ def test_import_image__copy_image(self): remote_image_id=None, remote_service_interface=None, stores=['fast'], - all_stores=None, + all_stores=False, all_stores_must_succeed=False, ) @@ -2126,7 +2271,7 @@ def test_import_image__copy_image_disallow_failure(self): remote_image_id=None, remote_service_interface=None, stores=['fast'], - all_stores=None, + all_stores=False, all_stores_must_succeed=True, ) @@ -2161,12 +2306,12 @@ def test_import_image__glance_download(self): remote_image_id='remote-image-id', remote_service_interface='private', stores=None, - all_stores=None, + all_stores=False, all_stores_must_succeed=False, ) -class TestImageSave(TestImage): +class TestImageSave(image_fakes.TestImagev2): image = image_fakes.create_one_image({}) def setUp(self): @@ -2190,11 +2335,33 @@ def test_save_data(self): self.cmd.take_action(parsed_args) self.image_client.download_image.assert_called_once_with( - self.image.id, stream=True, output='/path/to/file' + self.image.id, output='/path/to/file', stream=True, chunk_size=1024 + ) + + def test_save_data_with_chunk_size(self): + arglist = [ + '--file', + '/path/to/file', + '--chunk-size', + '2048', + self.image.id, + ] + + verifylist = [ + ('filename', '/path/to/file'), + ('chunk_size', 2048), + ('image', self.image.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.image_client.download_image.assert_called_once_with( + self.image.id, output='/path/to/file', stream=True, chunk_size=2048 ) -class TestImageGetData(TestImage): +class TestImageGetData(image_fakes.TestImagev2): def test_get_data_from_stdin(self): fd = io.BytesIO(b"some initial binary data: \x00\x01") @@ -2220,7 +2387,7 @@ def test_get_data_from_stdin__interactive(self): self.assertIsNone(test_fd) -class TestStoresInfo(TestImage): +class TestStoresInfo(image_fakes.TestImagev2): stores_info = image_fakes.create_one_stores_info() def setUp(self): diff --git a/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py b/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py index 68b3076d55..7363fd8de6 100644 --- a/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py +++ b/openstackclient/tests/unit/image/v2/test_metadef_namespaces.py @@ -12,6 +12,8 @@ # License for the specific language governing permissions and limitations # under the License. +from unittest import mock + from openstackclient.image.v2 import metadef_namespaces from openstackclient.tests.unit.image.v2 import fakes as image_fakes @@ -24,6 +26,7 @@ class TestMetadefNamespaceCreate(image_fakes.TestImagev2): 'display_name', 'namespace', 'owner', + 'tags', 'visibility', ) expected_data = ( @@ -31,6 +34,7 @@ class TestMetadefNamespaceCreate(image_fakes.TestImagev2): _metadef_namespace.display_name, _metadef_namespace.namespace, _metadef_namespace.owner, + _metadef_namespace.tags, _metadef_namespace.visibility, ) @@ -114,21 +118,25 @@ def test_namespace_list_no_options(self): class TestMetadefNamespaceSet(image_fakes.TestImagev2): - _metadef_namespace = image_fakes.create_one_metadef_namespace() - def setUp(self): super().setUp() + self.metadef_namespace = image_fakes.create_one_metadef_namespace() + + self.image_client.get_metadef_namespace.return_value = ( + self.metadef_namespace + ) self.image_client.update_metadef_namespace.return_value = ( - self._metadef_namespace + self.metadef_namespace ) + self.image_client.add_tag_to_metadef_namespace.return_value = None + self.cmd = metadef_namespaces.SetMetadefNamespace(self.app, None) - self.datalist = self._metadef_namespace def test_namespace_set_no_options(self): - arglist = [self._metadef_namespace.namespace] + arglist = [self.metadef_namespace.namespace] verifylist = [ - ('namespace', self._metadef_namespace.namespace), + ('namespace', self.metadef_namespace.namespace), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -137,6 +145,95 @@ def test_namespace_set_no_options(self): self.assertIsNone(result) + def test_namespace_set_tag(self): + arglist = [ + self.metadef_namespace.namespace, + '--tag', + 't1', + '--tag', + 't2', + ] + verifylist = [ + ('namespace', self.metadef_namespace.namespace), + ('tags', ['t1', 't2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.image_client.add_tag_to_metadef_namespace.assert_has_calls( + [ + mock.call(self.metadef_namespace.namespace, 't1'), + mock.call(self.metadef_namespace.namespace, 't2'), + ] + ) + + +class TestMetadefNamespaceUnset(image_fakes.TestImagev2): + def setUp(self): + super().setUp() + + self.metadef_namespace = image_fakes.create_one_metadef_namespace( + attrs={'tags': [{'name': 't1'}]} + ) + + self.image_client.get_metadef_namespace.return_value = ( + self.metadef_namespace + ) + self.image_client.update_metadef_namespace.return_value = ( + self.metadef_namespace + ) + self.image_client.remove_tag_from_metadef_namespace.return_value = None + self.image_client.remove_tags_from_metadef_namespace.return_value = ( + None + ) + + self.cmd = metadef_namespaces.UnsetMetadefNamespace(self.app, None) + + def test_namespace_unset_tag(self): + arglist = [ + self.metadef_namespace.namespace, + '--tag', + 't1', + '--tag', + 't2', + ] + verifylist = [ + ('namespace', self.metadef_namespace.namespace), + ('tags', ['t1', 't2']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.image_client.remove_tag_from_metadef_namespace.assert_has_calls( + [ + mock.call(self.metadef_namespace, 't1'), + mock.call(self.metadef_namespace, 't2'), + ] + ) + self.image_client.remove_tags_from_metadef_namespace.assert_not_called() + + def test_namespace_unset_all_tag(self): + arglist = [ + self.metadef_namespace.namespace, + '--all-tags', + ] + verifylist = [ + ('namespace', self.metadef_namespace.namespace), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.image_client.remove_tags_from_metadef_namespace.assert_called_once_with( + self.metadef_namespace + ) + self.image_client.remove_tag_from_metadef_namespace.assert_not_called() + class TestMetadefNamespaceShow(image_fakes.TestImagev2): _metadef_namespace = image_fakes.create_one_metadef_namespace() @@ -146,6 +243,7 @@ class TestMetadefNamespaceShow(image_fakes.TestImagev2): 'display_name', 'namespace', 'owner', + 'tags', 'visibility', ) expected_data = ( @@ -153,6 +251,7 @@ class TestMetadefNamespaceShow(image_fakes.TestImagev2): _metadef_namespace.display_name, _metadef_namespace.namespace, _metadef_namespace.owner, + _metadef_namespace.tags, _metadef_namespace.visibility, ) diff --git a/openstackclient/tests/unit/image/v2/test_metadef_objects.py b/openstackclient/tests/unit/image/v2/test_metadef_objects.py index 4996060d9d..6306e23eb1 100644 --- a/openstackclient/tests/unit/image/v2/test_metadef_objects.py +++ b/openstackclient/tests/unit/image/v2/test_metadef_objects.py @@ -113,6 +113,7 @@ def setUp(self): super().setUp() self.image_client.delete_metadef_object.return_value = None + self.image_client.delete_all_metadef_objects.return_value = None self.cmd = metadef_objects.DeleteMetadefObject(self.app, None) def test_object_delete(self): @@ -126,8 +127,29 @@ def test_object_delete(self): result = self.cmd.take_action(parsed_args) + self.image_client.delete_metadef_object.assert_called_once_with( + self.image_client.get_metadef_object(), + self._metadef_namespace.namespace, + ) + self.image_client.delete_all_metadef_objects.assert_not_called() self.assertIsNone(result) + def test_object_delete_all(self): + arglist = [ + self._metadef_namespace.namespace, + ] + + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.image_client.delete_all_metadef_objects.assert_called_with( + self._metadef_namespace.namespace, + ) + self.image_client.delete_metadef_object.assert_not_called() + class TestMetadefObjectList(fakes.TestImagev2): _metadef_namespace = fakes.create_one_metadef_namespace() diff --git a/openstackclient/tests/unit/image/v2/test_metadef_properties.py b/openstackclient/tests/unit/image/v2/test_metadef_properties.py index 274ed66353..e2f83d6296 100644 --- a/openstackclient/tests/unit/image/v2/test_metadef_properties.py +++ b/openstackclient/tests/unit/image/v2/test_metadef_properties.py @@ -91,6 +91,7 @@ def setUp(self): super().setUp() self.cmd = metadef_properties.DeleteMetadefProperty(self.app, None) + self.image_client.delete_all_metadef_properties.return_value = None def test_metadef_property_delete(self): arglist = ['namespace', 'property'] @@ -100,6 +101,10 @@ def test_metadef_property_delete(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) + self.image_client.delete_metadef_property.assert_called_with( + 'property', 'namespace', ignore_missing=False + ) + self.image_client.delete_all_metadef_properties.assert_not_called() def test_metadef_property_delete_missing_arguments(self): arglist = [] @@ -110,21 +115,13 @@ def test_metadef_property_delete_missing_arguments(self): arglist, [], ) - - arglist = ['namespace'] - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - [], - ) + self.image_client.delete_all_metadef_properties.assert_not_called() + self.image_client.delete_metadef_property.assert_not_called() def test_metadef_property_delete_exception(self): arglist = ['namespace', 'property'] verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.image_client.delete_metadef_property.side_effect = ( sdk_exceptions.ResourceNotFound ) @@ -132,6 +129,23 @@ def test_metadef_property_delete_exception(self): self.assertRaises( exceptions.CommandError, self.cmd.take_action, parsed_args ) + self.image_client.delete_metadef_property.assert_called_with( + 'property', 'namespace', ignore_missing=False + ) + self.image_client.delete_all_metadef_properties.assert_not_called() + + def test_metadef_property_delete_all(self): + arglist = ['namespace'] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.image_client.delete_all_metadef_properties.assert_called_with( + 'namespace' + ) + self.image_client.delete_metadef_property.assert_not_called() class TestMetadefPropertyList(image_fakes.TestImagev2): diff --git a/openstackclient/tests/unit/network/test_common.py b/openstackclient/tests/unit/network/test_common.py index c1ad9b28d2..f410067d05 100644 --- a/openstackclient/tests/unit/network/test_common.py +++ b/openstackclient/tests/unit/network/test_common.py @@ -13,91 +13,11 @@ from unittest import mock -import openstack -from osc_lib import exceptions from openstackclient.network import common from openstackclient.tests.unit import utils -def _add_common_argument(parser): - parser.add_argument( - 'common', - metavar='', - help='Common argument', - ) - return parser - - -def _add_network_argument(parser): - parser.add_argument( - 'network', - metavar='', - help='Network argument', - ) - return parser - - -def _add_compute_argument(parser): - parser.add_argument( - 'compute', - metavar='', - help='Compute argument', - ) - return parser - - -class FakeNetworkAndComputeCommand(common.NetworkAndComputeCommand): - def update_parser_common(self, parser): - return _add_common_argument(parser) - - def update_parser_network(self, parser): - return _add_network_argument(parser) - - def update_parser_compute(self, parser): - return _add_compute_argument(parser) - - def take_action_network(self, client, parsed_args): - return client.network_action(parsed_args) - - def take_action_compute(self, client, parsed_args): - return client.compute_action(parsed_args) - - -class FakeNetworkAndComputeLister(common.NetworkAndComputeLister): - def update_parser_common(self, parser): - return _add_common_argument(parser) - - def update_parser_network(self, parser): - return _add_network_argument(parser) - - def update_parser_compute(self, parser): - return _add_compute_argument(parser) - - def take_action_network(self, client, parsed_args): - return client.network_action(parsed_args) - - def take_action_compute(self, client, parsed_args): - return client.compute_action(parsed_args) - - -class FakeNetworkAndComputeShowOne(common.NetworkAndComputeShowOne): - def update_parser_common(self, parser): - return _add_common_argument(parser) - - def update_parser_network(self, parser): - return _add_network_argument(parser) - - def update_parser_compute(self, parser): - return _add_compute_argument(parser) - - def take_action_network(self, client, parsed_args): - return client.network_action(parsed_args) - - def take_action_compute(self, client, parsed_args): - return client.compute_action(parsed_args) - - class FakeCreateNeutronCommandWithExtraArgs( common.NeutronCommandWithExtraArgs ): @@ -119,85 +39,6 @@ def take_action(self, parsed_args): client.test_create_action(**attrs) -class TestNetworkAndCompute(utils.TestCommand): - def setUp(self): - super().setUp() - - # Create client mocks. Note that we intentionally do not use specced - # mocks since we want to test fake methods. - - self.app.client_manager.network = mock.Mock() - self.network_client = self.app.client_manager.network - self.network_client.network_action = mock.Mock( - return_value='take_action_network' - ) - - self.app.client_manager.compute = mock.Mock() - self.compute_client = self.app.client_manager.compute - self.compute_client.compute_action = mock.Mock( - return_value='take_action_compute' - ) - - self.cmd = FakeNetworkAndComputeCommand(self.app, None) - - def test_take_action_network(self): - arglist = ['common', 'network'] - verifylist = [('common', 'common'), ('network', 'network')] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.network_client.network_action.assert_called_with(parsed_args) - self.assertEqual('take_action_network', result) - - def test_take_action_compute(self): - arglist = ['common', 'compute'] - verifylist = [('common', 'common'), ('compute', 'compute')] - - self.app.client_manager.network_endpoint_enabled = False - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.compute_client.compute_action.assert_called_with(parsed_args) - self.assertEqual('take_action_compute', result) - - -class TestNetworkAndComputeCommand(TestNetworkAndCompute): - def setUp(self): - super().setUp() - self.cmd = FakeNetworkAndComputeCommand(self.app, None) - - -class TestNetworkAndComputeLister(TestNetworkAndCompute): - def setUp(self): - super().setUp() - self.cmd = FakeNetworkAndComputeLister(self.app, None) - - -class TestNetworkAndComputeShowOne(TestNetworkAndCompute): - def setUp(self): - super().setUp() - self.cmd = FakeNetworkAndComputeShowOne(self.app, None) - - def test_take_action_with_http_exception(self): - with mock.patch.object(self.cmd, 'take_action_network') as m_action: - m_action.side_effect = openstack.exceptions.HttpException("bar") - self.assertRaisesRegex( - exceptions.CommandError, - "bar", - self.cmd.take_action, - mock.Mock(), - ) - - self.app.client_manager.network_endpoint_enabled = False - with mock.patch.object(self.cmd, 'take_action_compute') as m_action: - m_action.side_effect = openstack.exceptions.HttpException("bar") - self.assertRaisesRegex( - exceptions.CommandError, - "bar", - self.cmd.take_action, - mock.Mock(), - ) - - class TestNeutronCommandWithExtraArgs(utils.TestCommand): def setUp(self): super().setUp() @@ -205,9 +46,9 @@ def setUp(self): # Create client mocks. Note that we intentionally do not use specced # mocks since we want to test fake methods. - self.app.client_manager.network = mock.Mock() - self.network_client = self.app.client_manager.network - self.network_client.test_create_action = mock.Mock() + self.app.client_manager.network = mock.Mock() # noqa: O401 + self.network_client = self.app.client_manager.network # noqa: O401 + self.network_client.test_create_action = mock.Mock() # noqa: O402 # Subclasses can override the command object to test. self.cmd = FakeCreateNeutronCommandWithExtraArgs(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/bgpvpn/__init__.py b/openstackclient/tests/unit/network/v2/bgpvpn/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/tests/unit/network/v2/bgpvpn/fakes.py b/openstackclient/tests/unit/network/v2/bgpvpn/fakes.py new file mode 100644 index 0000000000..42462d37ab --- /dev/null +++ b/openstackclient/tests/unit/network/v2/bgpvpn/fakes.py @@ -0,0 +1,179 @@ +# Copyright (c) 2016 Juniper Networks Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + + +from openstack.network.v2 import bgpvpn as _bgpvpn +from openstack.network.v2 import bgpvpn_network_association as _net_assoc +from openstack.network.v2 import bgpvpn_port_association as _port_assoc +from openstack.network.v2 import bgpvpn_router_association as _router_assoc +from openstack import resource as sdk_resource + +from openstackclient.tests.unit.network.v2 import fakes as test_fakes + + +_FAKE_PROJECT_ID = 'fake_project_id' + + +class TestNeutronClientBgpvpn(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + +def create_one_bgpvpn(attrs=None): + """Create a fake BGP VPN.""" + + attrs = attrs or {} + + # Set default attributes. + bgpvpn_attrs = { + 'id': 'fake_bgpvpn_id', + 'project_id': _FAKE_PROJECT_ID, + 'name': '', + 'type': 'l3', + 'route_targets': [], + 'import_targets': [], + 'export_targets': [], + 'route_distinguishers': [], + 'networks': [], + 'routers': [], + 'ports': [], + 'vni': 100, + 'local_pref': 777, + } + + # Overwrite default attributes. + bgpvpn_attrs.update(attrs) + return _bgpvpn.BgpVpn(**bgpvpn_attrs) + + +def create_bgpvpns(attrs=None, count=1): + """Create multiple fake BGP VPN.""" + + bgpvpns = [] + for i in range(0, count): + if attrs is None: + attrs = {'id': 'fake_id{i}'} + elif getattr(attrs, 'id', None) is None: + attrs['id'] = 'fake_id{i}' + bgpvpns.append(create_one_bgpvpn(attrs)) + + return bgpvpns + + +def create_one_resource(attrs=None): + """Create a fake resource.""" + attrs = attrs or {} + + res_attrs = { + 'id': 'fake_resource_id', + } + + res_attrs.update(attrs) + return sdk_resource.Resource(**res_attrs) + + +def create_resources(attrs=None, count=1): + """Create multiple fake resources.""" + + resources = [] + for i in range(0, count): + if attrs is None: + attrs = {'id': 'fake_id{i}'} + elif getattr(attrs, 'id', None) is None: + attrs['id'] = 'fake_id{i}' + resources.append(create_one_resource(attrs)) + + return resources + + +def create_one_network_association(attrs=None): + """Create a fake network association.""" + attrs = attrs or {} + assoc_attrs = { + 'id': 'fake_association_id', + 'network_id': 'fake_resource_id', + 'project_id': _FAKE_PROJECT_ID, + } + assoc_attrs.update(attrs) + return _net_assoc.BgpVpnNetworkAssociation(**assoc_attrs) + + +def create_network_associations(count=1): + """Create multiple fake network associations.""" + assocs = [] + for idx in range(count): + assocs.append( + { + 'id': f'fake_association_id{idx}', + 'network_id': f'fake_resource_id{idx}', + 'project_id': _FAKE_PROJECT_ID, + } + ) + return assocs + + +def create_one_router_association(attrs=None): + """Create a fake router association.""" + attrs = attrs or {} + assoc_attrs = { + 'id': 'fake_association_id', + 'router_id': 'fake_resource_id', + 'project_id': _FAKE_PROJECT_ID, + } + assoc_attrs.update(attrs) + return _router_assoc.BgpVpnRouterAssociation(**assoc_attrs) + + +def create_router_associations(count=1): + """Create multiple fake router associations.""" + assocs = [] + for idx in range(count): + assocs.append( + { + 'id': f'fake_association_id{idx}', + 'router_id': f'fake_resource_id{idx}', + 'project_id': _FAKE_PROJECT_ID, + } + ) + return assocs + + +def create_one_port_association(attrs=None): + """Create a fake port association.""" + attrs = attrs or {} + assoc_attrs = { + 'id': 'fake_association_id', + 'port_id': 'fake_resource_id', + 'project_id': _FAKE_PROJECT_ID, + 'routes': [], + } + assoc_attrs.update(attrs) + return _port_assoc.BgpVpnPortAssociation(**assoc_attrs) + + +def create_port_associations(count=1): + """Create multiple fake port associations.""" + assocs = [] + for idx in range(count): + assocs.append( + { + 'id': f'fake_association_id{idx}', + 'port_id': f'fake_resource_id{idx}', + 'project_id': _FAKE_PROJECT_ID, + 'routes': [], + } + ) + return assocs diff --git a/openstackclient/tests/unit/network/v2/bgpvpn/test_bgpvpn.py b/openstackclient/tests/unit/network/v2/bgpvpn/test_bgpvpn.py new file mode 100644 index 0000000000..ef2a6f41d0 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/bgpvpn/test_bgpvpn.py @@ -0,0 +1,584 @@ +# Copyright (c) 2016 Juniper Networks Inc. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy +import operator +from unittest import mock + +from osc_lib import exceptions +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient.network.v2.bgpvpn import bgpvpn +from openstackclient.tests.unit.network.v2.bgpvpn import fakes + + +columns_short = tuple( + col + for col, _, listing_mode in bgpvpn._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_SHORT_ONLY) +) +columns_long = tuple( + col + for col, _, listing_mode in bgpvpn._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +headers_short = tuple( + head + for _, head, listing_mode in bgpvpn._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_SHORT_ONLY) +) +headers_long = tuple( + head + for _, head, listing_mode in bgpvpn._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +sorted_attr_map = sorted(bgpvpn._attr_map, key=operator.itemgetter(1)) +sorted_columns = tuple(col for col, _, _ in sorted_attr_map) +sorted_headers = tuple(head for _, head, _ in sorted_attr_map) + + +def _get_data(attrs, columns=sorted_columns): + return osc_utils.get_dict_properties( + attrs, columns, formatters=bgpvpn._formatters + ) + + +class TestCreateBgpvpn(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.cmd = bgpvpn.CreateBgpvpn(self.app, None) + + def test_create_bgpvpn_with_no_args(self): + fake_bgpvpn = fakes.create_one_bgpvpn() + self.network_client.create_bgpvpn.return_value = fake_bgpvpn + arglist = [] + verifylist = [ + ('project', None), + ('name', None), + ('type', 'l3'), + ('vni', None), + ('local_pref', None), + ('route_targets', None), + ('import_targets', None), + ('export_targets', None), + ('route_distinguishers', None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + cols, _data = self.cmd.take_action(parsed_args) + + self.network_client.create_bgpvpn.assert_called_once_with( + **{'type': 'l3'} + ) + + self.assertEqual(sorted(sorted_columns), sorted(cols)) + + @mock.patch('osc_lib.cli.identity.find_project') + def test_create_bgpvpn_with_all_args(self, mock_find_project): + attrs = { + 'project_id': 'new_fake_project_id', + 'name': 'fake_name', + 'type': 'l2', + 'vni': 100, + 'local_pref': 777, + 'route_targets': ['fake_rt1', 'fake_rt2', 'fake_rt3'], + 'import_targets': ['fake_irt1', 'fake_irt2', 'fake_irt3'], + 'export_targets': ['fake_ert1', 'fake_ert2', 'fake_ert3'], + 'route_distinguishers': ['fake_rd1', 'fake_rd2', 'fake_rd3'], + } + fake_bgpvpn = fakes.create_one_bgpvpn(attrs) + self.network_client.create_bgpvpn.return_value = fake_bgpvpn + mock_find_project.return_value = mock.Mock( + id=fake_bgpvpn['project_id'] + ) + arglist = [ + '--project', + fake_bgpvpn['project_id'], + '--name', + fake_bgpvpn['name'], + '--type', + fake_bgpvpn['type'], + '--vni', + str(fake_bgpvpn['vni']), + '--local-pref', + str(fake_bgpvpn['local_pref']), + ] + for rt in fake_bgpvpn['route_targets']: + arglist.extend(['--route-target', rt]) + for rt in fake_bgpvpn['import_targets']: + arglist.extend(['--import-target', rt]) + for rt in fake_bgpvpn['export_targets']: + arglist.extend(['--export-target', rt]) + for rd in fake_bgpvpn['route_distinguishers']: + arglist.extend(['--route-distinguisher', rd]) + verifylist = [ + ('project', fake_bgpvpn['project_id']), + ('name', fake_bgpvpn['name']), + ('type', fake_bgpvpn['type']), + ('vni', fake_bgpvpn['vni']), + ('local_pref', fake_bgpvpn['local_pref']), + ('route_targets', fake_bgpvpn['route_targets']), + ('import_targets', fake_bgpvpn['import_targets']), + ('export_targets', fake_bgpvpn['export_targets']), + ('route_distinguishers', fake_bgpvpn['route_distinguishers']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + cols, _data = self.cmd.take_action(parsed_args) + + fake_bgpvpn_call = copy.deepcopy(attrs) + + self.network_client.create_bgpvpn.assert_called_once_with( + **fake_bgpvpn_call + ) + self.assertEqual(sorted(sorted_columns), sorted(cols)) + + +class TestSetBgpvpn(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + + self.cmd = bgpvpn.SetBgpvpn(self.app, None) + + def test_set_bgpvpn(self): + attrs = { + 'route_targets': ['set_rt1', 'set_rt2', 'set_rt3'], + 'import_targets': ['set_irt1', 'set_irt2', 'set_irt3'], + 'export_targets': ['set_ert1', 'set_ert2', 'set_ert3'], + 'route_distinguishers': ['set_rd1', 'set_rd2', 'set_rd3'], + } + fake_bgpvpn = fakes.create_one_bgpvpn(attrs) + self.network_client.get_bgpvpn.return_value = fake_bgpvpn + arglist = [ + fake_bgpvpn['id'], + '--name', + 'set_name', + '--route-target', + 'set_rt1', + '--import-target', + 'set_irt1', + '--export-target', + 'set_ert1', + '--route-distinguisher', + 'set_rd1', + ] + verifylist = [ + ('bgpvpn', fake_bgpvpn['id']), + ('name', 'set_name'), + ('route_targets', ['set_rt1']), + ('purge_route_target', False), + ('import_targets', ['set_irt1']), + ('purge_import_target', False), + ('export_targets', ['set_ert1']), + ('purge_export_target', False), + ('route_distinguishers', ['set_rd1']), + ('purge_route_distinguisher', False), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + attrs = { + 'name': 'set_name', + 'route_targets': list( + set(fake_bgpvpn['route_targets']) | set(['set_rt1']) + ), + 'import_targets': list( + set(fake_bgpvpn['import_targets']) | set(['set_irt1']) + ), + 'export_targets': list( + set(fake_bgpvpn['export_targets']) | set(['set_ert1']) + ), + 'route_distinguishers': list( + set(fake_bgpvpn['route_distinguishers']) | set(['set_rd1']) + ), + } + self.network_client.update_bgpvpn.assert_called_once_with( + fake_bgpvpn['id'], **attrs + ) + self.assertIsNone(result) + + def test_set_bgpvpn_with_purge_list(self): + fake_bgpvpn = fakes.create_one_bgpvpn() + self.network_client.get_bgpvpn.return_value = fake_bgpvpn + arglist = [ + fake_bgpvpn['id'], + '--route-target', + 'set_rt1', + '--no-route-target', + '--import-target', + 'set_irt1', + '--no-import-target', + '--export-target', + 'set_ert1', + '--no-export-target', + '--route-distinguisher', + 'set_rd1', + '--no-route-distinguisher', + ] + verifylist = [ + ('bgpvpn', fake_bgpvpn['id']), + ('route_targets', ['set_rt1']), + ('purge_route_target', True), + ('import_targets', ['set_irt1']), + ('purge_import_target', True), + ('export_targets', ['set_ert1']), + ('purge_export_target', True), + ('route_distinguishers', ['set_rd1']), + ('purge_route_distinguisher', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + attrs = { + 'route_targets': [], + 'import_targets': [], + 'export_targets': [], + 'route_distinguishers': [], + } + self.network_client.update_bgpvpn.assert_called_once_with( + fake_bgpvpn['id'], **attrs + ) + self.assertIsNone(result) + + +class TestUnsetBgpvpn(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.cmd = bgpvpn.UnsetBgpvpn(self.app, None) + + def test_unset_bgpvpn(self): + attrs = { + 'route_targets': ['unset_rt1', 'unset_rt2', 'unset_rt3'], + 'import_targets': ['unset_irt1', 'unset_irt2', 'unset_irt3'], + 'export_targets': ['unset_ert1', 'unset_ert2', 'unset_ert3'], + 'route_distinguishers': ['unset_rd1', 'unset_rd2', 'unset_rd3'], + } + fake_bgpvpn = fakes.create_one_bgpvpn(attrs) + self.network_client.get_bgpvpn.return_value = fake_bgpvpn + arglist = [ + fake_bgpvpn['id'], + '--route-target', + 'unset_rt1', + '--import-target', + 'unset_irt1', + '--export-target', + 'unset_ert1', + '--route-distinguisher', + 'unset_rd1', + ] + verifylist = [ + ('bgpvpn', fake_bgpvpn['id']), + ('route_targets', ['unset_rt1']), + ('purge_route_target', False), + ('import_targets', ['unset_irt1']), + ('purge_import_target', False), + ('export_targets', ['unset_ert1']), + ('purge_export_target', False), + ('route_distinguishers', ['unset_rd1']), + ('purge_route_distinguisher', False), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + attrs = { + 'route_targets': list( + set(fake_bgpvpn['route_targets']) - set(['unset_rt1']) + ), + 'import_targets': list( + set(fake_bgpvpn['import_targets']) - set(['unset_irt1']) + ), + 'export_targets': list( + set(fake_bgpvpn['export_targets']) - set(['unset_ert1']) + ), + 'route_distinguishers': list( + set(fake_bgpvpn['route_distinguishers']) - set(['unset_rd1']) + ), + } + self.network_client.update_bgpvpn.assert_called_once_with( + fake_bgpvpn['id'], **attrs + ) + self.assertIsNone(result) + + def test_unset_bgpvpn_with_purge_list(self): + fake_bgpvpn = fakes.create_one_bgpvpn() + self.network_client.get_bgpvpn.return_value = fake_bgpvpn + arglist = [ + fake_bgpvpn['id'], + '--route-target', + 'unset_rt1', + '--all-route-target', + '--import-target', + 'unset_irt1', + '--all-import-target', + '--export-target', + 'unset_ert1', + '--all-export-target', + '--route-distinguisher', + 'unset_rd1', + '--all-route-distinguisher', + ] + verifylist = [ + ('bgpvpn', fake_bgpvpn['id']), + ('route_targets', ['unset_rt1']), + ('purge_route_target', True), + ('import_targets', ['unset_irt1']), + ('purge_import_target', True), + ('export_targets', ['unset_ert1']), + ('purge_export_target', True), + ('route_distinguishers', ['unset_rd1']), + ('purge_route_distinguisher', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + attrs = { + 'route_targets': [], + 'import_targets': [], + 'export_targets': [], + 'route_distinguishers': [], + } + self.network_client.update_bgpvpn.assert_called_once_with( + fake_bgpvpn['id'], **attrs + ) + self.assertIsNone(result) + + +class TestDeleteBgpvpn(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.cmd = bgpvpn.DeleteBgpvpn(self.app, None) + + def test_delete_one_bgpvpn(self): + fake_bgpvpn = fakes.create_one_bgpvpn() + arglist = [ + fake_bgpvpn['id'], + ] + verifylist = [ + ('bgpvpns', [fake_bgpvpn['id']]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_bgpvpn.assert_called_once_with( + fake_bgpvpn['id'] + ) + self.assertIsNone(result) + + def test_delete_multi_bpgvpn(self): + fake_bgpvpns = fakes.create_bgpvpns(count=3) + fake_bgpvpn_ids = [fake_bgpvpn['id'] for fake_bgpvpn in fake_bgpvpns] + arglist = fake_bgpvpn_ids + verifylist = [ + ('bgpvpns', fake_bgpvpn_ids), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_bgpvpn.assert_has_calls( + [mock.call(id) for id in fake_bgpvpn_ids] + ) + self.assertIsNone(result) + + def test_delete_multi_bpgvpn_with_unknown(self): + count = 3 + fake_bgpvpns = fakes.create_bgpvpns(count=count) + fake_bgpvpn_ids = [fake_bgpvpn['id'] for fake_bgpvpn in fake_bgpvpns] + + def raise_unknonw_resource(resource_path, name_or_id): + if str(count - 2) in name_or_id: + raise Exception() + + self.network_client.delete_bgpvpn.side_effect = raise_unknonw_resource + arglist = fake_bgpvpn_ids + verifylist = [ + ('bgpvpns', fake_bgpvpn_ids), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + self.network_client.delete_bgpvpn.assert_has_calls( + [mock.call(id) for id in fake_bgpvpn_ids] + ) + + +class TestListBgpvpn(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.cmd = bgpvpn.ListBgpvpn(self.app, None) + + def test_list_all_bgpvpn(self): + count = 3 + fake_bgpvpns = fakes.create_bgpvpns(count=count) + self.network_client.bgpvpns.return_value = fake_bgpvpns + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.bgpvpns.assert_called_once() + self.assertEqual(headers, list(headers_short)) + self.assertListEqual( + list(data), + [ + _get_data(fake_bgpvpn, columns_short) + for fake_bgpvpn in fake_bgpvpns + ], + ) + + def test_list_all_bgpvpn_long_mode(self): + count = 3 + fake_bgpvpns = fakes.create_bgpvpns(count=count) + self.network_client.bgpvpns.return_value = fake_bgpvpns + arglist = [ + '--long', + ] + verifylist = [ + ('long', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.bgpvpns.assert_called_once() + self.assertEqual(headers, list(headers_long)) + self.assertListEqual( + list(data), + [ + _get_data(fake_bgpvpn, columns_long) + for fake_bgpvpn in fake_bgpvpns + ], + ) + + @mock.patch('osc_lib.cli.identity.find_project') + def test_list_project_bgpvpn(self, mock_find_project): + count = 3 + project_id = 'list_fake_project_id' + attrs = {'project_id': project_id} + fake_bgpvpns = fakes.create_bgpvpns(count=count, attrs=attrs) + self.network_client.bgpvpns.return_value = fake_bgpvpns + mock_find_project.return_value = mock.Mock(id=project_id) + arglist = [ + '--project', + project_id, + ] + verifylist = [ + ('project', project_id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.bgpvpns.assert_called_once_with( + project_id=project_id + ) + self.assertEqual(headers, list(headers_short)) + self.assertListEqual( + list(data), + [ + _get_data(fake_bgpvpn, columns_short) + for fake_bgpvpn in fake_bgpvpns + ], + ) + + def test_list_bgpvpn_with_filters(self): + count = 3 + name = 'fake_id0' + layer_type = 'l2' + attrs = {'type': layer_type} + fake_bgpvpns = fakes.create_bgpvpns(count=count, attrs=attrs) + returned_bgpvpn = fake_bgpvpns[0] + self.network_client.bgpvpns.return_value = [returned_bgpvpn] + arglist = [ + '--property', + f'name={name}', + '--property', + f'type={layer_type}', + ] + verifylist = [ + ('property', {'name': name, 'type': layer_type}), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.bgpvpns.assert_called_once_with( + name=name, type=layer_type + ) + self.assertEqual(headers, list(headers_short)) + self.assertListEqual( + list(data), [_get_data(returned_bgpvpn, columns_short)] + ) + + +class TestShowBgpvpn(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.cmd = bgpvpn.ShowBgpvpn(self.app, None) + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + + def test_show_bgpvpn(self): + fake_bgpvpn = fakes.create_one_bgpvpn() + self.network_client.get_bgpvpn.return_value = fake_bgpvpn + arglist = [ + fake_bgpvpn['id'], + ] + verifylist = [ + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + headers, _data = self.cmd.take_action(parsed_args) + + self.network_client.get_bgpvpn.assert_called_once_with( + fake_bgpvpn['id'] + ) + self.assertEqual(sorted(sorted_columns), sorted(headers)) diff --git a/openstackclient/tests/unit/network/v2/bgpvpn/test_network_association.py b/openstackclient/tests/unit/network/v2/bgpvpn/test_network_association.py new file mode 100644 index 0000000000..873c3625c1 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/bgpvpn/test_network_association.py @@ -0,0 +1,285 @@ +# Copyright 2026 Openinfra Foundation +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import operator +from unittest import mock + +from osc_lib import exceptions +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient.network.v2.bgpvpn import network_association +from openstackclient.tests.unit.network.v2.bgpvpn import fakes + + +columns_short = tuple( + col + for col, _, listing_mode in network_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_SHORT_ONLY) +) +columns_long = tuple( + col + for col, _, listing_mode in network_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +headers_short = tuple( + head + for _, head, listing_mode in network_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_SHORT_ONLY) +) +headers_long = tuple( + head + for _, head, listing_mode in network_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +sorted_attr_map = sorted( + network_association._attr_map, key=operator.itemgetter(1) +) +sorted_columns = tuple(col for col, _, _ in sorted_attr_map) +sorted_headers = tuple(head for _, head, _ in sorted_attr_map) + + +def _get_data(attrs, columns=sorted_columns): + return osc_utils.get_dict_properties( + attrs, columns, formatters=network_association._formatters + ) + + +class TestCreateNetAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.network_client.find_network.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.cmd = network_association.CreateBgpvpnNetAssoc(self.app, None) + + @mock.patch('osc_lib.cli.identity.find_project') + def test_create_network_association(self, mock_find_project): + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_res = fakes.create_one_resource() + fake_assoc = fakes.create_one_network_association() + self.network_client.create_bgpvpn_network_association.return_value = ( + fake_assoc + ) + mock_find_project.return_value = mock.Mock( + id=fake_bgpvpn['project_id'] + ) + arglist = [ + fake_bgpvpn['id'], + fake_res['id'], + '--project', + fake_bgpvpn['project_id'], + ] + verifylist = [ + ('bgpvpn', fake_bgpvpn['id']), + ('resource', fake_res['id']), + ('project', fake_bgpvpn['project_id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + cols, data = self.cmd.take_action(parsed_args) + + self.network_client.create_bgpvpn_network_association.assert_called_once_with( + fake_bgpvpn['id'], + network_id=fake_res['id'], + project_id='fake_project_id', + ) + self.assertEqual(sorted_columns, cols) + self.assertEqual(_get_data(fake_assoc), data) + + +class TestDeleteNetAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.cmd = network_association.DeleteBgpvpnNetAssoc(self.app, None) + + def test_delete_one_association(self): + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assoc = fakes.create_one_network_association() + arglist = [ + fake_assoc['id'], + fake_bgpvpn['id'], + ] + verifylist = [ + ('resource_association_ids', [fake_assoc['id']]), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_bgpvpn_network_association.assert_called_once_with( + fake_bgpvpn['id'], fake_assoc['id'] + ) + self.assertIsNone(result) + + def test_delete_multi_association(self): + count = 3 + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assocs = fakes.create_network_associations(count=count) + fake_assoc_ids = [a['id'] for a in fake_assocs] + arglist = [*fake_assoc_ids, fake_bgpvpn['id']] + verifylist = [ + ('resource_association_ids', fake_assoc_ids), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_bgpvpn_network_association.assert_has_calls( + [mock.call(fake_bgpvpn['id'], id) for id in fake_assoc_ids] + ) + self.assertIsNone(result) + + def test_delete_multi_association_with_unknown(self): + count = 3 + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assocs = fakes.create_network_associations(count=count) + fake_assoc_ids = [a['id'] for a in fake_assocs] + + def raise_unknown_resource(bgpvpn_id, association_id): + if str(count - 2) in association_id: + raise Exception() + + self.network_client.delete_bgpvpn_network_association.side_effect = ( + raise_unknown_resource + ) + arglist = [*fake_assoc_ids, fake_bgpvpn['id']] + verifylist = [ + ('resource_association_ids', fake_assoc_ids), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + self.network_client.delete_bgpvpn_network_association.assert_has_calls( + [mock.call(fake_bgpvpn['id'], id) for id in fake_assoc_ids] + ) + + +class TestListNetAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.cmd = network_association.ListBgpvpnNetAssoc(self.app, None) + + def test_list_network_associations(self): + count = 3 + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assocs = fakes.create_network_associations(count=count) + self.network_client.bgpvpn_network_associations.return_value = ( + fake_assocs + ) + arglist = [ + fake_bgpvpn['id'], + ] + verifylist = [ + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.bgpvpn_network_associations.assert_called_once_with( + fake_bgpvpn['id'], retrieve_all=True + ) + self.assertEqual(headers, list(headers_short)) + self.assertEqual( + list(data), + [_get_data(a, columns_short) for a in fake_assocs], + ) + + def test_list_network_associations_long_mode(self): + count = 3 + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assocs = fakes.create_network_associations(count=count) + self.network_client.bgpvpn_network_associations.return_value = ( + fake_assocs + ) + arglist = [ + '--long', + fake_bgpvpn['id'], + ] + verifylist = [ + ('long', True), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.bgpvpn_network_associations.assert_called_once_with( + fake_bgpvpn['id'], retrieve_all=True + ) + self.assertEqual(headers, list(headers_long)) + self.assertEqual( + list(data), + [_get_data(a, columns_long) for a in fake_assocs], + ) + + +class TestShowNetAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.cmd = network_association.ShowBgpvpnNetAssoc(self.app, None) + + def test_show_network_association(self): + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assoc = fakes.create_one_network_association() + self.network_client.get_bgpvpn_network_association.return_value = ( + fake_assoc + ) + + arglist = [ + fake_assoc['id'], + fake_bgpvpn['id'], + ] + verifylist = [ + ('resource_association_id', fake_assoc['id']), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.get_bgpvpn_network_association.assert_called_once_with( + fake_bgpvpn['id'], fake_assoc['id'] + ) + self.assertEqual(sorted_columns, columns) + self.assertEqual(data, _get_data(fake_assoc)) diff --git a/openstackclient/tests/unit/network/v2/bgpvpn/test_port_association.py b/openstackclient/tests/unit/network/v2/bgpvpn/test_port_association.py new file mode 100644 index 0000000000..a981d19524 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/bgpvpn/test_port_association.py @@ -0,0 +1,384 @@ +# Copyright 2026 Openinfra Foundation +# +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import operator +from unittest import mock + +from osc_lib import exceptions +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient.network.v2.bgpvpn import port_association +from openstackclient.tests.unit.network.v2.bgpvpn import fakes + + +columns_short = tuple( + col + for col, _, listing_mode in port_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_SHORT_ONLY) +) +columns_long = tuple( + col + for col, _, listing_mode in port_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +headers_short = tuple( + head + for _, head, listing_mode in port_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_SHORT_ONLY) +) +headers_long = tuple( + head + for _, head, listing_mode in port_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +sorted_attr_map = sorted( + port_association._attr_map, key=operator.itemgetter(1) +) +sorted_columns = tuple(col for col, _, _ in sorted_attr_map) +sorted_headers = tuple(head for _, head, _ in sorted_attr_map) + + +def _get_data(attrs, columns=sorted_columns): + return osc_utils.get_dict_properties( + attrs, columns, formatters=port_association._formatters + ) + + +class TestCreatePortAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.cmd = port_association.CreateBgpvpnPortAssoc(self.app, None) + self.fake_bgpvpn = fakes.create_one_bgpvpn() + self.fake_port = fakes.create_one_resource() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.network_client.find_port.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.fake_project = mock.Mock(id='fake_project_id') + self.identity_sdk_client.find_project = mock.Mock( + return_value=self.fake_project + ) + + @mock.patch('osc_lib.cli.identity.find_project') + def test_create_port_association(self, mock_find_project): + fake_assoc = fakes.create_one_port_association() + self.network_client.create_bgpvpn_port_association.return_value = ( + fake_assoc + ) + mock_find_project.return_value = mock.Mock( + id=self.fake_bgpvpn['project_id'] + ) + arglist = [ + self.fake_bgpvpn['id'], + self.fake_port['id'], + '--project', + self.fake_bgpvpn['project_id'], + ] + verifylist = [ + ('bgpvpn', self.fake_bgpvpn['id']), + ('port', self.fake_port['id']), + ('project', self.fake_bgpvpn['project_id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + cols, _data = self.cmd.take_action(parsed_args) + + self.network_client.create_bgpvpn_port_association.assert_called_once_with( + self.fake_bgpvpn['id'], + port_id=self.fake_port['id'], + project_id='fake_project_id', + routes=[], + ) + self.assertIn('id', cols) + self.assertIn('port_id', cols) + + +class TestSetPortAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.cmd = port_association.SetBgpvpnPortAssoc(self.app, None) + self.fake_bgpvpn = fakes.create_one_bgpvpn() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.network_client.find_bgpvpn_port_association.return_value = { + 'routes': [] + } + + def test_set_port_association_advertise(self): + fake_assoc = fakes.create_one_port_association() + arglist = [ + fake_assoc['id'], + self.fake_bgpvpn['id'], + '--advertise-fixed-ips', + ] + verifylist = [ + ('port_association_id', fake_assoc['id']), + ('bgpvpn', self.fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_bgpvpn_port_association.assert_called_once_with( + self.fake_bgpvpn['id'], + fake_assoc['id'], + advertise_fixed_ips=True, + routes=[], + ) + self.assertIsNone(result) + + def test_set_port_association_no_advertise(self): + fake_assoc = fakes.create_one_port_association() + arglist = [ + fake_assoc['id'], + self.fake_bgpvpn['id'], + '--no-advertise-fixed-ips', + ] + verifylist = [ + ('port_association_id', fake_assoc['id']), + ('bgpvpn', self.fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_bgpvpn_port_association.assert_called_once_with( + self.fake_bgpvpn['id'], + fake_assoc['id'], + advertise_fixed_ips=False, + routes=[], + ) + self.assertIsNone(result) + + +class TestUnsetPortAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.cmd = port_association.UnsetBgpvpnPortAssoc(self.app, None) + self.fake_bgpvpn = fakes.create_one_bgpvpn() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.network_client.find_bgpvpn_port_association.return_value = { + 'routes': [] + } + + def test_unset_port_association_advertise(self): + fake_assoc = fakes.create_one_port_association() + arglist = [ + fake_assoc['id'], + self.fake_bgpvpn['id'], + '--advertise-fixed-ips', + ] + verifylist = [ + ('port_association_id', fake_assoc['id']), + ('bgpvpn', self.fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_bgpvpn_port_association.assert_called_once_with( + self.fake_bgpvpn['id'], + fake_assoc['id'], + advertise_fixed_ips=False, + routes=[], + ) + self.assertIsNone(result) + + +class TestDeletePortAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.cmd = port_association.DeleteBgpvpnPortAssoc(self.app, None) + + def test_delete_one_association(self): + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assoc = fakes.create_one_port_association() + arglist = [ + fake_assoc['id'], + fake_bgpvpn['id'], + ] + verifylist = [ + ('port_association_ids', [fake_assoc['id']]), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_bgpvpn_port_association.assert_called_once_with( + fake_bgpvpn['id'], fake_assoc['id'] + ) + self.assertIsNone(result) + + def test_delete_multi_association(self): + count = 3 + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assocs = fakes.create_port_associations(count=count) + fake_assoc_ids = [a['id'] for a in fake_assocs] + arglist = [*fake_assoc_ids, fake_bgpvpn['id']] + verifylist = [ + ('port_association_ids', fake_assoc_ids), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_bgpvpn_port_association.assert_has_calls( + [mock.call(fake_bgpvpn['id'], id) for id in fake_assoc_ids] + ) + self.assertIsNone(result) + + def test_delete_multi_association_with_unknown(self): + count = 3 + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assocs = fakes.create_port_associations(count=count) + fake_assoc_ids = [a['id'] for a in fake_assocs] + + def raise_unknown_resource(bgpvpn_id, association_id): + if str(count - 2) in association_id: + raise Exception() + + self.network_client.delete_bgpvpn_port_association.side_effect = ( + raise_unknown_resource + ) + arglist = [*fake_assoc_ids, fake_bgpvpn['id']] + verifylist = [ + ('port_association_ids', fake_assoc_ids), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + self.network_client.delete_bgpvpn_port_association.assert_has_calls( + [mock.call(fake_bgpvpn['id'], id) for id in fake_assoc_ids] + ) + + +class TestListPortAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.cmd = port_association.ListBgpvpnPortAssoc(self.app, None) + + def test_list_port_associations(self): + count = 3 + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assocs = fakes.create_port_associations(count=count) + self.network_client.bgpvpn_port_associations.return_value = fake_assocs + arglist = [ + fake_bgpvpn['id'], + ] + verifylist = [ + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.bgpvpn_port_associations.assert_called_once_with( + fake_bgpvpn['id'], retrieve_all=True + ) + self.assertEqual(headers, list(headers_short)) + self.assertEqual( + list(data), + [_get_data(a, columns_short) for a in fake_assocs], + ) + + def test_list_port_associations_long_mode(self): + count = 3 + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assocs = fakes.create_port_associations(count=count) + self.network_client.bgpvpn_port_associations.return_value = fake_assocs + arglist = [ + '--long', + fake_bgpvpn['id'], + ] + verifylist = [ + ('long', True), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.bgpvpn_port_associations.assert_called_once_with( + fake_bgpvpn['id'], retrieve_all=True + ) + self.assertEqual(headers, list(headers_long)) + self.assertEqual( + list(data), + [_get_data(a, columns_long) for a in fake_assocs], + ) + + +class TestShowPortAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.cmd = port_association.ShowBgpvpnPortAssoc(self.app, None) + + def test_show_port_association(self): + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assoc = fakes.create_one_port_association() + self.network_client.get_bgpvpn_port_association.return_value = ( + fake_assoc + ) + + arglist = [ + fake_assoc['id'], + fake_bgpvpn['id'], + ] + verifylist = [ + ('port_association_id', fake_assoc['id']), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + cols, _data = self.cmd.take_action(parsed_args) + + self.network_client.get_bgpvpn_port_association.assert_called_once_with( + fake_bgpvpn['id'], fake_assoc['id'] + ) + # _transform_resource removes 'routes' and only adds + # prefix_routes/bgpvpn_routes if routes exist, so with empty + # routes the show columns won't include those fields + self.assertIn('id', cols) + self.assertIn('port_id', cols) diff --git a/openstackclient/tests/unit/network/v2/bgpvpn/test_router_association.py b/openstackclient/tests/unit/network/v2/bgpvpn/test_router_association.py new file mode 100644 index 0000000000..c3e1b24246 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/bgpvpn/test_router_association.py @@ -0,0 +1,297 @@ +# Copyright (c) 2018 Orange SA. +# All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import operator +from unittest import mock + +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient.network.v2.bgpvpn import router_association +from openstackclient.tests.unit.network.v2.bgpvpn import fakes +from openstackclient.tests.unit import utils + + +columns_short = tuple( + col + for col, _, listing_mode in router_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_SHORT_ONLY) +) +columns_long = tuple( + col + for col, _, listing_mode in router_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +headers_short = tuple( + head + for _, head, listing_mode in router_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_SHORT_ONLY) +) +headers_long = tuple( + head + for _, head, listing_mode in router_association._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +sorted_attr_map = sorted( + router_association._attr_map, key=operator.itemgetter(1) +) +sorted_columns = tuple(col for col, _, _ in sorted_attr_map) +sorted_headers = tuple(head for _, head, _ in sorted_attr_map) + + +def _get_data(attrs, columns=sorted_columns): + return osc_utils.get_dict_properties( + attrs, columns, formatters=router_association._formatters + ) + + +class TestCreateRouterAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.cmd = router_association.CreateBgpvpnRouterAssoc(self.app, None) + self.fake_bgpvpn = fakes.create_one_bgpvpn() + self.fake_router = fakes.create_one_resource() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.network_client.find_router.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + self.fake_project = mock.Mock(id='fake_project_id') + self.identity_sdk_client.find_project = mock.Mock( + return_value=self.fake_project + ) + + def _build_args(self, param=None): + arglist_base = [ + self.fake_bgpvpn['id'], + self.fake_router['id'], + '--project', + self.fake_bgpvpn['project_id'], + ] + if param is not None: + if isinstance(param, list): + arglist_base.extend(param) + else: + arglist_base.append(param) + return arglist_base + + def _build_verify_list(self, param=None): + verifylist = [ + ('bgpvpn', self.fake_bgpvpn['id']), + ('resource', self.fake_router['id']), + ('project', self.fake_bgpvpn['project_id']), + ] + if param is not None: + verifylist.append(param) + return verifylist + + def _exec_create_router_association( + self, + fake_assoc, + arglist, + verifylist, + ): + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + cols, data = self.cmd.take_action(parsed_args) + + fake_assoc_call = { + 'router_id': self.fake_router['id'], + 'project_id': 'fake_project_id', + } + if verifylist: + for key, value in verifylist: + if ( + key + not in ( + 'bgpvpn', + 'resource', + 'project', + ) + and value not in fake_assoc_call.values() + ): + fake_assoc_call[key] = value + self.network_client.create_bgpvpn_router_association.assert_called_once_with( + self.fake_bgpvpn['id'], **fake_assoc_call + ) + return cols, data + + def test_create_router_association(self): + fake_assoc = fakes.create_one_router_association() + + self.network_client.create_bgpvpn_router_association.return_value = ( + fake_assoc + ) + + arglist = self._build_args() + # advertise_extra_routes will be False since none + # of the mutually exclusive args present + verifylist = self._build_verify_list(('advertise_extra_routes', False)) + + self._exec_create_router_association(fake_assoc, arglist, verifylist) + + def test_create_router_association_advertise(self): + fake_assoc = fakes.create_one_router_association( + {'advertise_extra_routes': True} + ) + + self.network_client.create_bgpvpn_router_association.return_value = ( + fake_assoc + ) + + arglist = self._build_args('--advertise_extra_routes') + verifylist = self._build_verify_list(('advertise_extra_routes', True)) + + cols, data = self._exec_create_router_association( + fake_assoc, arglist, verifylist + ) + self.assertEqual(sorted(sorted_columns), sorted(cols)) + self.assertEqual(_get_data(fake_assoc, cols), data) + + def test_create_router_association_no_advertise(self): + fake_assoc = fakes.create_one_router_association( + {'advertise_extra_routes': False} + ) + + self.network_client.create_bgpvpn_router_association.return_value = ( + fake_assoc + ) + + arglist = self._build_args('--no-advertise_extra_routes') + verifylist = self._build_verify_list(('advertise_extra_routes', False)) + + cols, data = self._exec_create_router_association( + fake_assoc, arglist, verifylist + ) + self.assertEqual(sorted(sorted_columns), sorted(cols)) + self.assertEqual(_get_data(fake_assoc, cols), data) + + def test_create_router_association_advertise_fault(self): + arglist = self._build_args( + ['--advertise_extra_routes', '--no-advertise_extra_routes'] + ) + + try: + self._exec_create_router_association(None, arglist, None) + except utils.ParserException as e: + self.assertIn('Argument parse failed', format(e)) + + def test_router_association_unknown_arg(self): + arglist = self._build_args('--unknown arg') + + try: + self._exec_create_router_association(None, arglist, None) + except utils.ParserException as e: + self.assertIn('Argument parse failed', format(e)) + + +class TestSetRouterAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.cmd = router_association.SetBgpvpnRouterAssoc(self.app, None) + self.fake_bgpvpn = fakes.create_one_bgpvpn() + self.fake_router = fakes.create_one_resource() + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + + def _build_args(self, fake_assoc, param=None): + arglist_base = [fake_assoc['id'], self.fake_bgpvpn['id']] + if param is not None: + if isinstance(param, list): + arglist_base.extend(param) + else: + arglist_base.append(param) + return arglist_base + + def test_set_router_association_no_advertise(self): + fake_assoc = fakes.create_one_router_association( + {'advertise_extra_routes': True} + ) + + arglist = self._build_args(fake_assoc, '--no-advertise_extra_routes') + verifylist = [ + ('resource_association_id', fake_assoc['id']), + ('bgpvpn', self.fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_bgpvpn_router_association.assert_called_once_with( + self.fake_bgpvpn['id'], + fake_assoc['id'], + **{'advertise_extra_routes': False}, + ) + self.assertIsNone(result) + + def test_set_router_association_advertise(self): + fake_assoc = fakes.create_one_router_association( + {'advertise_extra_routes': False} + ) + + arglist = self._build_args(fake_assoc, '--advertise_extra_routes') + verifylist = [ + ('resource_association_id', fake_assoc['id']), + ('bgpvpn', self.fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_bgpvpn_router_association.assert_called_once_with( + self.fake_bgpvpn['id'], + fake_assoc['id'], + **{'advertise_extra_routes': True}, + ) + self.assertIsNone(result) + + +class TestShowRouterAssoc(fakes.TestNeutronClientBgpvpn): + def setUp(self): + super().setUp() + self.cmd = router_association.ShowBgpvpnRouterAssoc(self.app, None) + self.network_client.find_bgpvpn.side_effect = ( + lambda name_or_id, **kwargs: {'id': name_or_id} + ) + + def test_show_router_association(self): + fake_bgpvpn = fakes.create_one_bgpvpn() + fake_assoc = fakes.create_one_router_association( + {'advertise_extra_routes': True} + ) + self.network_client.get_bgpvpn_router_association.return_value = ( + fake_assoc + ) + arglist = [ + fake_assoc['id'], + fake_bgpvpn['id'], + ] + verifylist = [ + ('resource_association_id', fake_assoc['id']), + ('bgpvpn', fake_bgpvpn['id']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + cols, data = self.cmd.take_action(parsed_args) + + self.network_client.get_bgpvpn_router_association.assert_called_once_with( + fake_bgpvpn['id'], fake_assoc['id'] + ) + self.assertEqual(sorted(sorted_columns), sorted(cols)) + self.assertEqual(data, _get_data(fake_assoc, cols)) diff --git a/openstackclient/tests/unit/network/v2/dynamic_routing/__init__.py b/openstackclient/tests/unit/network/v2/dynamic_routing/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/tests/unit/network/v2/dynamic_routing/fakes.py b/openstackclient/tests/unit/network/v2/dynamic_routing/fakes.py new file mode 100644 index 0000000000..7b1060273a --- /dev/null +++ b/openstackclient/tests/unit/network/v2/dynamic_routing/fakes.py @@ -0,0 +1,120 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import uuid + +from openstack.network.v2 import agent as _agent +from openstack.network.v2 import bgp_peer as _bgp_peer +from openstack.network.v2 import bgp_speaker as _bgp_speaker + + +class FakeBgpSpeaker: + """Fake one or more bgp speakers.""" + + @staticmethod + def create_one_bgp_speaker(attrs=None): + attrs = attrs or {} + # Set default attributes. + bgp_speaker_attrs = { + 'peers': [], + 'local_as': 200, + 'advertise_tenant_networks': True, + 'networks': [], + 'ip_version': 4, + 'advertise_floating_ip_host_routes': True, + 'id': uuid.uuid4().hex, + 'name': 'bgp-speaker-' + uuid.uuid4().hex, + 'tenant_id': uuid.uuid4().hex, + } + + # Overwrite default attributes. + bgp_speaker_attrs.update(attrs) + ret_bgp_speaker = _bgp_speaker.BgpSpeaker(**bgp_speaker_attrs) + + return ret_bgp_speaker + + @staticmethod + def create_bgp_speakers(attrs=None, count=1): + """Create multiple fake bgp speakers.""" + bgp_speakers = [] + for i in range(count): + bgp_speaker = FakeBgpSpeaker.create_one_bgp_speaker(attrs) + bgp_speakers.append(bgp_speaker) + + return bgp_speakers + + +class FakeBgpPeer: + """Fake one or more bgp peers.""" + + @staticmethod + def create_one_bgp_peer(attrs=None): + attrs = attrs or {} + # Set default attributes. + bgp_peer_attrs = { + 'auth_type': None, + 'peer_ip': '1.1.1.1', + 'remote_as': 100, + 'id': uuid.uuid4().hex, + 'name': 'bgp-peer-' + uuid.uuid4().hex, + 'tenant_id': uuid.uuid4().hex, + } + + # Overwrite default attributes. + bgp_peer_attrs.update(attrs) + ret_bgp_peer = _bgp_peer.BgpPeer(**bgp_peer_attrs) + + return ret_bgp_peer + + @staticmethod + def create_bgp_peers(attrs=None, count=1): + """Create one or multiple fake bgp peers.""" + bgp_peers = [] + for i in range(count): + bgp_peer = FakeBgpPeer.create_one_bgp_peer(attrs) + bgp_peers.append(bgp_peer) + + return bgp_peers + + +class FakeDRAgent: + """Fake one or more dynamic routing agents.""" + + @staticmethod + def create_one_dragent(attrs=None): + attrs = attrs or {} + # Set default attributes. + dragent_attrs = { + 'binary': 'neutron-bgp-dragent', + 'admin_state_up': True, + 'availability_zone': None, + 'alive': True, + 'topic': 'bgp_dragent', + 'host': 'network-' + uuid.uuid4().hex, + 'name': 'bgp-dragent-' + uuid.uuid4().hex, + 'agent_type': 'BGP dynamic routing agent', + 'id': uuid.uuid4().hex, + } + + # Overwrite default attributes. + dragent_attrs.update(attrs) + return _agent.Agent(**dragent_attrs) + + @staticmethod + def create_dragents(attrs=None, count=1): + """Create one or multiple fake dynamic routing agents.""" + agents = [] + for i in range(count): + agent = FakeDRAgent.create_one_dragent(attrs) + agents.append(agent) + + return agents diff --git a/openstackclient/tests/unit/network/v2/dynamic_routing/test_bgp_dragent.py b/openstackclient/tests/unit/network/v2/dynamic_routing/test_bgp_dragent.py new file mode 100644 index 0000000000..8b5c287898 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/dynamic_routing/test_bgp_dragent.py @@ -0,0 +1,87 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from unittest import mock + +from openstackclient.network.v2.dynamic_routing import bgp_dragent +from openstackclient.tests.unit.network.v2.dynamic_routing import fakes +from openstackclient.tests.unit.network.v2 import fakes as test_fakes + + +class TestAddBgpSpeakerToDRAgent(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self._bgp_speaker = fakes.FakeBgpSpeaker.create_one_bgp_speaker() + self._bgp_dragent = fakes.FakeDRAgent.create_one_dragent() + self._bgp_speaker_id = self._bgp_speaker['id'] + self._bgp_dragent_id = self._bgp_dragent['id'] + + # Get the command object to test + self.cmd = bgp_dragent.AddBgpSpeakerToDRAgent(self.app, None) + + def test_add_bgp_speaker_to_dragent(self): + arglist = [ + self._bgp_dragent_id, + self._bgp_speaker_id, + ] + verifylist = [ + ('dragent_id', self._bgp_dragent_id), + ('bgp_speaker', self._bgp_speaker_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.network_client.find_bgp_speaker.return_value = mock.Mock( + id=self._bgp_speaker_id + ) + self.network_client.add_bgp_speaker_to_dragent.return_value = None + + result = self.cmd.take_action(parsed_args) + self.network_client.add_bgp_speaker_to_dragent.assert_called_once_with( + self._bgp_dragent_id, self._bgp_speaker_id + ) + self.assertIsNone(result) + + +class TestRemoveBgpSpeakerFromDRAgent(test_fakes.TestNetworkV2): + _bgp_speaker = fakes.FakeBgpSpeaker.create_one_bgp_speaker() + _bgp_dragent = fakes.FakeDRAgent.create_one_dragent() + _bgp_speaker_id = _bgp_speaker['id'] + _bgp_dragent_id = _bgp_dragent['id'] + + def setUp(self): + super().setUp() + + # Get the command object to test + self.cmd = bgp_dragent.RemoveBgpSpeakerFromDRAgent(self.app, None) + + def test_remove_bgp_speaker_from_dragent(self): + arglist = [ + self._bgp_dragent_id, + self._bgp_speaker_id, + ] + verifylist = [ + ('dragent_id', self._bgp_dragent_id), + ('bgp_speaker', self._bgp_speaker_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.network_client.find_bgp_speaker.return_value = mock.Mock( + id=self._bgp_speaker_id + ) + self.network_client.remove_bgp_speaker_from_dragent.return_value = None + + result = self.cmd.take_action(parsed_args) + self.network_client.remove_bgp_speaker_from_dragent.assert_called_once_with( + self._bgp_dragent_id, self._bgp_speaker_id + ) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/dynamic_routing/test_bgp_peer.py b/openstackclient/tests/unit/network/v2/dynamic_routing/test_bgp_peer.py new file mode 100644 index 0000000000..67e27c2ed3 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/dynamic_routing/test_bgp_peer.py @@ -0,0 +1,159 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from unittest import mock + +from openstackclient.network.v2.dynamic_routing import bgp_peer +from openstackclient.tests.unit.network.v2.dynamic_routing import fakes +from openstackclient.tests.unit.network.v2 import fakes as test_fakes + + +class TestListBgpPeer(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self._bgp_peers = fakes.FakeBgpPeer.create_bgp_peers(count=1) + self.columns = ('ID', 'Name', 'Peer IP', 'Remote AS') + self.data = [] + for _bgp_peer in self._bgp_peers: + self.data.append( + ( + _bgp_peer['id'], + _bgp_peer['name'], + _bgp_peer['peer_ip'], + _bgp_peer['remote_as'], + ) + ) + + self.network_client.bgp_peers.return_value = self._bgp_peers + + # Get the command object to test + self.cmd = bgp_peer.ListBgpPeer(self.app, None) + + def test_bgp_peer_list(self): + parsed_args = self.check_parser(self.cmd, [], []) + + columns, data = self.cmd.take_action(parsed_args) + self.network_client.bgp_peers.assert_called_once_with( + retrieve_all=True + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestDeleteBgpPeer(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self._bgp_peer = fakes.FakeBgpPeer.create_one_bgp_peer() + + self.network_client.delete_bgp_peer.return_value = None + self.network_client.find_bgp_peer.return_value = mock.Mock( + id=self._bgp_peer.id + ) + self.cmd = bgp_peer.DeleteBgpPeer(self.app, None) + + def test_delete_bgp_peer(self): + arglist = [ + self._bgp_peer['name'], + ] + verifylist = [ + ('bgp_peer', self._bgp_peer['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_bgp_peer.assert_called_once_with( + self._bgp_peer.id + ) + self.assertIsNone(result) + + +class TestShowBgpPeer(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self._one_bgp_peer = fakes.FakeBgpPeer.create_one_bgp_peer() + self.data = ( + self._one_bgp_peer['auth_type'], + self._one_bgp_peer['id'], + self._one_bgp_peer['name'], + self._one_bgp_peer['peer_ip'], + self._one_bgp_peer['tenant_id'], + self._one_bgp_peer['remote_as'], + ) + self._bgp_peer = self._one_bgp_peer + self._bgp_peer_name = self._one_bgp_peer['name'] + self.columns = ( + 'auth_type', + 'id', + 'name', + 'peer_ip', + 'project_id', + 'remote_as', + ) + + self.network_client.find_bgp_peer.return_value = self._bgp_peer + # Get the command object to test + self.cmd = bgp_peer.ShowBgpPeer(self.app, None) + + def test_bgp_peer_show(self): + arglist = [ + self._bgp_peer_name, + ] + verifylist = [ + ('bgp_peer', self._bgp_peer_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + data = self.cmd.take_action(parsed_args) + self.network_client.find_bgp_peer.assert_called_once_with( + self._bgp_peer.name, ignore_missing=False + ) + self.assertEqual(self.columns, data[0]) + self.assertEqual(self.data, data[1]) + + +class TestSetBgpPeer(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self._one_bgp_peer = fakes.FakeBgpPeer.create_one_bgp_peer() + self._bgp_peer_name = self._one_bgp_peer['name'] + + self.network_client.update_bgp_peer.return_value = None + self.network_client.find_bgp_peer.return_value = mock.Mock( + id=self._one_bgp_peer.id + ) + + self.cmd = bgp_peer.SetBgpPeer(self.app, None) + + def test_set_bgp_peer(self): + arglist = [ + self._bgp_peer_name, + '--name', + 'noob', + ] + verifylist = [ + ('bgp_peer', self._bgp_peer_name), + ('name', 'noob'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = {'name': 'noob', 'password': None} + self.network_client.update_bgp_peer.assert_called_once_with( + self._one_bgp_peer.id, **attrs + ) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/dynamic_routing/test_bgp_speaker.py b/openstackclient/tests/unit/network/v2/dynamic_routing/test_bgp_speaker.py new file mode 100644 index 0000000000..8406e8378d --- /dev/null +++ b/openstackclient/tests/unit/network/v2/dynamic_routing/test_bgp_speaker.py @@ -0,0 +1,165 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +from unittest import mock + +from openstackclient.network.v2.dynamic_routing import bgp_speaker +from openstackclient.tests.unit.network.v2.dynamic_routing import fakes +from openstackclient.tests.unit.network.v2 import fakes as test_fakes + + +class TestListBgpSpeaker(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self._bgp_speakers = fakes.FakeBgpSpeaker.create_bgp_speakers() + self.columns = ('ID', 'Name', 'Local AS', 'IP Version') + self.data = [] + for _bgp_speaker in self._bgp_speakers: + self.data.append( + ( + _bgp_speaker['id'], + _bgp_speaker['name'], + _bgp_speaker['local_as'], + _bgp_speaker['ip_version'], + ) + ) + + self.network_client.bgp_speakers.return_value = self._bgp_speakers + + # Get the command object to test + self.cmd = bgp_speaker.ListBgpSpeaker(self.app, None) + + def test_bgp_speaker_list(self): + parsed_args = self.check_parser(self.cmd, [], []) + + columns, data = self.cmd.take_action(parsed_args) + self.network_client.bgp_speakers.assert_called_once_with( + retrieve_all=True + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestDeleteBgpSpeaker(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self._bgp_speaker = fakes.FakeBgpSpeaker.create_one_bgp_speaker() + self.network_client.delete_bgp_speaker.return_value = None + self.network_client.find_bgp_speaker.return_value = mock.Mock( + id=self._bgp_speaker.id + ) + + self.cmd = bgp_speaker.DeleteBgpSpeaker(self.app, None) + + def test_delete_bgp_speaker(self): + arglist = [ + self._bgp_speaker['name'], + ] + verifylist = [ + ('bgp_speaker', self._bgp_speaker['name']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_bgp_speaker.assert_called_once_with( + self._bgp_speaker.id + ) + self.assertIsNone(result) + + +class TestShowBgpSpeaker(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self._one_bgp_speaker = fakes.FakeBgpSpeaker.create_one_bgp_speaker() + self.data = ( + self._one_bgp_speaker['advertise_floating_ip_host_routes'], + self._one_bgp_speaker['advertise_tenant_networks'], + self._one_bgp_speaker['id'], + self._one_bgp_speaker['ip_version'], + self._one_bgp_speaker['local_as'], + self._one_bgp_speaker['name'], + self._one_bgp_speaker['networks'], + self._one_bgp_speaker['peers'], + self._one_bgp_speaker['tenant_id'], + ) + self._bgp_speaker = self._one_bgp_speaker + self._bgp_speaker_name = self._one_bgp_speaker['name'] + self.columns = ( + 'advertise_floating_ip_host_routes', + 'advertise_tenant_networks', + 'id', + 'ip_version', + 'local_as', + 'name', + 'networks', + 'peers', + 'project_id', + ) + + self.network_client.find_bgp_speaker.return_value = self._bgp_speaker + # Get the command object to test + self.cmd = bgp_speaker.ShowBgpSpeaker(self.app, None) + + def test_bgp_speaker_show(self): + arglist = [ + self._bgp_speaker_name, + ] + verifylist = [ + ('bgp_speaker', self._bgp_speaker_name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + data = self.cmd.take_action(parsed_args) + self.network_client.find_bgp_speaker.assert_called_once_with( + self._bgp_speaker.name, ignore_missing=False + ) + self.assertEqual(self.columns, data[0]) + self.assertEqual(self.data, data[1]) + + +class TestSetBgpSpeaker(test_fakes.TestNetworkV2): + _one_bgp_speaker = fakes.FakeBgpSpeaker.create_one_bgp_speaker() + _bgp_speaker_name = _one_bgp_speaker['name'] + + def setUp(self): + super().setUp() + self.network_client.update_bgp_speaker.return_value = None + self.network_client.find_bgp_speaker.return_value = mock.Mock( + id=self._one_bgp_speaker.id + ) + + self.cmd = bgp_speaker.SetBgpSpeaker(self.app, None) + + def test_set_bgp_speaker(self): + arglist = [ + self._bgp_speaker_name, + '--name', + 'noob', + ] + verifylist = [ + ('bgp_speaker', self._bgp_speaker_name), + ('name', 'noob'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + attrs = {'name': 'noob'} + self.network_client.update_bgp_speaker.assert_called_once_with( + self._one_bgp_speaker.id, **attrs + ) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/fakes.py b/openstackclient/tests/unit/network/v2/fakes.py index d58f6e190e..837a1f71a3 100644 --- a/openstackclient/tests/unit/network/v2/fakes.py +++ b/openstackclient/tests/unit/network/v2/fakes.py @@ -16,7 +16,7 @@ from unittest import mock import uuid -from openstack.network.v2 import _proxy +from openstack.network import v2 as network_v2 from openstack.network.v2 import address_group as _address_group from openstack.network.v2 import address_scope as _address_scope from openstack.network.v2 import agent as network_agent @@ -58,6 +58,8 @@ from openstackclient.tests.unit import utils +PVLAN_TYPE_COMMUNITY = 'community' +PVLAN_COMMUNITY_NAME = 'community_1' RULE_TYPE_BANDWIDTH_LIMIT = 'bandwidth-limit' RULE_TYPE_DSCP_MARKING = 'dscp-marking' RULE_TYPE_MINIMUM_BANDWIDTH = 'minimum-bandwidth' @@ -97,7 +99,10 @@ class FakeClientMixin: def setUp(self): super().setUp() - self.app.client_manager.network = mock.Mock(spec=_proxy.Proxy) + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.app.client_manager.network = mock.Mock(spec=network_v2.Proxy) self.network_client = self.app.client_manager.network @@ -951,6 +956,7 @@ def create_one_network(attrs=None): 'provider:network_type': 'vlan', 'provider:physical_network': 'physnet1', 'provider:segmentation_id': "400", + 'pvlan': "False", 'router:external': True, 'availability_zones': [], 'availability_zone_hints': [], @@ -1211,6 +1217,8 @@ def create_one_port(attrs=None): 'security_group_ids': [], 'status': 'ACTIVE', 'project_id': 'project-id-' + uuid.uuid4().hex, + 'pvlan_type': PVLAN_TYPE_COMMUNITY, + 'pvlan_community': PVLAN_COMMUNITY_NAME, 'qos_network_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, 'qos_policy_id': 'qos-policy-id-' + uuid.uuid4().hex, 'tags': [], @@ -1309,25 +1317,6 @@ def create_network_agents(attrs=None, count=2): return agents -def get_network_agents(agents=None, count=2): - """Get an iterable Mock object with a list of faked network agents. - - If network agents list is provided, then initialize the Mock object - with the list. Otherwise create one. - - :param List agents: - A list of Agent objects faking network agents - :param int count: - The number of network agents to fake - :return: - An iterable Mock object with side_effect set to a list of faked - network agents - """ - if agents is None: - agents = create_network_agents(count) - return mock.Mock(side_effect=agents) - - def create_one_network_rbac(attrs=None): """Create a fake network rbac @@ -1412,6 +1401,7 @@ def create_one_security_group(attrs=None): 'security_group_rules': [], 'tags': [], 'location': 'MUNCHMUNCHMUNCH', + 'is_shared': False, } # Overwrite default attributes. @@ -1961,26 +1951,6 @@ def create_local_ip_associations(attrs=None, count=2): return local_ip_associations -def get_local_ip_associations(local_ip_associations=None, count=2): - """Get a list of faked local ip associations - - If local ip association list is provided, then initialize - the Mock object with the list. Otherwise create one. - - :param List local_ip_associations: - A list of FakeResource objects faking local ip associations - :param int count: - The number of local ip associations to fake - :return: - An iterable Mock object with side_effect set to a list of faked - local ip associations - """ - if local_ip_associations is None: - local_ip_associations = create_local_ip_associations(count) - - return mock.Mock(side_effect=local_ip_associations) - - def create_one_ndp_proxy(attrs=None): """Create a fake NDP proxy. @@ -2026,25 +1996,6 @@ def create_ndp_proxies(attrs=None, count=2): return ndp_proxies -def get_ndp_proxies(ndp_proxies=None, count=2): - """Get a list of faked NDP proxies. - - If ndp_proxy list is provided, then initialize the Mock object - with the list. Otherwise create one. - - :param List ndp_proxies: - A list of FakeResource objects faking ndp proxy - :param int count: - The number of ndp proxy to fake - :return: - An iterable Mock object with side_effect set to a list of faked - ndp proxy - """ - if ndp_proxies is None: - ndp_proxies = create_ndp_proxies(count) - return mock.Mock(side_effect=ndp_proxies) - - def create_one_trunk(attrs=None): """Create a fake trunk. @@ -2095,22 +2046,3 @@ def create_trunks(attrs=None, count=2): trunks.append(create_one_trunk(attrs)) return trunks - - -def get_trunks(trunks=None, count=2): - """Get an iterable Mock object with a list of faked trunks. - - If trunk list is provided, then initialize the Mock object - with the list. Otherwise create one. - - :param List trunks: - A list of FakeResource objects faking trunks - :param int count: - The number of trunks to fake - :return: - An iterable Mock object with side_effect set to a list of faked - trunks - """ - if trunks is None: - trunks = create_trunks(count) - return mock.Mock(side_effect=trunks) diff --git a/openstackclient/tests/unit/network/v2/fwaas/__init__.py b/openstackclient/tests/unit/network/v2/fwaas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/tests/unit/network/v2/fwaas/test_group.py b/openstackclient/tests/unit/network/v2/fwaas/test_group.py new file mode 100644 index 0000000000..b9f50a05af --- /dev/null +++ b/openstackclient/tests/unit/network/v2/fwaas/test_group.py @@ -0,0 +1,923 @@ +# Copyright 2016 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy +import re +from unittest import mock + +from openstack.network.v2 import firewall_group +from openstack.test import fakes as sdk_fakes +from osc_lib import exceptions + +from openstackclient.network.v2.fwaas import group as fwaas_group +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as test_utils + + +CONVERT_MAP = { + 'ingress_firewall_policy': 'ingress_firewall_policy_id', + 'egress_firewall_policy': 'egress_firewall_policy_id', + 'no_ingress_firewall_policy': 'ingress_firewall_policy_id', + 'no_egress_firewall_policy': 'egress_firewall_policy_id', + 'positional_name': 'name', + 'project': 'project_id', + 'port': 'ports', +} + + +def _generate_response(source=None, data=None): + source = source if source else {} + up = { + 'admin_state_up': fwaas_group.AdminStateColumn( + source['admin_state_up'] + ) + } + if data: + up.append(data) + source.update(up) + return source + + +def _generate_req_and_res(verifylist, response): + request = dict(verifylist) + for key, val in verifylist: + del request[key] + if re.match('^no_', key) and val is True: + new_value = None + elif val is True or val is False: + new_value = val + elif key in ('name', 'description'): + new_value = val + else: + new_value = val + converted = CONVERT_MAP.get(key, key) + request[converted] = new_value + response[converted] = new_value + return request, response + + +class TestFirewallGroup(network_fakes.TestNetworkV2): + def check_results(self, headers, data, exp_req, is_list=False): + if is_list: + req_body = {self.res_plural: list(exp_req)} + else: + req_body = exp_req + self.mocked.assert_called_once_with(**req_body) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + + def setUp(self): + super().setUp() + + self.resource = sdk_fakes.generate_fake_resource( + firewall_group.FirewallGroup + ) + + def _find_resource(*args, **kwargs): + return self.resource + + self.network_client.find_firewall_group.side_effect = _find_resource + self.identity_sdk_client.find_project.side_effect = ( + lambda name_or_id, **kw: mock.Mock(id=name_or_id) + ) + self.res = 'firewall_group' + self.res_plural = 'firewall_groups' + self.list_headers = ( + 'ID', + 'Name', + 'Ingress Policy ID', + 'Egress Policy ID', + ) + self.list_data = ( + self.resource['id'], + self.resource['name'], + self.resource['ingress_firewall_policy_id'], + self.resource['egress_firewall_policy_id'], + ) + self.headers = tuple( + ( + *self.list_headers, + 'Description', + 'Status', + 'Ports', + 'State', + 'Shared', + 'Project', + ) + ) + self.data = _generate_response(self.resource) + self.ordered_headers = copy.deepcopy(tuple(sorted(self.headers))) + self.expected_data = ( + self.resource['description'], + self.resource['egress_firewall_policy_id'], + self.resource['id'], + self.resource['ingress_firewall_policy_id'], + self.resource['name'], + self.resource['ports'], + self.resource['project_id'], + self.resource['shared'], + fwaas_group.AdminStateColumn(self.resource['admin_state_up']), + self.resource['status'], + ) + self.ordered_columns = ( + 'description', + 'egress_firewall_policy_id', + 'id', + 'ingress_firewall_policy_id', + 'name', + 'ports', + 'project_id', + 'shared', + 'admin_state_up', + 'status', + ) + + +class TestCreateFirewallGroup(TestFirewallGroup): + def setUp(self): + super().setUp() + self.network_client.create_firewall_group.return_value = self.resource + self.mocked = self.network_client.create_firewall_group + self.cmd = fwaas_group.CreateFirewallGroup(self.app, None) + + def _update_expect_response(self, request, response): + """Set expected request and response + + :param request + A dictionary of request body(dict of verifylist) + :param response + A OrderedDict of request body + """ + # Update response body + self.network_client.create_firewall_group.return_value = response + # Update response(finally returns 'data') + self.data = _generate_response(source=response) + self.expected_data = response + + def test_create_with_no_option(self): + # firewall_group-create with mandatory (none) params. + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + + def test_create_with_port(self): + # firewall_group-create with 'port' + port_id = 'id_for_port' + + def _mock_find(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_port.side_effect = _mock_find + arglist = ['--port', port_id] + verifylist = [('port', [port_id])] + request, response = _generate_req_and_res(verifylist, self.resource) + self._update_expect_response(request, response) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.check_results(headers, data, request) + + def test_create_with_ingress_policy(self): + ingress_policy = 'my-ingress-policy' + + def _mock_port_fwg(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_policy.side_effect = _mock_port_fwg + + arglist = ['--ingress-firewall-policy', ingress_policy] + verifylist = [('ingress_firewall_policy', ingress_policy)] + request, response = _generate_req_and_res(verifylist, self.resource) + self._update_expect_response(request, response) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + self.network_client.find_firewall_policy.assert_called_once_with( + ingress_policy, ignore_missing=False + ) + + self.check_results(headers, data, request) + + def test_create_with_egress_policy(self): + egress_policy = 'my-egress-policy' + + def _mock_find(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_group.side_effect = _mock_find + self.network_client.find_firewall_policy.side_effect = _mock_find + + arglist = ['--egress-firewall-policy', egress_policy] + verifylist = [('egress_firewall_policy', egress_policy)] + request, response = _generate_req_and_res(verifylist, self.resource) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.find_firewall_policy.assert_called_once_with( + egress_policy, ignore_missing=False + ) + self.check_results(headers, data, request) + + def test_create_with_all_params(self): + name = 'my-name' + description = 'my-desc' + ingress_policy = 'my-ingress-policy' + egress_policy = 'my-egress-policy' + + def _mock_find(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_policy.side_effect = _mock_find + port = 'port' + self.network_client.find_port.side_effect = _mock_find + project_id = 'my-project' + arglist = [ + name, + '--description', + description, + '--ingress-firewall-policy', + ingress_policy, + '--egress-firewall-policy', + egress_policy, + '--port', + port, + '--project', + project_id, + '--share', + '--disable', + ] + verifylist = [ + ('positional_name', name), + ('description', description), + ('ingress_firewall_policy', ingress_policy), + ('egress_firewall_policy', egress_policy), + ('port', [port]), + ('shared', True), + ('project', project_id), + ('admin_state_up', False), + ] + request, response = _generate_req_and_res(verifylist, self.resource) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.check_results(headers, data, request) + + def test_create_with_name_option_deprecated(self): + name = 'my-name' + arglist = ['--name', name] + verifylist = [('name', name)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + with mock.patch.object(fwaas_group.LOG, 'warning') as mock_warning: + headers, _data = self.cmd.take_action(parsed_args) + mock_warning.assert_called_once_with( + 'The --name option is deprecated for the "firewall group ' + 'create" command, please pass the name as a positional ' + 'argument instead.' + ) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + + def test_create_with_both_positional_and_option_name(self): + name = 'my-name' + arglist = [name, '--name', 'other-name'] + verifylist = [ + ('positional_name', name), + ('name', 'other-name'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + def test_create_with_shared_and_no_share(self): + arglist = [ + '--share', + '--no-share', + ] + verifylist = [ + ('shared', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_ports_and_no(self): + port = 'my-port' + arglist = [ + '--port', + port, + '--no-port', + ] + verifylist = [ + ('port', [port]), + ('no_port', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_ingress_policy_and_no(self): + policy = 'my-policy' + arglist = [ + '--ingress-firewall-policy', + policy, + '--no-ingress-firewall-policy', + ] + verifylist = [ + ('ingress_firewall_policy', policy), + ('no_ingress_firewall_policy', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_egress_policy_and_no(self): + policy = 'my-policy' + arglist = [ + '--egress-firewall-policy', + policy, + '--no-egress-firewall-policy', + ] + verifylist = [ + ('egress_firewall_policy', policy), + ('no_egress_firewall_policy', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + +class TestListFirewallGroup(TestFirewallGroup): + def setUp(self): + super().setUp() + self.network_client.firewall_groups.return_value = [self.resource] + self.mocked = self.network_client.firewall_groups + self.cmd = fwaas_group.ListFirewallGroup(self.app, None) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.list_headers), headers) + self.assertEqual([self.list_data], list(data)) + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + +class TestShowFirewallGroup(TestFirewallGroup): + def setUp(self): + super().setUp() + self.network_client.get_firewall_group.return_value = self.resource + self.mocked = self.network_client.get_firewall_group + self.cmd = fwaas_group.ShowFirewallGroup(self.app, None) + + def test_show_filtered_by_id_or_name(self): + target = self.resource['id'] + + def _mock_fwaas(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_group.side_effect = _mock_fwaas + + arglist = [target] + verifylist = [(self.res, target)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target) + self.assertEqual(self.ordered_headers, headers) + + +class TestSetFirewallGroup(TestFirewallGroup): + def setUp(self): + super().setUp() + self.resource['ports'] = ['old_port'] + self.network_client.update_firewall_group.return_value = { + self.res: self.resource + } + self.mocked = self.network_client.update_firewall_group + + def _mock_find_port(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_port.side_effect = _mock_find_port + + self.cmd = fwaas_group.SetFirewallGroup(self.app, None) + + def _update_expect_response(self, request, response): + """Set expected request and response + + :param request + A dictionary of request body(dict of verifylist) + :param response + A OrderedDict of request body + """ + # Update response(finally returns 'data') + self.data = _generate_response(source=response) + self.ordered_data = tuple( + response[column] for column in self.ordered_columns + ) + + def test_set_name(self): + target = self.resource['id'] + update = 'change' + arglist = [target, '--name', update] + verifylist = [ + (self.res, target), + ('name', update), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'name': update}) + self.assertIsNone(result) + + def test_set_options(self): + target = self.resource['id'] + updated_desc = 'change-desc' + arglist = [target, '--description', updated_desc, '--share'] + verifylist = [ + (self.res, target), + ('description', updated_desc), + ('shared', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, shared=True, description=updated_desc + ) + self.assertIsNone(result) + + def test_set_ingress_policy_and_egress_policy(self): + target = self.resource['id'] + ingress_policy = 'ingress_policy' + egress_policy = 'egress_policy' + + def _mock_fwg_policy(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_group.side_effect = _mock_fwg_policy + self.network_client.find_firewall_policy.side_effect = _mock_fwg_policy + + arglist = [ + target, + '--ingress-firewall-policy', + ingress_policy, + '--egress-firewall-policy', + egress_policy, + ] + verifylist = [ + (self.res, target), + ('ingress_firewall_policy', ingress_policy), + ('egress_firewall_policy', egress_policy), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, + **{ + 'ingress_firewall_policy_id': ingress_policy, + 'egress_firewall_policy_id': egress_policy, + }, + ) + self.assertIsNone(result) + + def test_set_port(self): + target = self.resource['id'] + port1 = 'additional_port1' + port2 = 'additional_port2' + + def _mock_port_fwg(*args, **kwargs): + return mock.Mock(id=args[0], ports=self.resource['ports']) + + self.network_client.find_firewall_group.side_effect = _mock_port_fwg + self.network_client.find_port.side_effect = _mock_port_fwg + + arglist = [ + target, + '--port', + port1, + '--port', + port2, + ] + verifylist = [ + (self.res, target), + ('port', [port1, port2]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + expect = {'ports': sorted(self.resource['ports'] + [port1, port2])} + self.mocked.assert_called_once_with(target, **expect) + self.assertEqual(2, self.network_client.find_firewall_group.call_count) + self.assertIsNone(result) + + def test_set_no_port(self): + # firewall_group-update myid --policy newpolicy. + target = self.resource['id'] + arglist = [target, '--no-port'] + verifylist = [ + (self.res, target), + ('no_port', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'ports': []}) + self.assertIsNone(result) + + def test_set_admin_state(self): + target = self.resource['id'] + arglist = [target, '--enable'] + verifylist = [ + (self.res, target), + ('admin_state_up', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'admin_state_up': True}) + self.assertIsNone(result) + + def test_set_shared(self): + target = self.resource['id'] + arglist = [target, '--share'] + verifylist = [ + (self.res, target), + ('shared', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'shared': True}) + self.assertIsNone(result) + + def test_set_no_share(self): + target = self.resource['id'] + arglist = [target, '--no-share'] + verifylist = [ + (self.res, target), + ('shared', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'shared': False}) + self.assertIsNone(result) + + def test_set_egress_policy(self): + target = self.resource['id'] + policy = 'egress_policy' + + def _mock_find_policy(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_policy.side_effect = ( + _mock_find_policy + ) + + arglist = [target, '--egress-firewall-policy', policy] + verifylist = [ + (self.res, target), + ('egress_firewall_policy', policy), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'egress_firewall_policy_id': policy} + ) + self.assertIsNone(result) + + def test_set_no_ingress_policies(self): + target = self.resource['id'] + arglist = [target, '--no-ingress-firewall-policy'] + verifylist = [ + (self.res, target), + ('no_ingress_firewall_policy', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'ingress_firewall_policy_id': None} + ) + self.assertIsNone(result) + + def test_set_no_egress_policies(self): + target = self.resource['id'] + arglist = [target, '--no-egress-firewall-policy'] + verifylist = [ + (self.res, target), + ('no_egress_firewall_policy', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'egress_firewall_policy_id': None} + ) + self.assertIsNone(result) + + def test_set_port_and_no_port(self): + target = self.resource['id'] + port = 'my-port' + arglist = [ + target, + '--port', + port, + '--no-port', + ] + verifylist = [ + (self.res, target), + ('port', [port]), + ('no_port', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.mocked.assert_called_once_with(target, **{'ports': [port]}) + self.assertIsNone(result) + + def test_set_ingress_policy_and_no_ingress_policy(self): + target = self.resource['id'] + arglist = [ + target, + '--ingress-firewall-policy', + 'my-ingress', + '--no-ingress-firewall-policy', + ] + verifylist = [ + (self.res, target), + ('ingress_firewall_policy', 'my-ingress'), + ('no_ingress_firewall_policy', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_set_egress_policy_and_no_egress_policy(self): + target = self.resource['id'] + arglist = [ + target, + '--egress-firewall-policy', + 'my-egress', + '--no-egress-firewall-policy', + ] + verifylist = [ + (self.res, target), + ('egress_firewall_policy', 'my-egress'), + ('no_egress_firewall_policy', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_set_and_raises(self): + self.network_client.update_firewall_group.side_effect = Exception + target = self.resource['id'] + arglist = [target, '--name', 'my-name'] + verifylist = [(self.res, target), ('name', 'my-name')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestDeleteFirewallGroup(TestFirewallGroup): + def setUp(self): + super().setUp() + # Mock objects + self.mocked = self.network_client.delete_firewall_group + self.cmd = fwaas_group.DeleteFirewallGroup(self.app, None) + + def test_delete_with_one_resource(self): + target = self.resource['id'] + + def _mock_fwaas(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_group.side_effect = _mock_fwaas + + arglist = [target] + verifylist = [(self.res, [target])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target) + self.assertIsNone(result) + + def test_delete_with_multiple_resources(self): + + def _mock_fwaas(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_group.side_effect = _mock_fwaas + + target1 = 'target1' + target2 = 'target2' + arglist = [target1, target2] + verifylist = [(self.res, [target1, target2])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertEqual(2, self.mocked.call_count) + for idx, reference in enumerate([target1, target2]): + actual = ''.join(self.mocked.call_args_list[idx][0][0]) + self.assertEqual(reference, actual) + + def test_delete_multiple_with_exception(self): + target1 = 'target1' + target2 = 'target2' + arglist = [target1, target2] + verifylist = [(self.res, [target1, target2])] + + def _mock_find(*args, **kwargs): + if args[0] == target2: + raise Exception('Not found') + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_group.side_effect = _mock_find + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestUnsetFirewallGroup(TestFirewallGroup): + def setUp(self): + super().setUp() + self.resource['ports'] = ['old_port'] + # Mock objects + self.mocked = self.network_client.update_firewall_group + self.cmd = fwaas_group.UnsetFirewallGroup(self.app, None) + + def test_unset_shared(self): + target = self.resource['id'] + arglist = [ + target, + '--share', + ] + verifylist = [ + (self.res, target), + ('share', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + with mock.patch.object(fwaas_group.LOG, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) + mock_warning.assert_called_once_with( + 'The --share option is deprecated, please use ' + '"firewall group set --no-share" instead.' + ) + self.mocked.assert_called_once_with(target, **{'shared': False}) + self.assertIsNone(result) + + def test_unset_ingress_policy(self): + target = self.resource['id'] + arglist = [ + target, + '--ingress-firewall-policy', + ] + verifylist = [ + (self.res, target), + ('ingress_firewall_policy', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.mocked.assert_called_once_with( + target, **{'ingress_firewall_policy_id': None} + ) + self.assertIsNone(result) + + def test_unset_egress_policy(self): + target = self.resource['id'] + arglist = [ + target, + '--egress-firewall-policy', + ] + verifylist = [ + (self.res, target), + ('egress_firewall_policy', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.mocked.assert_called_once_with( + target, **{'egress_firewall_policy_id': None} + ) + self.assertIsNone(result) + + def test_unset_enable(self): + target = self.resource['id'] + arglist = [ + target, + '--enable', + ] + verifylist = [ + (self.res, target), + ('enable', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + with mock.patch.object(fwaas_group.LOG, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) + mock_warning.assert_called_once_with( + 'The --enable option is deprecated, please use ' + '"firewall group set --disable" instead.' + ) + self.mocked.assert_called_once_with( + target, **{'admin_state_up': False} + ) + self.assertIsNone(result) + + def test_unset_port(self): + target = self.resource['id'] + port = 'old_port' + + def _mock_port_fwg(*args, **kwargs): + return mock.Mock(id=args[0], ports=self.resource['ports']) + + self.network_client.find_firewall_group.side_effect = _mock_port_fwg + self.network_client.find_port.side_effect = _mock_port_fwg + + arglist = [ + target, + '--port', + port, + ] + verifylist = [ + (self.res, target), + ('port', [port]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.mocked.assert_called_once_with(target, **{'ports': []}) + self.assertIsNone(result) + + def test_unset_all_port(self): + target = self.resource['id'] + arglist = [ + target, + '--all-port', + ] + verifylist = [ + (self.res, target), + ('all_port', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.mocked.assert_called_once_with(target, **{'ports': []}) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/fwaas/test_policy.py b/openstackclient/tests/unit/network/v2/fwaas/test_policy.py new file mode 100644 index 0000000000..3fed68405d --- /dev/null +++ b/openstackclient/tests/unit/network/v2/fwaas/test_policy.py @@ -0,0 +1,869 @@ +# Copyright 2016 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import re +from unittest import mock + +from openstack.network.v2 import firewall_policy +from openstack.network.v2 import firewall_rule +from openstack.test import fakes as sdk_fakes +from osc_lib import exceptions + +from openstackclient.network.v2.fwaas import policy as fwaas_policy +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as test_utils + +CONVERT_MAP = { + 'project': 'project_id', + 'port': 'ports', + 'name': 'name', + 'id': 'id', + 'firewall_rule': 'firewall_rules', + 'description': 'description', +} + + +def _generate_data(source=None, data=None): + source = source or {} + if data: + source.update(data) + return tuple(source[key] for key in source) + + +def _generate_req_and_res(verifylist, response): + request = dict(verifylist) + for key, val in verifylist: + converted = CONVERT_MAP.get(key, key) + del request[key] + if re.match('^no_', key) and val is True: + new_value = None + elif val is True or val is False: + new_value = val + elif key in ('name', 'description'): + new_value = val + else: + new_value = val + request[converted] = new_value + response[converted] = new_value + return request, response + + +class TestFirewallPolicy(network_fakes.TestNetworkV2): + def check_results(self, headers, data, exp_req, is_list=False): + if is_list: + req_body = {self.res_plural: [exp_req]} + else: + req_body = exp_req + self.mocked.assert_called_once_with(**req_body) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + + def setUp(self): + super().setUp() + + self.identity_sdk_client.find_project.side_effect = ( + lambda name_or_id, **kw: mock.Mock(id=name_or_id) + ) + + self.res = 'firewall_policy' + self.res_plural = 'firewall_policies' + self.resource = sdk_fakes.generate_fake_resource( + firewall_policy.FirewallPolicy + ) + # TODO(slaweq): Remove this "firewall_rules" override once bug + # https://bugs.launchpad.net/openstacksdk/+bug/2146537 will be fixed in + # OpenStackSDK + self.resource['firewall_rules'] = [self.resource['firewall_rules']] + + self.list_headers = ( + 'ID', + 'Name', + 'Firewall Rules', + ) + self.list_data = ( + self.resource['id'], + self.resource['name'], + self.resource['firewall_rules'], + ) + self.headers = tuple( + (*self.list_headers, 'Description', 'Audited', 'Shared', 'Project') + ) + self.data = _generate_data(self.resource) + self.ordered_headers = ( + 'Audited', + 'Description', + 'Firewall Rules', + 'ID', + 'Name', + 'Project', + 'Shared', + ) + self.ordered_data = ( + self.resource['audited'], + self.resource['description'], + self.resource['firewall_rules'], + self.resource['id'], + self.resource['name'], + self.resource['project_id'], + self.resource['shared'], + ) + self.ordered_columns = ( + 'audited', + 'description', + 'firewall_rules', + 'id', + 'name', + 'project_id', + 'shared', + ) + + +class TestCreateFirewallPolicy(TestFirewallPolicy): + def setUp(self): + super().setUp() + self.network_client.create_firewall_policy.return_value = self.resource + self.mocked = self.network_client.create_firewall_policy + self.cmd = fwaas_policy.CreateFirewallPolicy(self.app, None) + + def _update_expect_response(self, request, response): + """Set expected request and response + + :param request + A dictionary of request body(dict of verifylist) + :param response + A OrderedDict of request body + """ + # Update response body + pass + # Update response(finally returns 'data') + self.data = _generate_data(data=response) + self.ordered_data = tuple( + response[column] for column in self.ordered_columns + ) + + def test_create_with_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_mandatory_param(self): + name = 'my-fwg' + arglist = [ + name, + ] + verifylist = [ + ('name', name), + ] + request, response = _generate_req_and_res(verifylist, self.resource) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.check_results(headers, data, request) + + def test_create_with_rules(self): + name = 'my-fwg' + rule1 = 'rule1' + rule2 = 'rule2' + + def _mock_find_rule(*args, **kwargs): + return sdk_fakes.generate_fake_resource( + firewall_rule.FirewallRule, id=args[0] + ) + + self.network_client.find_firewall_rule.side_effect = _mock_find_rule + + arglist = [ + name, + '--firewall-rule', + rule1, + '--firewall-rule', + rule2, + ] + verifylist = [ + ('name', name), + ('firewall_rule', [rule1, rule2]), + ] + request, response = _generate_req_and_res(verifylist, self.resource) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + self.assertEqual(2, self.network_client.find_firewall_rule.call_count) + + self.check_results(headers, data, request) + + def test_create_with_all_params(self): + name = 'my-fwp' + desc = 'my-desc' + rule1 = 'rule1' + rule2 = 'rule2' + project = 'my-tenant' + + def _mock_find(*args, **kwargs): + if self.res in args[0]: + rules = self.resource['firewall_rules'] + return mock.Mock(id=args[0], firewall_rules=rules) + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_policy.side_effect = _mock_find + self.network_client.find_firewall_rule.side_effect = _mock_find + + arglist = [ + name, + '--description', + desc, + '--firewall-rule', + rule1, + '--firewall-rule', + rule2, + '--project', + project, + '--share', + '--audited', + ] + verifylist = [ + ('name', name), + ('description', desc), + ('firewall_rule', [rule1, rule2]), + ('project', project), + ('shared', True), + ('audited', True), + ] + request, response = _generate_req_and_res(verifylist, self.resource) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.check_results(headers, data, request) + + def test_create_with_firewall_rule_and_no(self): + name = 'my-fwp' + rule1 = 'rule1' + rule2 = 'rule2' + arglist = [ + name, + '--firewall-rule', + rule1, + '--firewall-rule', + rule2, + '--no-firewall-rule', + ] + verifylist = [ + ('name', name), + ('firewall_rule', [rule1, rule2]), + ('no_firewall_rule', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_shared_and_no_share(self): + name = 'my-fwp' + arglist = [ + name, + '--share', + '--no-share', + ] + verifylist = [ + ('name', name), + ('shared', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_audited_and_no(self): + name = 'my-fwp' + arglist = [ + name, + '--audited', + '--no-audited', + ] + verifylist = [ + ('name', name), + ('audited', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + +class TestListFirewallPolicy(TestFirewallPolicy): + def setUp(self): + super().setUp() + self.network_client.firewall_policies.return_value = [self.resource] + self.mocked = self.network_client.firewall_policies + self.cmd = fwaas_policy.ListFirewallPolicy(self.app, None) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.list_headers), headers) + self.assertEqual([self.list_data], list(data)) + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + +class TestShowFirewallPolicy(TestFirewallPolicy): + def setUp(self): + super().setUp() + self.network_client.get_firewall_policy.return_value = self.resource + self.mocked = self.network_client.get_firewall_policy + self.cmd = fwaas_policy.ShowFirewallPolicy(self.app, None) + + def test_show_filtered_by_id_or_name(self): + target = self.resource['id'] + + def _mock_fwaas(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_policy.side_effect = _mock_fwaas + + arglist = [target] + verifylist = [(self.res, target)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target) + self.assertEqual(self.ordered_headers, headers) + + +class TestSetFirewallPolicy(TestFirewallPolicy): + def setUp(self): + super().setUp() + self.network_client.update_firewall_policy.return_value = self.resource + self.mocked = self.network_client.update_firewall_policy + + def _mock_find_rule(*args, **kwargs): + return sdk_fakes.generate_fake_resource( + firewall_rule.FirewallRule, id=args[0] + ) + + def _mock_find_policy(*args, **kwargs): + return self.resource + + self.network_client.find_firewall_policy.side_effect = ( + _mock_find_policy + ) + self.network_client.find_firewall_rule.side_effect = _mock_find_rule + + self.cmd = fwaas_policy.SetFirewallPolicy(self.app, None) + + def test_set_name(self): + target = self.resource['id'] + update = 'change' + arglist = [target, '--name', update] + verifylist = [ + (self.res, target), + ('name', update), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'name': update}) + self.assertIsNone(result) + + def test_set_options(self): + target = self.resource['id'] + updated_desc = 'change-desc' + arglist = [target, '--description', updated_desc, '--share'] + verifylist = [ + (self.res, target), + ('description', updated_desc), + ('shared', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, shared=True, description=updated_desc + ) + self.assertIsNone(result) + + def test_set_rules(self): + target = self.resource['id'] + rule1 = 'new_rule1' + rule2 = 'new_rule2' + arglist = [ + target, + '--firewall-rule', + rule1, + '--firewall-rule', + rule2, + ] + verifylist = [ + (self.res, target), + ('firewall_rule', [rule1, rule2]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + expect = self.resource['firewall_rules'] + [rule1, rule2] + body = {'firewall_rules': expect} + self.mocked.assert_called_once_with(target, **body) + self.assertEqual(2, self.network_client.find_firewall_rule.call_count) + self.assertEqual( + 2, self.network_client.find_firewall_policy.call_count + ) + self.assertIsNone(result) + + def test_set_no_rules(self): + target = self.resource['id'] + arglist = [target, '--no-firewall-rule'] + verifylist = [ + (self.res, target), + ('no_firewall_rule', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + body = {'firewall_rules': []} + self.mocked.assert_called_once_with(target, **body) + self.assertIsNone(result) + + def test_set_rules_and_no_rules(self): + target = self.resource['id'] + rule1 = 'rule1' + arglist = [ + target, + '--firewall-rule', + rule1, + '--no-firewall-rule', + ] + verifylist = [ + (self.res, target), + ('firewall_rule', [rule1]), + ('no_firewall_rule', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + body = {'firewall_rules': [rule1]} + self.mocked.assert_called_once_with(target, **body) + self.assertEqual(1, self.network_client.find_firewall_rule.call_count) + self.assertEqual( + 1, self.network_client.find_firewall_policy.call_count + ) + self.assertIsNone(result) + + def test_set_audited(self): + target = self.resource['id'] + arglist = [target, '--audited'] + verifylist = [ + (self.res, target), + ('audited', True), + ] + body = {'audited': True} + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **body) + self.assertIsNone(result) + + def test_set_no_audited(self): + target = self.resource['id'] + arglist = [target, '--no-audited'] + verifylist = [ + (self.res, target), + ('audited', False), + ] + body = {'audited': False} + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **body) + self.assertIsNone(result) + + def test_set_audited_and_no_audited(self): + target = self.resource['id'] + arglist = [ + target, + '--audited', + '--no-audited', + ] + verifylist = [ + (self.res, target), + ('audited', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_set_and_raises(self): + self.network_client.update_firewall_policy.side_effect = Exception + target = self.resource['id'] + + arglist = [target, '--name', 'my-name'] + verifylist = [(self.res, target), ('name', 'my-name')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestDeleteFirewallPolicy(TestFirewallPolicy): + def setUp(self): + super().setUp() + self.network_client.delete_firewall_policy.return_value = { + self.res: self.resource + } + self.mocked = self.network_client.delete_firewall_policy + self.cmd = fwaas_policy.DeleteFirewallPolicy(self.app, None) + + def test_delete_with_one_resource(self): + target = self.resource['id'] + + def _mock_fwaas(*args, **kwargs): + return self.resource + + self.network_client.find_firewall_policy.side_effect = _mock_fwaas + + arglist = [target] + verifylist = [(self.res, [target])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target) + self.assertIsNone(result) + + def test_delete_with_multiple_resources(self): + + def _mock_fwaas(*args, **kwargs): + return sdk_fakes.generate_fake_resource( + firewall_policy.FirewallPolicy, id=args[0] + ) + + self.network_client.find_firewall_policy.side_effect = _mock_fwaas + + target1 = 'target1' + target2 = 'target2' + arglist = [target1, target2] + verifylist = [(self.res, [target1, target2])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertEqual(2, self.mocked.call_count) + for idx, reference in enumerate([target1, target2]): + actual = ''.join(self.mocked.call_args_list[idx][0][0]) + self.assertEqual(reference, actual) + + def test_delete_multiple_with_exception(self): + target1 = 'target1' + target2 = 'target2' + arglist = [target1, target2] + verifylist = [(self.res, [target1, target2])] + + def _mock_find(*args, **kwargs): + if args[0] == target2: + raise Exception('Not found') + return self.resource + + self.network_client.find_firewall_policy.side_effect = _mock_find + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestFirewallPolicyInsertRule(TestFirewallPolicy): + def setUp(self): + super().setUp() + self.network_client.insert_rule_into_policy.return_value = { + self.res: self.resource + } + self.mocked = self.network_client.insert_rule_into_policy + + def _mock_find_policy(*args, **kwargs): + return self.resource + + def _mock_find_rule(*args, **kwargs): + return sdk_fakes.generate_fake_resource( + firewall_rule.FirewallRule, id=args[0] + ) + + self.network_client.find_firewall_policy.side_effect = ( + _mock_find_policy + ) + self.network_client.find_firewall_rule.side_effect = _mock_find_rule + + self.cmd = fwaas_policy.FirewallPolicyInsertRule(self.app, None) + + def test_insert_firewall_rule(self): + target = self.resource['id'] + rule = 'new-rule' + before = 'before' + after = 'after' + arglist = [ + target, + rule, + '--insert-before', + before, + '--insert-after', + after, + ] + verifylist = [ + (self.res, target), + ('firewall_rule', rule), + ('insert_before', before), + ('insert_after', after), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + body = { + 'firewall_rule_id': rule, + 'insert_before': before, + 'insert_after': after, + } + self.mocked.assert_called_once_with(target, **body) + self.assertIsNone(result) + self.assertEqual( + 1, self.network_client.find_firewall_policy.call_count + ) + self.assertEqual(3, self.network_client.find_firewall_rule.call_count) + + def test_insert_with_no_firewall_rule(self): + target = self.resource['id'] + arglist = [ + target, + ] + verifylist = [ + (self.res, target), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + +class TestFirewallPolicyRemoveRule(TestFirewallPolicy): + def setUp(self): + super().setUp() + self.network_client.remove_rule_from_policy.return_value = { + self.res: self.resource + } + self.mocked = self.network_client.remove_rule_from_policy + + def _mock_find_policy(*args, **kwargs): + return mock.Mock(id=args[0]) + + self.network_client.find_firewall_policy.side_effect = ( + _mock_find_policy + ) + self.network_client.find_firewall_rule.side_effect = _mock_find_policy + + self.cmd = fwaas_policy.FirewallPolicyRemoveRule(self.app, None) + + def test_remove_firewall_rule(self): + target = self.resource['id'] + rule = 'remove-rule' + arglist = [ + target, + rule, + ] + verifylist = [ + (self.res, target), + ('firewall_rule', rule), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + body = {'firewall_rule_id': rule} + self.mocked.assert_called_once_with(target, **body) + self.assertIsNone(result) + self.assertEqual( + 1, self.network_client.find_firewall_policy.call_count + ) + self.assertEqual(1, self.network_client.find_firewall_rule.call_count) + + def test_remove_with_no_firewall_rule(self): + target = self.resource['id'] + arglist = [ + target, + ] + verifylist = [ + (self.res, target), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + +class TestUnsetFirewallPolicy(TestFirewallPolicy): + def setUp(self): + super().setUp() + self.network_client.update_firewall_policy.return_value = { + self.res: self.resource + } + self.mocked = self.network_client.update_firewall_policy + + def _mock_find_rule(*args, **kwargs): + return sdk_fakes.generate_fake_resource( + firewall_rule.FirewallRule, id=args[0] + ) + + def _mock_find_policy(*args, **kwargs): + return mock.Mock( + id=args[0], + firewall_rules=self.resource['firewall_rules'], + ) + + self.network_client.find_firewall_policy.side_effect = ( + _mock_find_policy + ) + self.network_client.find_firewall_rule.side_effect = _mock_find_rule + + self.cmd = fwaas_policy.UnsetFirewallPolicy(self.app, None) + + def test_unset_shared(self): + target = self.resource['id'] + arglist = [ + target, + '--share', + ] + verifylist = [ + (self.res, target), + ('share', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + with mock.patch.object(fwaas_policy.LOG, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) + mock_warning.assert_called_once_with( + 'The --share option is deprecated, please use ' + '"firewall policy set --no-share" instead.' + ) + self.mocked.assert_called_once_with(target, **{'shared': False}) + self.assertIsNone(result) + + def test_unset_audited(self): + target = self.resource['id'] + arglist = [ + target, + '--audited', + ] + verifylist = [ + (self.res, target), + ('audited', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + body = {'audited': False} + self.mocked.assert_called_once_with(target, **body) + self.assertIsNone(result) + + def test_unset_firewall_rule_not_matched(self): + self.resource['firewall_rules'] = ['old_rule'] + target = self.resource['id'] + rule = 'new_rule' + arglist = [ + target, + '--firewall-rule', + rule, + ] + verifylist = [ + (self.res, target), + ('firewall_rule', [rule]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + body = {'firewall_rules': self.resource['firewall_rules']} + self.mocked.assert_called_once_with(target, **body) + self.assertIsNone(result) + + def test_unset_firewall_rule_matched(self): + self.resource['firewall_rules'] = ['rule1', 'rule2'] + target = self.resource['id'] + rule = 'rule1' + arglist = [ + target, + '--firewall-rule', + rule, + ] + verifylist = [ + (self.res, target), + ('firewall_rule', [rule]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + body = {'firewall_rules': ['rule2']} + self.mocked.assert_called_once_with(target, **body) + self.assertIsNone(result) + self.assertEqual( + 2, self.network_client.find_firewall_policy.call_count + ) + self.assertEqual(1, self.network_client.find_firewall_rule.call_count) + + def test_unset_all_firewall_rule(self): + target = self.resource['id'] + arglist = [ + target, + '--all-firewall-rule', + ] + verifylist = [ + (self.res, target), + ('all_firewall_rule', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + body = {'firewall_rules': []} + self.mocked.assert_called_once_with(target, **body) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/fwaas/test_rule.py b/openstackclient/tests/unit/network/v2/fwaas/test_rule.py new file mode 100644 index 0000000000..e04ad64182 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/fwaas/test_rule.py @@ -0,0 +1,984 @@ +# Copyright 2016 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import re +from unittest import mock + +from openstack.network.v2 import firewall_rule +from openstack.test import fakes as sdk_fakes +from osc_lib import exceptions +import testtools + +from openstackclient.network.v2.fwaas import rule as fwaas_rule +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as test_utils + + +CONVERT_MAP = { + 'positional_name': 'name', + 'project': 'project_id', +} + + +def _generate_data(source=None, data=None): + if data: + source.update(data) + ret = tuple(_replace_display_columns(key, source[key]) for key in source) + return ret + + +def _replace_display_columns(key, val): + if key == 'protocol': + return fwaas_rule.ProtocolColumn(val) + return val + + +def _generate_req_and_res(verifylist, response): + request = dict(verifylist) + for key, val in verifylist: + converted = CONVERT_MAP.get(key, key) + del request[key] + if re.match('^no_', key) and val is True: + new_value = None + elif key == 'protocol' and val and val.lower() == 'any': + new_value = None + elif val is True or val is False: + new_value = val + elif key in ('name', 'description'): + new_value = val + else: + new_value = val + request[converted] = new_value + response[converted] = new_value + return request, response + + +class TestFirewallRule(network_fakes.TestNetworkV2): + def check_results(self, headers, data, exp_req=None, is_list=False): + if is_list: + req_body = {self.res_plural: [exp_req]} + else: + req_body = exp_req + if not exp_req: + self.mocked.assert_called_once_with() + else: + self.mocked.assert_called_once_with(**req_body) + self.assertEqual(self.ordered_headers, headers) + + def setUp(self): + super().setUp() + + self.identity_sdk_client.find_project.side_effect = ( + lambda name_or_id, **kw: mock.Mock(id=name_or_id) + ) + self.res = 'firewall_rule' + self.res_plural = 'firewall_rules' + self.resource = sdk_fakes.generate_fake_resource( + firewall_rule.FirewallRule + ) + self.headers = ( + 'ID', + 'Name', + 'Enabled', + 'Description', + 'Firewall Policy', + 'IP Version', + 'Action', + 'Protocol', + 'Source IP Address', + 'Source Port', + 'Destination IP Address', + 'Destination Port', + 'Shared', + 'Project', + 'Source Firewall Group ID', + 'Destination Firewall Group ID', + ) + self.data = _generate_data(self.resource) + self.ordered_headers = ( + 'Action', + 'Description', + 'Destination Firewall Group ID', + 'Destination IP Address', + 'Destination Port', + 'Enabled', + 'Firewall Policy', + 'ID', + 'IP Version', + 'Name', + 'Project', + 'Protocol', + 'Shared', + 'Source Firewall Group ID', + 'Source IP Address', + 'Source Port', + 'Summary', + ) + self.ordered_data = ( + self.resource['action'], + self.resource['description'], + self.resource['destination_ip_address'], + self.resource['destination_port'], + self.resource['firewall_policy_id'], + self.resource['enabled'], + self.resource['id'], + self.resource['ip_version'], + self.resource['name'], + self.resource['project_id'], + _replace_display_columns('protocol', self.resource['protocol']), + self.resource['shared'], + self.resource['source_ip_address'], + self.resource['source_port'], + ) + self.ordered_columns = ( + 'action', + 'description', + 'destination_ip_address', + 'destination_port', + 'enabled', + 'id', + 'ip_version', + 'name', + 'project_id', + 'protocol', + 'shared', + 'source_ip_address', + 'source_port', + ) + + +class TestCreateFirewallRule(TestFirewallRule): + def setUp(self): + super().setUp() + self.network_client.create_firewall_rule.return_value = self.resource + self.mocked = self.network_client.create_firewall_rule + + def _mock_find_group(*args, **kwargs): + return self.resource + + self.network_client.find_firewall_group.side_effect = _mock_find_group + + self.cmd = fwaas_rule.CreateFirewallRule(self.app, None) + + def _update_expect_response(self, request, response): + """Set expected request and response + + :param request + A dictionary of request body(dict of verifylist) + :param response + A OrderedDict of request body + """ + # Update response body + self.network_client.create_firewall_rule.return_value = response + # Update response(finally returns 'data') + self.data = _generate_data(source=response) + self.ordered_data = tuple( + _replace_display_columns(column, response[column]) + for column in self.ordered_columns + ) + + def _set_all_params(self, args={}): + name = args.get('name') or 'my-name' + description = args.get('description') or 'my-desc' + source_ip = args.get('source_ip_address') or '192.168.1.0/24' + destination_ip = args.get('destination_ip_address') or '192.168.2.0/24' + source_port = args.get('source_port') or '0:65535' + protocol = args.get('protocol') or 'udp' + action = args.get('action') or 'deny' + ip_version = args.get('ip_version') or '4' + destination_port = args.get('destination_port') or '0:65535' + project_id = args.get('project_id') or 'my-tenant' + arglist = [ + name, + '--description', + description, + '--protocol', + protocol, + '--ip-version', + ip_version, + '--source-ip-address', + source_ip, + '--destination-ip-address', + destination_ip, + '--source-port', + source_port, + '--destination-port', + destination_port, + '--action', + action, + '--project', + project_id, + '--disable-rule', + '--share', + ] + + verifylist = [ + ('positional_name', name), + ('description', description), + ('shared', True), + ('protocol', protocol), + ('ip_version', ip_version), + ('source_ip_address', source_ip), + ('destination_ip_address', destination_ip), + ('source_port', source_port), + ('destination_port', destination_port), + ('action', action), + ('enabled', False), + ('project', project_id), + ] + return arglist, verifylist + + def _test_create_with_all_params(self, args={}): + arglist, verifylist = self._set_all_params(args) + request, response = _generate_req_and_res(verifylist, self.resource) + self._update_expect_response(request, response) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.check_results(headers, data, request) + + def test_create_with_no_options(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + self.check_results(headers, data, None) + + def test_create_with_all_params(self): + self._test_create_with_all_params() + + def test_create_with_all_params_protocol_any(self): + self._test_create_with_all_params({'protocol': 'any'}) + + def test_create_with_all_params_ip_version_6(self): + self._test_create_with_all_params({'ip_version': '6'}) + + def test_create_with_all_params_invalid_ip_version(self): + arglist, verifylist = self._set_all_params({'ip_version': '128'}) + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_all_params_action_upper_capitalized(self): + for action in ('Allow', 'DENY', 'Reject'): + arglist, verifylist = self._set_all_params({'action': action}) + self.assertRaises( + testtools.matchers._impl.MismatchError, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_all_params_protocol_upper_capitalized(self): + for protocol in ('TCP', 'Tcp', 'ANY', 'AnY', 'iCMp'): + arglist, verifylist = self._set_all_params({'protocol': protocol}) + self.assertRaises( + testtools.matchers._impl.MismatchError, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_name_option_deprecated(self): + name = 'my-name' + arglist = ['--name', name] + verifylist = [('name', name)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + with mock.patch.object(fwaas_rule.LOG, 'warning') as mock_warning: + _headers, _data = self.cmd.take_action(parsed_args) + mock_warning.assert_called_once_with( + 'The --name option is deprecated for the "firewall group ' + 'rule create" command, please pass the name as a positional ' + 'argument instead.' + ) + + def test_create_with_both_positional_and_option_name(self): + name = 'my-name' + arglist = [name, '--name', 'other-name'] + verifylist = [ + ('positional_name', name), + ('name', 'other-name'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestListFirewallRule(TestFirewallRule): + def _setup_summary(self, expect=None): + protocol = (self.resource['protocol'] or 'any').upper() + src = 'source(port): 192.168.1.0/24(1:11111)' + dst = 'dest(port): 192.168.2.2(2:22222)' + action = 'deny' + if expect: + if expect.get('protocol'): + protocol = expect['protocol'].upper() + if expect.get('source_ip_address'): + src_ip = expect['source_ip_address'] + if expect.get('source_port'): + src_port = expect['source_port'] + if expect.get('destination_ip_address'): + dst_ip = expect['destination_ip_address'] + if expect.get('destination_port'): + dst_port = expect['destination_port'] + if expect.get('action'): + action = expect['action'] + src = 'source(port): ' + src_ip + '(' + src_port + ')' + dst = 'dest(port): ' + dst_ip + '(' + dst_port + ')' + return ',\n '.join([protocol, src, dst, action]) + + def setUp(self): + super().setUp() + self.cmd = fwaas_rule.ListFirewallRule(self.app, None) + + self.short_header = ( + 'ID', + 'Name', + 'Enabled', + 'Summary', + 'Firewall Policy', + ) + + summary = self._setup_summary(self.resource) + + self.short_data = ( + self.resource['id'], + self.resource['name'], + self.resource['enabled'], + summary, + self.resource['firewall_policy_id'], + ) + self.network_client.firewall_rules.return_value = [self.resource] + self.mocked = self.network_client.firewall_rules + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertListItemEqual([self.short_data], list(data)) + + +class TestShowFirewallRule(TestFirewallRule): + def setUp(self): + super().setUp() + self.network_client.get_firewall_rule.return_value = self.resource + self.mocked = self.network_client.get_firewall_rule + self.cmd = fwaas_rule.ShowFirewallRule(self.app, None) + + def test_show_filtered_by_id_or_name(self): + target = self.resource['id'] + + def _mock_fwaas(*args, **kwargs): + return sdk_fakes.generate_fake_resource( + firewall_rule.FirewallRule, id=args[0] + ) + + self.network_client.find_firewall_rule.side_effect = _mock_fwaas + + arglist = [target] + verifylist = [(self.res, target)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target) + self.assertEqual(self.ordered_headers, headers) + + +class TestSetFirewallRule(TestFirewallRule): + def setUp(self): + super().setUp() + self.network_client.update_firewall_rule.return_value = self.resource + self.mocked = self.network_client.update_firewall_rule + + def _mock_find_rule(*args, **kwargs): + return sdk_fakes.generate_fake_resource( + firewall_rule.FirewallRule, id=args[0] + ) + + self.network_client.find_firewall_rule.side_effect = _mock_find_rule + + self.cmd = fwaas_rule.SetFirewallRule(self.app, None) + + def test_set_name(self): + target = self.resource['id'] + update = 'change' + arglist = [target, '--name', update] + verifylist = [ + (self.res, target), + ('name', update), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'name': update}) + self.assertIsNone(result) + + def test_set_options(self): + target = self.resource['id'] + updated_desc = 'change-desc' + arglist = [target, '--description', updated_desc, '--share'] + verifylist = [ + (self.res, target), + ('description', updated_desc), + ('shared', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, shared=True, description=updated_desc + ) + self.assertIsNone(result) + + def test_set_protocol_with_any(self): + target = self.resource['id'] + protocol = 'any' + arglist = [target, '--protocol', protocol] + verifylist = [ + (self.res, target), + ('protocol', protocol), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'protocol': None}) + self.assertIsNone(result) + + def test_set_protocol_with_udp(self): + target = self.resource['id'] + protocol = 'udp' + arglist = [target, '--protocol', protocol] + verifylist = [ + (self.res, target), + ('protocol', protocol), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'protocol': protocol}) + self.assertIsNone(result) + + def test_set_source_ip_address(self): + target = self.resource['id'] + src_ip = '192.192.192.192' + arglist = [target, '--source-ip-address', src_ip] + verifylist = [ + (self.res, target), + ('source_ip_address', src_ip), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'source_ip_address': src_ip} + ) + self.assertIsNone(result) + + def test_set_source_port(self): + target = self.resource['id'] + src_port = '32678' + arglist = [target, '--source-port', src_port] + verifylist = [ + (self.res, target), + ('source_port', src_port), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'source_port': src_port} + ) + self.assertIsNone(result) + + def test_set_destination_ip_address(self): + target = self.resource['id'] + dst_ip = '0.1.0.1' + arglist = [target, '--destination-ip-address', dst_ip] + verifylist = [ + (self.res, target), + ('destination_ip_address', dst_ip), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'destination_ip_address': dst_ip} + ) + self.assertIsNone(result) + + def test_set_destination_port(self): + target = self.resource['id'] + dst_port = '65432' + arglist = [target, '--destination-port', dst_port] + verifylist = [ + (self.res, target), + ('destination_port', dst_port), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'destination_port': dst_port} + ) + self.assertIsNone(result) + + def test_set_enable_rule(self): + target = self.resource['id'] + arglist = [target, '--enable-rule'] + verifylist = [ + (self.res, target), + ('enabled', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'enabled': True}) + self.assertIsNone(result) + + def test_set_disable_rule(self): + target = self.resource['id'] + arglist = [target, '--disable-rule'] + verifylist = [ + (self.res, target), + ('enabled', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'enabled': False}) + self.assertIsNone(result) + + def test_set_action(self): + target = self.resource['id'] + action = 'reject' + arglist = [target, '--action', action] + verifylist = [ + (self.res, target), + ('action', action), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'action': action}) + self.assertIsNone(result) + + def test_set_enable_rule_and_disable_rule(self): + target = self.resource['id'] + arglist = [target, '--enable-rule', '--disable-rule'] + verifylist = [ + (self.res, target), + ('enabled', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_set_no_source_ip_address(self): + target = self.resource['id'] + arglist = [ + target, + '--no-source-ip-address', + ] + verifylist = [ + (self.res, target), + ('no_source_ip_address', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'source_ip_address': None} + ) + self.assertIsNone(result) + + def test_set_no_source_port(self): + target = self.resource['id'] + arglist = [ + target, + '--no-source-port', + ] + verifylist = [ + (self.res, target), + ('no_source_port', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'source_port': None}) + self.assertIsNone(result) + + def test_set_no_destination_ip_address(self): + target = self.resource['id'] + arglist = [ + target, + '--no-destination-ip-address', + ] + verifylist = [ + (self.res, target), + ('no_destination_ip_address', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'destination_ip_address': None} + ) + self.assertIsNone(result) + + def test_set_no_destination_port(self): + target = self.resource['id'] + arglist = [ + target, + '--no-destination-port', + ] + verifylist = [ + (self.res, target), + ('no_destination_port', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'destination_port': None} + ) + self.assertIsNone(result) + + def test_set_source_ip_address_and_no(self): + target = self.resource['id'] + arglist = [ + target, + '--source-ip-address', + '192.168.1.0/24', + '--no-source-ip-address', + ] + verifylist = [ + (self.res, target), + ('source_ip_address', '192.168.1.0/24'), + ('no_source_ip_address', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_set_destination_ip_address_and_no(self): + target = self.resource['id'] + arglist = [ + target, + '--destination-ip-address', + '192.168.2.0/24', + '--no-destination-ip-address', + ] + verifylist = [ + (self.res, target), + ('destination_ip_address', '192.168.2.0/24'), + ('no_destination_ip_address', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_set_source_port_and_no(self): + target = self.resource['id'] + arglist = [ + target, + '--source-port', + '1:12345', + '--no-source-port', + ] + verifylist = [ + (self.res, target), + ('source_port', '1:12345'), + ('no_source_port', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_set_destination_port_and_no(self): + target = self.resource['id'] + arglist = [ + target, + '--destination-port', + '1:54321', + '--no-destination-port', + ] + verifylist = [ + (self.res, target), + ('destination_port', '1:54321'), + ('no_destination_port', True), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_set_and_raises(self): + self.network_client.update_firewall_rule.side_effect = Exception + target = self.resource['id'] + arglist = [target, '--name', 'my-name'] + verifylist = [(self.res, target), ('name', 'my-name')] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestUnsetFirewallRule(TestFirewallRule): + def setUp(self): + super().setUp() + self.mocked = self.network_client.update_firewall_rule + + def _mock_find_rule(*args, **kwargs): + return sdk_fakes.generate_fake_resource( + firewall_rule.FirewallRule, id=args[0] + ) + + self.network_client.find_firewall_rule.side_effect = _mock_find_rule + + self.cmd = fwaas_rule.UnsetFirewallRule(self.app, None) + + def test_unset_shared(self): + target = self.resource['id'] + arglist = [ + target, + '--share', + ] + verifylist = [ + (self.res, target), + ('share', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + with mock.patch.object(fwaas_rule.LOG, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) + mock_warning.assert_called_once_with( + 'The --share option is deprecated, please use ' + '"firewall rule set --no-share" instead.' + ) + self.mocked.assert_called_once_with(target, **{'shared': False}) + self.assertIsNone(result) + + def test_unset_protocol_and_raise(self): + self.network_client.update_firewall_rule.side_effect = Exception + target = self.resource['id'] + arglist = [ + target, + '--protocol', + ] + verifylist = [(self.res, target), ('protocol', False)] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_unset_source_port(self): + target = self.resource['id'] + arglist = [ + target, + '--source-port', + ] + verifylist = [ + (self.res, target), + ('source_port', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target, **{'source_port': None}) + self.assertIsNone(result) + + def test_unset_destination_port(self): + target = self.resource['id'] + arglist = [ + target, + '--destination-port', + ] + verifylist = [ + (self.res, target), + ('destination_port', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'destination_port': None} + ) + self.assertIsNone(result) + + def test_unset_source_ip_address(self): + target = self.resource['id'] + arglist = [ + target, + '--source-ip-address', + ] + verifylist = [ + (self.res, target), + ('source_ip_address', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'source_ip_address': None} + ) + self.assertIsNone(result) + + def test_unset_destination_ip_address(self): + target = self.resource['id'] + arglist = [ + target, + '--destination-ip-address', + ] + verifylist = [ + (self.res, target), + ('destination_ip_address', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + target, **{'destination_ip_address': None} + ) + self.assertIsNone(result) + + def test_unset_enable_rule(self): + target = self.resource['id'] + arglist = [ + target, + '--enable-rule', + ] + verifylist = [ + (self.res, target), + ('enable_rule', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + with mock.patch.object(fwaas_rule.LOG, 'warning') as mock_warning: + result = self.cmd.take_action(parsed_args) + mock_warning.assert_called_once_with( + 'The --enable-rule option is deprecated, please use ' + '"firewall rule set --disable-rule" instead.' + ) + self.mocked.assert_called_once_with(target, **{'enabled': False}) + self.assertIsNone(result) + + +class TestDeleteFirewallRule(TestFirewallRule): + def setUp(self): + super().setUp() + self.mocked = self.network_client.delete_firewall_rule + self.cmd = fwaas_rule.DeleteFirewallRule(self.app, None) + + def test_delete_with_one_resource(self): + target = self.resource['id'] + + def _mock_fwaas(*args, **kwargs): + return sdk_fakes.generate_fake_resource( + firewall_rule.FirewallRule, id=args[0] + ) + + self.network_client.find_firewall_rule.side_effect = _mock_fwaas + + arglist = [target] + verifylist = [(self.res, [target])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with(target) + self.assertIsNone(result) + + def test_delete_with_multiple_resources(self): + + def _mock_fwaas(*args, **kwargs): + return sdk_fakes.generate_fake_resource( + firewall_rule.FirewallRule, id=args[0] + ) + + self.network_client.find_firewall_rule.side_effect = _mock_fwaas + + target1 = 'target1' + target2 = 'target2' + arglist = [target1, target2] + verifylist = [(self.res, [target1, target2])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertEqual(2, self.mocked.call_count) + for idx, reference in enumerate([target1, target2]): + actual = ''.join(self.mocked.call_args_list[idx][0][0]) + self.assertEqual(reference, actual) + + def test_delete_multiple_with_exception(self): + target1 = 'target1' + target2 = 'target2' + arglist = [target1, target2] + verifylist = [(self.res, [target1, target2])] + + def _mock_find(*args, **kwargs): + if args[0] == target2: + raise Exception('Not found') + return self.resource + + self.network_client.find_firewall_rule.side_effect = _mock_find + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) diff --git a/openstackclient/tests/unit/network/v2/taas/__init__.py b/openstackclient/tests/unit/network/v2/taas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/tests/unit/network/v2/taas/test_tap_flow.py b/openstackclient/tests/unit/network/v2/taas/test_tap_flow.py new file mode 100644 index 0000000000..8cd1c3f158 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/taas/test_tap_flow.py @@ -0,0 +1,269 @@ +# All Rights Reserved 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import operator +import uuid + +from openstack.network.v2 import tap_flow as _tap_flow +from openstack.network.v2 import tap_service as _tap_service +from openstack.test import fakes as sdk_fakes +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient.network.v2.taas import tap_flow as osc_tap_flow +from openstackclient.network.v2.taas import tap_service as osc_tap_service +from openstackclient.tests.unit.network.v2 import fakes as network_fakes + + +columns_long = tuple( + col + for col, _, listing_mode in osc_tap_flow._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +headers_long = tuple( + head + for _, head, listing_mode in osc_tap_flow._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +sorted_attr_map = sorted(osc_tap_flow._attr_map, key=operator.itemgetter(1)) +sorted_columns = tuple(col for col, _, _ in sorted_attr_map) +sorted_headers = tuple(head for _, head, _ in sorted_attr_map) + + +def _get_data(attrs, columns=sorted_columns): + return osc_utils.get_dict_properties(attrs, columns) + + +class TestCreateTapFlow(network_fakes.TestNetworkV2): + columns = ( + 'description', + 'direction', + 'id', + 'name', + 'project_id', + 'source_port', + 'status', + 'tap_service_id', + ) + + def setUp(self): + super().setUp() + self.cmd = osc_tap_flow.CreateTapFlow(self.app, None) + + def test_create_tap_flow(self): + """Test Create Tap Flow.""" + fake_tap_service = sdk_fakes.generate_fake_resource( + _tap_service.TapService + ) + port_id = str(uuid.uuid4()) + fake_port = network_fakes.create_one_port(attrs={'id': port_id}) + fake_tap_flow = sdk_fakes.generate_fake_resource( + _tap_flow.TapFlow, + **{ + 'source_port': port_id, + 'tap_service_id': fake_tap_service['id'], + 'direction': 'BOTH', + }, + ) + self.network_client.create_tap_flow.return_value = fake_tap_flow + self.network_client.find_port.return_value = fake_port + self.network_client.find_tap_service.return_value = fake_tap_service + arg_list = [ + '--name', + fake_tap_flow['name'], + '--port', + fake_tap_flow['source_port'], + '--tap-service', + fake_tap_flow['tap_service_id'], + '--direction', + fake_tap_flow['direction'], + ] + + verify_list = [ + ('name', fake_tap_flow['name']), + ('port', fake_tap_flow['source_port']), + ('tap_service', fake_tap_flow['tap_service_id']), + ] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + columns, data = self.cmd.take_action(parsed_args) + self.network_client.create_tap_flow.assert_called_once_with( + **{ + 'name': fake_tap_flow['name'], + 'source_port': fake_tap_flow['source_port'], + 'tap_service_id': fake_tap_flow['tap_service_id'], + 'direction': fake_tap_flow['direction'], + } + ) + self.assertEqual(self.columns, columns) + fake_data = _get_data( + fake_tap_flow, osc_tap_service._get_columns(fake_tap_flow)[1] + ) + self.assertEqual(fake_data, data) + + +class TestListTapFlow(network_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + self.cmd = osc_tap_flow.ListTapFlow(self.app, None) + + def test_list_tap_flows(self): + """Test List Tap Flow.""" + fake_tap_flows = list( + sdk_fakes.generate_fake_resources(_tap_flow.TapFlow, count=2) + ) + self.network_client.tap_flows.return_value = fake_tap_flows + arg_list = [] + verify_list = [] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.tap_flows.assert_called_once() + self.assertEqual(headers, list(headers_long)) + self.assertCountEqual( + list(data), + [ + _get_data(fake_tap_flow, columns_long) + for fake_tap_flow in fake_tap_flows + ], + ) + + +class TestDeleteTapFlow(network_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + self.network_client.find_tap_flow.side_effect = ( + lambda name_or_id, ignore_missing: _tap_flow.TapFlow(id=name_or_id) + ) + self.cmd = osc_tap_flow.DeleteTapFlow(self.app, None) + + def test_delete_tap_flow(self): + """Test Delete tap flow.""" + + fake_tap_flow = sdk_fakes.generate_fake_resource(_tap_flow.TapFlow) + arg_list = [ + fake_tap_flow['id'], + ] + verify_list = [ + (osc_tap_flow.TAP_FLOW, [fake_tap_flow['id']]), + ] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_tap_flow.assert_called_once_with( + fake_tap_flow['id'] + ) + self.assertIsNone(result) + + +class TestShowTapFlow(network_fakes.TestNetworkV2): + columns = ( + 'description', + 'direction', + 'id', + 'name', + 'project_id', + 'source_port', + 'status', + 'tap_service_id', + ) + + def setUp(self): + super().setUp() + self.network_client.find_tap_flow.side_effect = ( + lambda name_or_id, ignore_missing: _tap_flow.TapFlow(id=name_or_id) + ) + self.cmd = osc_tap_flow.ShowTapFlow(self.app, None) + + def test_show_tap_flow(self): + """Test Show tap flow.""" + fake_tap_flow = sdk_fakes.generate_fake_resource(_tap_flow.TapFlow) + self.network_client.get_tap_flow.return_value = fake_tap_flow + arg_list = [ + fake_tap_flow['id'], + ] + verify_list = [ + (osc_tap_flow.TAP_FLOW, fake_tap_flow['id']), + ] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.get_tap_flow.assert_called_once_with( + fake_tap_flow['id'] + ) + self.assertEqual(self.columns, headers) + fake_data = _get_data( + fake_tap_flow, osc_tap_service._get_columns(fake_tap_flow)[1] + ) + self.assertEqual(fake_data, data) + + +class TestUpdateTapFlow(network_fakes.TestNetworkV2): + _new_name = 'new_name' + + # NOTE(mtomaska): The Resource class from which TapFlow inherits from + # returns duplicate `ID and `Name` keys. + columns = ( + 'Direction', + 'ID', + 'ID', + 'Name', + 'Name', + 'Status', + 'Tenant', + 'description', + 'location', + 'project_id', + 'source_port', + 'tap_service_id', + ) + + def setUp(self): + super().setUp() + self.cmd = osc_tap_flow.UpdateTapFlow(self.app, None) + self.network_client.find_tap_flow.side_effect = ( + lambda name_or_id, ignore_missing: _tap_flow.TapFlow(id=name_or_id) + ) + + def test_update_tap_flow(self): + """Test update tap service""" + fake_tap_flow = sdk_fakes.generate_fake_resource(_tap_flow.TapFlow) + new_tap_flow = copy.deepcopy(fake_tap_flow) + new_tap_flow['name'] = self._new_name + + self.network_client.update_tap_flow.return_value = new_tap_flow + + arg_list = [ + fake_tap_flow['id'], + '--name', + self._new_name, + ] + verify_list = [('name', self._new_name)] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + columns, data = self.cmd.take_action(parsed_args) + attrs = {'name': self._new_name} + + self.network_client.update_tap_flow.assert_called_once_with( + new_tap_flow['id'], **attrs + ) + self.assertEqual(self.columns, columns) + self.assertEqual(_get_data(new_tap_flow, self.columns), data) diff --git a/openstackclient/tests/unit/network/v2/taas/test_tap_mirror.py b/openstackclient/tests/unit/network/v2/taas/test_tap_mirror.py new file mode 100644 index 0000000000..1d153b51e9 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/taas/test_tap_mirror.py @@ -0,0 +1,278 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import operator +import uuid + +from openstack.network.v2 import tap_mirror +from openstack.test import fakes as sdk_fakes +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient.network.v2.taas import tap_mirror as osc_tap_mirror +from openstackclient.tests.unit.network.v2 import fakes as network_fakes + + +columns_long = tuple( + col + for col, _, listing_mode in osc_tap_mirror._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +headers_long = tuple( + head + for _, head, listing_mode in osc_tap_mirror._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +sorted_attr_map = sorted(osc_tap_mirror._attr_map, key=operator.itemgetter(1)) +sorted_columns = tuple(col for col, _, _ in sorted_attr_map) +sorted_headers = tuple(head for _, head, _ in sorted_attr_map) + + +def _get_data(attrs, columns=sorted_columns): + return osc_utils.get_dict_properties(attrs, columns) + + +class TestCreateTapMirror(network_fakes.TestNetworkV2): + columns = ( + 'description', + 'directions', + 'id', + 'mirror_type', + 'name', + 'port_id', + 'project_id', + 'remote_ip', + ) + + def setUp(self): + super().setUp() + self.cmd = osc_tap_mirror.CreateTapMirror(self.app, None) + + def test_create_tap_mirror(self): + port_id = str(uuid.uuid4()) + fake_port = network_fakes.create_one_port(attrs={'id': port_id}) + fake_tap_mirror = sdk_fakes.generate_fake_resource( + tap_mirror.TapMirror, **{'port_id': port_id, 'directions': 'IN=99'} + ) + self.network_client.create_tap_mirror.return_value = fake_tap_mirror + self.network_client.find_port.return_value = fake_port + self.network_client.find_tap_mirror.side_effect = ( + lambda _, name_or_id: {'id': name_or_id} + ) + arg_list = [ + '--name', + fake_tap_mirror['name'], + '--port', + fake_tap_mirror['port_id'], + '--directions', + fake_tap_mirror['directions'], + '--remote-ip', + fake_tap_mirror['remote_ip'], + '--mirror-type', + fake_tap_mirror['mirror_type'], + ] + + verify_directions = fake_tap_mirror['directions'].split('=') + verify_directions_dict = {verify_directions[0]: verify_directions[1]} + + verify_list = [ + ('name', fake_tap_mirror['name']), + ('port_id', fake_tap_mirror['port_id']), + ('directions', verify_directions_dict), + ('remote_ip', fake_tap_mirror['remote_ip']), + ('mirror_type', fake_tap_mirror['mirror_type']), + ] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + self.network_client.find_tap_mirror.return_value = fake_tap_mirror + + columns, data = self.cmd.take_action(parsed_args) + self.network_client.create_tap_mirror.assert_called_once_with( + **{ + 'name': fake_tap_mirror['name'], + 'port_id': fake_tap_mirror['port_id'], + 'directions': verify_directions_dict, + 'remote_ip': fake_tap_mirror['remote_ip'], + 'mirror_type': fake_tap_mirror['mirror_type'], + } + ) + self.assertEqual(self.columns, columns) + fake_data = _get_data( + fake_tap_mirror, osc_tap_mirror._get_columns(fake_tap_mirror)[1] + ) + self.assertEqual(fake_data, data) + + +class TestListTapMirror(network_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + self.cmd = osc_tap_mirror.ListTapMirror(self.app, None) + + def test_list_tap_mirror(self): + """Test List Tap Mirror.""" + fake_tap_mirrors = list( + sdk_fakes.generate_fake_resources(tap_mirror.TapMirror, count=4) + ) + self.network_client.tap_mirrors.return_value = fake_tap_mirrors + + arg_list = [] + verify_list = [] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.tap_mirrors.assert_called_once() + self.assertEqual(headers, list(headers_long)) + self.assertCountEqual( + list(data), + [ + _get_data(fake_tap_mirror, columns_long) + for fake_tap_mirror in fake_tap_mirrors + ], + ) + + +class TestDeleteTapMirror(network_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + self.network_client.find_tap_mirror.side_effect = ( + lambda name_or_id, ignore_missing: tap_mirror.TapMirror( + id=name_or_id + ) + ) + self.cmd = osc_tap_mirror.DeleteTapMirror(self.app, None) + + def test_delete_tap_mirror(self): + """Test Delete Tap Mirror.""" + + fake_tap_mirror = sdk_fakes.generate_fake_resource( + tap_mirror.TapMirror + ) + + arg_list = [ + fake_tap_mirror['id'], + ] + verify_list = [ + (osc_tap_mirror.TAP_MIRROR, [fake_tap_mirror['id']]), + ] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_tap_mirror.assert_called_once_with( + fake_tap_mirror['id'] + ) + self.assertIsNone(result) + + +class TestShowTapMirror(network_fakes.TestNetworkV2): + columns = ( + 'description', + 'directions', + 'id', + 'mirror_type', + 'name', + 'port_id', + 'project_id', + 'remote_ip', + ) + + def setUp(self): + super().setUp() + self.network_client.find_tap_mirror.side_effect = ( + lambda name_or_id, ignore_missing: tap_mirror.TapMirror( + id=name_or_id + ) + ) + self.cmd = osc_tap_mirror.ShowTapMirror(self.app, None) + + def test_show_tap_mirror(self): + """Test Show Tap Mirror.""" + + fake_tap_mirror = sdk_fakes.generate_fake_resource( + tap_mirror.TapMirror + ) + self.network_client.get_tap_mirror.return_value = fake_tap_mirror + arg_list = [ + fake_tap_mirror['id'], + ] + verify_list = [ + (osc_tap_mirror.TAP_MIRROR, fake_tap_mirror['id']), + ] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.get_tap_mirror.assert_called_once_with( + fake_tap_mirror['id'] + ) + self.assertEqual(self.columns, headers) + fake_data = _get_data( + fake_tap_mirror, osc_tap_mirror._get_columns(fake_tap_mirror)[1] + ) + self.assertEqual(fake_data, data) + + +class TestUpdateTapMirror(network_fakes.TestNetworkV2): + _new_name = 'new_name' + columns = ( + 'description', + 'directions', + 'id', + 'mirror_type', + 'name', + 'port_id', + 'project_id', + 'remote_ip', + ) + + def setUp(self): + super().setUp() + self.cmd = osc_tap_mirror.UpdateTapMirror(self.app, None) + self.network_client.find_tap_mirror.side_effect = ( + lambda name_or_id, ignore_missing: tap_mirror.TapMirror( + id=name_or_id + ) + ) + + def test_update_tap_mirror(self): + """Test update Tap Mirror""" + fake_tap_mirror = sdk_fakes.generate_fake_resource( + tap_mirror.TapMirror + ) + new_tap_mirror = copy.deepcopy(fake_tap_mirror) + new_tap_mirror['name'] = self._new_name + + self.network_client.update_tap_mirror.return_value = new_tap_mirror + + arg_list = [ + fake_tap_mirror['id'], + '--name', + self._new_name, + ] + verify_list = [('name', self._new_name)] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + columns, data = self.cmd.take_action(parsed_args) + attrs = {'name': self._new_name} + + self.network_client.update_tap_mirror.assert_called_once_with( + fake_tap_mirror['id'], **attrs + ) + self.assertEqual(self.columns, columns) + fake_data = _get_data( + new_tap_mirror, osc_tap_mirror._get_columns(new_tap_mirror)[1] + ) + self.assertEqual(fake_data, data) diff --git a/openstackclient/tests/unit/network/v2/taas/test_tap_service.py b/openstackclient/tests/unit/network/v2/taas/test_tap_service.py new file mode 100644 index 0000000000..dac094f90f --- /dev/null +++ b/openstackclient/tests/unit/network/v2/taas/test_tap_service.py @@ -0,0 +1,261 @@ +# All Rights Reserved 2020 +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import copy +import operator +import uuid + +from openstack.network.v2 import tap_service +from openstack.test import fakes as sdk_fakes +from osc_lib import utils as osc_utils +from osc_lib.utils import columns as column_util + +from openstackclient.network.v2.taas import tap_service as osc_tap_service +from openstackclient.tests.unit.network.v2 import fakes as network_fakes + + +columns_long = tuple( + col + for col, _, listing_mode in osc_tap_service._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +headers_long = tuple( + head + for _, head, listing_mode in osc_tap_service._attr_map + if listing_mode in (column_util.LIST_BOTH, column_util.LIST_LONG_ONLY) +) +sorted_attr_map = sorted(osc_tap_service._attr_map, key=operator.itemgetter(1)) +sorted_columns = tuple(col for col, _, _ in sorted_attr_map) +sorted_headers = tuple(head for _, head, _ in sorted_attr_map) + + +def _get_data(attrs, columns=sorted_columns): + return osc_utils.get_dict_properties(attrs, columns) + + +class TestCreateTapService(network_fakes.TestNetworkV2): + columns = ( + 'description', + 'id', + 'name', + 'port_id', + 'project_id', + 'status', + ) + + def setUp(self): + super().setUp() + self.cmd = osc_tap_service.CreateTapService(self.app, None) + + def test_create_tap_service(self): + """Test Create Tap Service.""" + port_id = str(uuid.uuid4()) + fake_port = network_fakes.create_one_port(attrs={'id': port_id}) + fake_tap_service = sdk_fakes.generate_fake_resource( + tap_service.TapService, **{'port_id': port_id} + ) + self.network_client.create_tap_service.return_value = fake_tap_service + self.network_client.find_port.return_value = fake_port + self.network_client.find_tap_service.side_effect = ( + lambda _, name_or_id: {'id': name_or_id} + ) + arg_list = [ + '--name', + fake_tap_service['name'], + '--port', + fake_tap_service['port_id'], + ] + + verify_list = [ + ('name', fake_tap_service['name']), + ('port_id', fake_tap_service['port_id']), + ] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + self.network_client.find_tap_service.return_value = fake_tap_service + + columns, data = self.cmd.take_action(parsed_args) + self.network_client.create_tap_service.assert_called_once_with( + **{ + 'name': fake_tap_service['name'], + 'port_id': fake_tap_service['port_id'], + } + ) + self.assertEqual(self.columns, columns) + fake_data = _get_data( + fake_tap_service, osc_tap_service._get_columns(fake_tap_service)[1] + ) + self.assertEqual(fake_data, data) + + +class TestListTapService(network_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + self.cmd = osc_tap_service.ListTapService(self.app, None) + + def test_list_tap_service(self): + """Test List Tap Service.""" + fake_tap_services = list( + sdk_fakes.generate_fake_resources(tap_service.TapService, count=4) + ) + self.network_client.tap_services.return_value = fake_tap_services + + arg_list = [] + verify_list = [] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.tap_services.assert_called_once() + self.assertEqual(headers, list(headers_long)) + self.assertCountEqual( + list(data), + [ + _get_data(fake_tap_service, columns_long) + for fake_tap_service in fake_tap_services + ], + ) + + +class TestDeleteTapService(network_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + self.network_client.find_tap_service.side_effect = ( + lambda name_or_id, ignore_missing: tap_service.TapService( + id=name_or_id + ) + ) + self.cmd = osc_tap_service.DeleteTapService(self.app, None) + + def test_delete_tap_service(self): + """Test Delete tap service.""" + + fake_tap_service = sdk_fakes.generate_fake_resource( + tap_service.TapService + ) + + arg_list = [ + fake_tap_service['id'], + ] + verify_list = [ + (osc_tap_service.TAP_SERVICE, [fake_tap_service['id']]), + ] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_tap_service.assert_called_once_with( + fake_tap_service['id'] + ) + self.assertIsNone(result) + + +class TestShowTapService(network_fakes.TestNetworkV2): + columns = ( + 'description', + 'id', + 'name', + 'port_id', + 'project_id', + 'status', + ) + + def setUp(self): + super().setUp() + self.network_client.find_tap_service.side_effect = ( + lambda name_or_id, ignore_missing: tap_service.TapService( + id=name_or_id + ) + ) + self.cmd = osc_tap_service.ShowTapService(self.app, None) + + def test_show_tap_service(self): + """Test Show tap service.""" + + fake_tap_service = sdk_fakes.generate_fake_resource( + tap_service.TapService + ) + self.network_client.get_tap_service.return_value = fake_tap_service + arg_list = [ + fake_tap_service['id'], + ] + verify_list = [ + (osc_tap_service.TAP_SERVICE, fake_tap_service['id']), + ] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.get_tap_service.assert_called_once_with( + fake_tap_service['id'] + ) + self.assertEqual(self.columns, headers) + fake_data = _get_data( + fake_tap_service, osc_tap_service._get_columns(fake_tap_service)[1] + ) + self.assertEqual(fake_data, data) + + +class TestUpdateTapService(network_fakes.TestNetworkV2): + _new_name = 'new_name' + + columns = ( + 'description', + 'id', + 'name', + 'port_id', + 'project_id', + 'status', + ) + + def setUp(self): + super().setUp() + self.cmd = osc_tap_service.UpdateTapService(self.app, None) + self.network_client.find_tap_service.side_effect = ( + lambda name_or_id, ignore_missing: tap_service.TapService( + id=name_or_id + ) + ) + + def test_update_tap_service(self): + """Test update tap service""" + fake_tap_service = sdk_fakes.generate_fake_resource( + tap_service.TapService + ) + new_tap_service = copy.deepcopy(fake_tap_service) + new_tap_service['name'] = self._new_name + + self.network_client.update_tap_service.return_value = new_tap_service + + arg_list = [ + fake_tap_service['id'], + '--name', + self._new_name, + ] + verify_list = [('name', self._new_name)] + + parsed_args = self.check_parser(self.cmd, arg_list, verify_list) + columns, data = self.cmd.take_action(parsed_args) + attrs = {'name': self._new_name} + + self.network_client.update_tap_service.assert_called_once_with( + fake_tap_service['id'], **attrs + ) + self.assertEqual(self.columns, columns) + fake_data = _get_data( + new_tap_service, osc_tap_service._get_columns(new_tap_service)[1] + ) + self.assertEqual(fake_data, data) diff --git a/openstackclient/tests/unit/network/v2/test_address_group.py b/openstackclient/tests/unit/network/v2/test_address_group.py index 411d199237..11d9a2605a 100644 --- a/openstackclient/tests/unit/network/v2/test_address_group.py +++ b/openstackclient/tests/unit/network/v2/test_address_group.py @@ -9,32 +9,22 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network.v2 import address_group -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils -class TestAddressGroup(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - - -class TestCreateAddressGroup(TestAddressGroup): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() +class TestCreateAddressGroup(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) # The new address group created. new_address_group = network_fakes.create_one_address_group( attrs={ @@ -58,15 +48,14 @@ class TestCreateAddressGroup(TestAddressGroup): def setUp(self): super().setUp() - self.network_client.create_address_group = mock.Mock( - return_value=self.new_address_group + self.network_client.create_address_group.return_value = ( + self.new_address_group ) # Get the command object to test self.cmd = address_group.CreateAddressGroup(self.app, None) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project def test_create_no_options(self): arglist = [] @@ -139,13 +128,13 @@ def test_create_all_options(self): self.assertCountEqual(self.data, data) -class TestDeleteAddressGroup(TestAddressGroup): +class TestDeleteAddressGroup(network_fakes.TestNetworkV2): # The address group to delete. _address_groups = network_fakes.create_address_groups(count=2) def setUp(self): super().setUp() - self.network_client.delete_address_group = mock.Mock(return_value=None) + self.network_client.delete_address_group.return_value = None self.network_client.find_address_group = ( network_fakes.get_address_groups( address_groups=self._address_groups @@ -206,9 +195,7 @@ def test_multi_address_groups_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self._address_groups[0], exceptions.CommandError] - self.network_client.find_address_group = mock.Mock( - side_effect=find_mock_result - ) + self.network_client.find_address_group.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -227,7 +214,7 @@ def test_multi_address_groups_delete_with_exception(self): ) -class TestListAddressGroup(TestAddressGroup): +class TestListAddressGroup(network_fakes.TestNetworkV2): # The address groups to list up. address_groups = network_fakes.create_address_groups(count=3) columns = ( @@ -251,9 +238,7 @@ class TestListAddressGroup(TestAddressGroup): def setUp(self): super().setUp() - self.network_client.address_groups = mock.Mock( - return_value=self.address_groups - ) + self.network_client.address_groups.return_value = self.address_groups # Get the command object to test self.cmd = address_group.ListAddressGroup(self.app, None) @@ -269,6 +254,30 @@ def test_address_group_list(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) + def test_address_group_list_pagination(self): + arglist = [ + '--marker', + self.address_groups[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self.address_groups[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.address_groups.assert_called_once_with( + **{ + 'marker': self.address_groups[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_address_group_list_name(self): arglist = [ '--name', @@ -287,8 +296,8 @@ def test_address_group_list_name(self): self.assertCountEqual(self.data, list(data)) def test_address_group_list_project(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -306,8 +315,8 @@ def test_address_group_list_project(self): self.assertCountEqual(self.data, list(data)) def test_address_group_project_domain(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -327,19 +336,21 @@ def test_address_group_project_domain(self): self.assertCountEqual(self.data, list(data)) -class TestSetAddressGroup(TestAddressGroup): +class TestSetAddressGroup(network_fakes.TestNetworkV2): # The address group to set. _address_group = network_fakes.create_one_address_group() def setUp(self): super().setUp() - self.network_client.update_address_group = mock.Mock(return_value=None) - self.network_client.find_address_group = mock.Mock( - return_value=self._address_group + self.network_client.update_address_group.return_value = None + self.network_client.find_address_group.return_value = ( + self._address_group ) - self.network_client.add_addresses_to_address_group = mock.Mock( - return_value=self._address_group + + self.network_client.add_addresses_to_address_group.return_value = ( + self._address_group ) + # Get the command object to test self.cmd = address_group.SetAddressGroup(self.app, None) @@ -422,7 +433,7 @@ def test_set_multiple_addresses(self): self.assertIsNone(result) -class TestShowAddressGroup(TestAddressGroup): +class TestShowAddressGroup(network_fakes.TestNetworkV2): # The address group to show. _address_group = network_fakes.create_one_address_group() columns = ( @@ -442,8 +453,8 @@ class TestShowAddressGroup(TestAddressGroup): def setUp(self): super().setUp() - self.network_client.find_address_group = mock.Mock( - return_value=self._address_group + self.network_client.find_address_group.return_value = ( + self._address_group ) # Get the command object to test @@ -480,18 +491,18 @@ def test_show_all_options(self): self.assertCountEqual(self.data, list(data)) -class TestUnsetAddressGroup(TestAddressGroup): +class TestUnsetAddressGroup(network_fakes.TestNetworkV2): # The address group to unset. _address_group = network_fakes.create_one_address_group() def setUp(self): super().setUp() - self.network_client.find_address_group = mock.Mock( - return_value=self._address_group - ) - self.network_client.remove_addresses_from_address_group = mock.Mock( - return_value=self._address_group + self.network_client.find_address_group.return_value = ( + self._address_group ) + + self.network_client.remove_addresses_from_address_group.return_value = self._address_group + # Get the command object to test self.cmd = address_group.UnsetAddressGroup(self.app, None) @@ -522,7 +533,7 @@ def test_unset_one_address(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.network_client.remove_addresses_from_address_group.assert_called_once_with( # noqa: E501 + self.network_client.remove_addresses_from_address_group.assert_called_once_with( self._address_group, ['10.0.0.2/32'] ) self.assertIsNone(result) @@ -542,7 +553,7 @@ def test_unset_multiple_addresses(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.network_client.remove_addresses_from_address_group.assert_called_once_with( # noqa: E501 + self.network_client.remove_addresses_from_address_group.assert_called_once_with( self._address_group, ['10.0.0.2/32', '2001::/16'] ) self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/test_address_scope.py b/openstackclient/tests/unit/network/v2/test_address_scope.py index c3eb83d060..4f13ef8bed 100644 --- a/openstackclient/tests/unit/network/v2/test_address_scope.py +++ b/openstackclient/tests/unit/network/v2/test_address_scope.py @@ -9,15 +9,15 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network.v2 import address_scope -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -26,15 +26,10 @@ class TestAddressScope(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - class TestCreateAddressScope(TestAddressScope): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) # The new address scope created. new_address_scope = network_fakes.create_one_address_scope( attrs={ @@ -52,15 +47,14 @@ class TestCreateAddressScope(TestAddressScope): def setUp(self): super().setUp() - self.network_client.create_address_scope = mock.Mock( - return_value=self.new_address_scope + self.network_client.create_address_scope.return_value = ( + self.new_address_scope ) # Get the command object to test self.cmd = address_scope.CreateAddressScope(self.app, None) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project def test_create_no_options(self): arglist = [] @@ -160,7 +154,7 @@ class TestDeleteAddressScope(TestAddressScope): def setUp(self): super().setUp() - self.network_client.delete_address_scope = mock.Mock(return_value=None) + self.network_client.delete_address_scope.return_value = None self.network_client.find_address_scope = ( network_fakes.get_address_scopes( address_scopes=self._address_scopes @@ -222,9 +216,7 @@ def test_multi_address_scopes_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self._address_scopes[0], exceptions.CommandError] - self.network_client.find_address_scope = mock.Mock( - side_effect=find_mock_result - ) + self.network_client.find_address_scope.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -267,9 +259,7 @@ class TestListAddressScope(TestAddressScope): def setUp(self): super().setUp() - self.network_client.address_scopes = mock.Mock( - return_value=self.address_scopes - ) + self.network_client.address_scopes.return_value = self.address_scopes # Get the command object to test self.cmd = address_scope.ListAddressScope(self.app, None) @@ -285,6 +275,30 @@ def test_address_scope_list(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_address_scope_list_pagination(self): + arglist = [ + '--marker', + self.address_scopes[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self.address_scopes[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.address_scopes.assert_called_once_with( + **{ + 'marker': self.address_scopes[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_address_scope_list_name(self): arglist = [ '--name', @@ -320,8 +334,8 @@ def test_address_scope_list_ip_version(self): self.assertEqual(self.data, list(data)) def test_address_scope_list_project(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -339,8 +353,8 @@ def test_address_scope_list_project(self): self.assertEqual(self.data, list(data)) def test_address_scope_project_domain(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -398,9 +412,9 @@ class TestSetAddressScope(TestAddressScope): def setUp(self): super().setUp() - self.network_client.update_address_scope = mock.Mock(return_value=None) - self.network_client.find_address_scope = mock.Mock( - return_value=self._address_scope + self.network_client.update_address_scope.return_value = None + self.network_client.find_address_scope.return_value = ( + self._address_scope ) # Get the command object to test @@ -488,8 +502,8 @@ class TestShowAddressScope(TestAddressScope): def setUp(self): super().setUp() - self.network_client.find_address_scope = mock.Mock( - return_value=self._address_scope + self.network_client.find_address_scope.return_value = ( + self._address_scope ) # Get the command object to test diff --git a/openstackclient/tests/unit/network/v2/test_default_security_group_rule.py b/openstackclient/tests/unit/network/v2/test_default_security_group_rule.py index 9b461269ca..7a33479db7 100644 --- a/openstackclient/tests/unit/network/v2/test_default_security_group_rule.py +++ b/openstackclient/tests/unit/network/v2/test_default_security_group_rule.py @@ -9,13 +9,10 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -from unittest import mock from unittest.mock import call import uuid -from openstack.network.v2 import _proxy from openstack.network.v2 import ( default_security_group_rule as _default_security_group_rule, ) @@ -28,18 +25,7 @@ from openstackclient.tests.unit import utils as tests_utils -class TestDefaultSecurityGroupRule(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - self.app.client_manager.sdk_connection = mock.Mock() - self.app.client_manager.sdk_connection.network = mock.Mock( - spec=_proxy.Proxy, - ) - self.sdk_client = self.app.client_manager.sdk_connection.network - - -class TestCreateDefaultSecurityGroupRule(TestDefaultSecurityGroupRule): +class TestCreateDefaultSecurityGroupRule(network_fakes.TestNetworkV2): expected_columns = ( 'description', 'direction', @@ -82,7 +68,7 @@ def _setup_default_security_group_rule(self, attrs=None): **default_security_group_rule_attrs, ) - self.sdk_client.create_default_security_group_rule.return_value = ( + self.network_client.create_default_security_group_rule.return_value = ( self._default_sg_rule ) self.expected_data = ( @@ -208,7 +194,7 @@ def test_create_default_rule(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -252,7 +238,7 @@ def _test_create_protocol_any_helper( columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -302,7 +288,7 @@ def test_create_remote_address_group(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -347,7 +333,7 @@ def test_create_remote_group(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -381,7 +367,7 @@ def test_create_source_group(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -415,7 +401,7 @@ def test_create_source_ip(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -449,7 +435,7 @@ def test_create_remote_ip(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -599,7 +585,7 @@ def test_create_icmp_type(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -637,7 +623,7 @@ def test_create_icmp_type_zero(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -675,7 +661,7 @@ def test_create_icmp_type_greater_than_zero(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -713,7 +699,7 @@ def test_create_icmp_type_negative_value(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -754,7 +740,7 @@ def test_create_ipv6_icmp_type_code(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -794,7 +780,7 @@ def test_create_icmpv6_type(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'direction': self._default_sg_rule.direction, 'ethertype': self._default_sg_rule.ether_type, @@ -825,7 +811,7 @@ def test_create_with_description(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.create_default_security_group_rule.assert_called_once_with( + self.network_client.create_default_security_group_rule.assert_called_once_with( **{ 'description': self._default_sg_rule.description, 'direction': self._default_sg_rule.direction, @@ -840,7 +826,7 @@ def test_create_with_description(self): self.assertEqual(self.expected_data, data) -class TestDeleteDefaultSecurityGroupRule(TestDefaultSecurityGroupRule): +class TestDeleteDefaultSecurityGroupRule(network_fakes.TestNetworkV2): # The default security group rules to be deleted. default_security_group_rule_attrs = { 'direction': 'ingress', @@ -866,7 +852,9 @@ class TestDeleteDefaultSecurityGroupRule(TestDefaultSecurityGroupRule): def setUp(self): super().setUp() - self.sdk_client.delete_default_security_group_rule.return_value = None + self.network_client.delete_default_security_group_rule.return_value = ( + None + ) # Get the command object to test self.cmd = default_security_group_rule.DeleteDefaultSecurityGroupRule( @@ -880,7 +868,7 @@ def test_default_security_group_rule_delete(self): verifylist = [ ('rule', [self._default_sg_rules[0].id]), ] - self.sdk_client.find_default_security_group_rule.return_value = ( + self.network_client.find_default_security_group_rule.return_value = ( self._default_sg_rules[0] ) @@ -888,7 +876,7 @@ def test_default_security_group_rule_delete(self): result = self.cmd.take_action(parsed_args) - self.sdk_client.delete_default_security_group_rule.assert_called_once_with( + self.network_client.delete_default_security_group_rule.assert_called_once_with( self._default_sg_rules[0] ) self.assertIsNone(result) @@ -902,7 +890,7 @@ def test_multi_default_security_group_rules_delete(self): verifylist = [ ('rule', arglist), ] - self.sdk_client.find_default_security_group_rule.side_effect = ( + self.network_client.find_default_security_group_rule.side_effect = ( self._default_sg_rules ) parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -912,7 +900,7 @@ def test_multi_default_security_group_rules_delete(self): calls = [] for s in self._default_sg_rules: calls.append(call(s)) - self.sdk_client.delete_default_security_group_rule.assert_has_calls( + self.network_client.delete_default_security_group_rule.assert_has_calls( calls ) self.assertIsNone(result) @@ -931,8 +919,8 @@ def test_multi_default_security_group_rules_delete_with_exception(self): self._default_sg_rules[0], exceptions.CommandError, ] - self.sdk_client.find_default_security_group_rule = mock.Mock( - side_effect=find_mock_result + self.network_client.find_default_security_group_rule.side_effect = ( + find_mock_result ) try: @@ -941,18 +929,18 @@ def test_multi_default_security_group_rules_delete_with_exception(self): except exceptions.CommandError as e: self.assertEqual('1 of 2 default rules failed to delete.', str(e)) - self.sdk_client.find_default_security_group_rule.assert_any_call( + self.network_client.find_default_security_group_rule.assert_any_call( self._default_sg_rules[0].id, ignore_missing=False ) - self.sdk_client.find_default_security_group_rule.assert_any_call( + self.network_client.find_default_security_group_rule.assert_any_call( 'unexist_rule', ignore_missing=False ) - self.sdk_client.delete_default_security_group_rule.assert_called_once_with( + self.network_client.delete_default_security_group_rule.assert_called_once_with( self._default_sg_rules[0] ) -class TestListDefaultSecurityGroupRule(TestDefaultSecurityGroupRule): +class TestListDefaultSecurityGroupRule(network_fakes.TestNetworkV2): # The security group rule to be listed. _default_sg_rule_tcp = sdk_fakes.generate_fake_resource( _default_security_group_rule.DefaultSecurityGroupRule, @@ -1001,7 +989,7 @@ class TestListDefaultSecurityGroupRule(TestDefaultSecurityGroupRule): def setUp(self): super().setUp() - self.sdk_client.default_security_group_rules.return_value = ( + self.network_client.default_security_group_rules.return_value = ( self._default_sg_rules ) @@ -1016,7 +1004,7 @@ def test_list_default(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.default_security_group_rules.assert_called_once_with( + self.network_client.default_security_group_rules.assert_called_once_with( **{} ) self.assertEqual(self.expected_columns, columns) @@ -1035,7 +1023,7 @@ def test_list_with_protocol(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.default_security_group_rules.assert_called_once_with( + self.network_client.default_security_group_rules.assert_called_once_with( **{ 'protocol': 'tcp', } @@ -1055,7 +1043,7 @@ def test_list_with_ingress(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.default_security_group_rules.assert_called_once_with( + self.network_client.default_security_group_rules.assert_called_once_with( **{ 'direction': 'ingress', } @@ -1075,7 +1063,7 @@ def test_list_with_wrong_egress(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.default_security_group_rules.assert_called_once_with( + self.network_client.default_security_group_rules.assert_called_once_with( **{ 'direction': 'egress', } @@ -1084,7 +1072,7 @@ def test_list_with_wrong_egress(self): self.assertEqual(self.expected_data, list(data)) -class TestShowDefaultSecurityGroupRule(TestDefaultSecurityGroupRule): +class TestShowDefaultSecurityGroupRule(network_fakes.TestNetworkV2): # The default security group rule to be shown. _default_sg_rule = sdk_fakes.generate_fake_resource( _default_security_group_rule.DefaultSecurityGroupRule @@ -1123,7 +1111,7 @@ class TestShowDefaultSecurityGroupRule(TestDefaultSecurityGroupRule): def setUp(self): super().setUp() - self.sdk_client.find_default_security_group_rule.return_value = ( + self.network_client.find_default_security_group_rule.return_value = ( self._default_sg_rule ) @@ -1148,7 +1136,7 @@ def test_show_all_options(self): columns, data = self.cmd.take_action(parsed_args) - self.sdk_client.find_default_security_group_rule.assert_called_once_with( + self.network_client.find_default_security_group_rule.assert_called_once_with( self._default_sg_rule.id, ignore_missing=False ) self.assertEqual(self.columns, columns) diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py b/openstackclient/tests/unit/network/v2/test_floating_ip.py similarity index 91% rename from openstackclient/tests/unit/network/v2/test_floating_ip_network.py rename to openstackclient/tests/unit/network/v2/test_floating_ip.py index d7d0b7799c..6c224d1317 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip.py @@ -9,18 +9,17 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project from openstack.network.v2 import floating_ip as _floating_ip from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from openstackclient.network.v2 import floating_ip as fip -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -29,11 +28,6 @@ class TestFloatingIPNetwork(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - class TestCreateFloatingIPNetwork(TestFloatingIPNetwork): # Fake data for option tests. @@ -86,16 +80,14 @@ class TestCreateFloatingIPNetwork(TestFloatingIPNetwork): def setUp(self): super().setUp() - self.network_client.create_ip = mock.Mock( - return_value=self.floating_ip - ) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.create_ip.return_value = self.floating_ip - self.network_client.find_network = mock.Mock( - return_value=self.floating_network - ) - self.network_client.find_subnet = mock.Mock(return_value=self.subnet) - self.network_client.find_port = mock.Mock(return_value=self.port) + self.network_client.set_tags.return_value = None + + self.network_client.find_network.return_value = self.floating_network + + self.network_client.find_subnet.return_value = self.subnet + self.network_client.find_port.return_value = self.port # Get the command object to test self.cmd = fip.CreateFloatingIP(self.app, None) @@ -179,8 +171,8 @@ def test_create_all_options(self): self.assertEqual(self.data, data) def test_floating_ip_create_project(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -204,9 +196,9 @@ def test_floating_ip_create_project(self): self.assertEqual(self.data, data) def test_floating_ip_create_project_domain(self): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) + self.identity_sdk_client.find_project.return_value = project arglist = [ "--project", project.name, @@ -304,7 +296,7 @@ class TestDeleteFloatingIPNetwork(TestFloatingIPNetwork): def setUp(self): super().setUp() - self.network_client.delete_ip = mock.Mock(return_value=None) + self.network_client.delete_ip.return_value = None # Get the command object to test self.cmd = fip.DeleteFloatingIP(self.app, None) @@ -386,7 +378,7 @@ def test_floating_ip_delete_multi_exception(self): self.cmd.take_action(parsed_args) self.fail('CommandError should be raised.') except exceptions.CommandError as e: - self.assertEqual('1 of 2 floating_ips failed to delete.', str(e)) + self.assertEqual('1 of 2 floating IPs failed to delete.', str(e)) self.network_client.find_ip.assert_any_call( self.floating_ips[0].id, @@ -428,7 +420,8 @@ class TestListFloatingIPNetwork(TestFloatingIPNetwork): 'Floating Network', 'Project', ) - columns_long = columns + ( + columns_long = ( + *columns, 'Router', 'Status', 'Description', @@ -470,14 +463,11 @@ class TestListFloatingIPNetwork(TestFloatingIPNetwork): def setUp(self): super().setUp() - self.network_client.ips = mock.Mock(return_value=self.floating_ips) - self.network_client.find_network = mock.Mock( - return_value=self.fake_network - ) - self.network_client.find_port = mock.Mock(return_value=self.fake_port) - self.network_client.find_router = mock.Mock( - return_value=self.fake_router - ) + self.network_client.ips.return_value = self.floating_ips + self.network_client.find_network.return_value = self.fake_network + + self.network_client.find_port.return_value = self.fake_port + self.network_client.find_router.return_value = self.fake_router # Get the command object to test self.cmd = fip.ListFloatingIP(self.app, None) @@ -499,7 +489,7 @@ def test_floating_ip_list_network(self): 'fake_network_id', ] verifylist = [ - ('network', 'fake_network_id'), + ('networks', ['fake_network_id']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -507,7 +497,7 @@ def test_floating_ip_list_network(self): self.network_client.ips.assert_called_once_with( **{ - 'floating_network_id': 'fake_network_id', + 'floating_network_id': ['fake_network_id'], } ) self.assertEqual(self.columns, columns) @@ -519,7 +509,7 @@ def test_floating_ip_list_port(self): 'fake_port_id', ] verifylist = [ - ('port', 'fake_port_id'), + ('ports', ['fake_port_id']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -527,7 +517,7 @@ def test_floating_ip_list_port(self): self.network_client.ips.assert_called_once_with( **{ - 'port_id': 'fake_port_id', + 'port_id': ['fake_port_id'], } ) self.assertEqual(self.columns, columns) @@ -613,8 +603,8 @@ def test_floating_ip_list_status(self): self.assertEqual(self.data_long, list(data)) def test_floating_ip_list_project(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -633,8 +623,8 @@ def test_floating_ip_list_project(self): self.assertEqual(self.data, list(data)) def test_floating_ip_list_project_domain(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -660,7 +650,7 @@ def test_floating_ip_list_router(self): '--long', ] verifylist = [ - ('router', 'fake_router_id'), + ('routers', ['fake_router_id']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -668,7 +658,7 @@ def test_floating_ip_list_router(self): self.network_client.ips.assert_called_once_with( **{ - 'router_id': 'fake_router_id', + 'router_id': ['fake_router_id'], } ) self.assertEqual(self.columns_long, columns) @@ -705,6 +695,30 @@ def test_list_with_tag_options(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_floating_ip_list_pagination(self): + arglist = [ + '--marker', + self.floating_ips[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self.floating_ips[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.ips.assert_called_once_with( + **{ + 'marker': self.floating_ips[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestShowFloatingIPNetwork(TestFloatingIPNetwork): def setUp(self): @@ -713,7 +727,7 @@ def setUp(self): self.floating_ip = sdk_fakes.generate_fake_resource( _floating_ip.FloatingIP ) - self.network_client.find_ip = mock.Mock(return_value=self.floating_ip) + self.network_client.find_ip.return_value = self.floating_ip self.columns = ( 'created_at', @@ -797,10 +811,10 @@ class TestSetFloatingIP(TestFloatingIPNetwork): def setUp(self): super().setUp() - self.network_client.find_ip = mock.Mock(return_value=self.floating_ip) - self.network_client.find_port = mock.Mock(return_value=self.port) - self.network_client.update_ip = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.find_ip.return_value = self.floating_ip + self.network_client.find_port.return_value = self.port + self.network_client.update_ip.return_value = None + self.network_client.set_tags.return_value = None # Get the command object to test self.cmd = fip.SetFloatingIP(self.app, None) @@ -1044,9 +1058,9 @@ class TestUnsetFloatingIP(TestFloatingIPNetwork): def setUp(self): super().setUp() - self.network_client.find_ip = mock.Mock(return_value=self.floating_ip) - self.network_client.update_ip = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.find_ip.return_value = self.floating_ip + self.network_client.update_ip.return_value = None + self.network_client.set_tags.return_value = None # Get the command object to test self.cmd = fip.UnsetFloatingIP(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_compute.py b/openstackclient/tests/unit/network/v2/test_floating_ip_compute.py deleted file mode 100644 index 89137d6e98..0000000000 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_compute.py +++ /dev/null @@ -1,248 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -from unittest import mock - -from osc_lib import exceptions - -from openstackclient.api import compute_v2 -from openstackclient.network.v2 import floating_ip as fip -from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -from openstackclient.tests.unit import utils as tests_utils - - -@mock.patch.object(compute_v2, 'create_floating_ip') -class TestCreateFloatingIPCompute(compute_fakes.TestComputev2): - _floating_ip = compute_fakes.create_one_floating_ip() - - columns = ( - 'fixed_ip', - 'id', - 'instance_id', - 'ip', - 'pool', - ) - - data = ( - _floating_ip['fixed_ip'], - _floating_ip['id'], - _floating_ip['instance_id'], - _floating_ip['ip'], - _floating_ip['pool'], - ) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - self.cmd = fip.CreateFloatingIP(self.app, None) - - def test_floating_ip_create_no_arg(self, fip_mock): - arglist = [] - verifylist = [] - - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - verifylist, - ) - - def test_floating_ip_create_default(self, fip_mock): - fip_mock.return_value = self._floating_ip - arglist = [ - self._floating_ip['pool'], - ] - verifylist = [ - ('network', self._floating_ip['pool']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - fip_mock.assert_called_once_with( - self.compute_client, self._floating_ip['pool'] - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - -@mock.patch.object(compute_v2, 'delete_floating_ip') -class TestDeleteFloatingIPCompute(compute_fakes.TestComputev2): - _floating_ips = compute_fakes.create_floating_ips(count=2) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - self.cmd = fip.DeleteFloatingIP(self.app, None) - - def test_floating_ip_delete(self, fip_mock): - fip_mock.return_value = mock.Mock(return_value=None) - arglist = [ - self._floating_ips[0]['id'], - ] - verifylist = [ - ('floating_ip', [self._floating_ips[0]['id']]), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - fip_mock.assert_called_once_with( - self.compute_client, self._floating_ips[0]['id'] - ) - self.assertIsNone(result) - - def test_floating_ip_delete_multi(self, fip_mock): - fip_mock.return_value = mock.Mock(return_value=None) - arglist = [ - self._floating_ips[0]['id'], - self._floating_ips[1]['id'], - ] - verifylist = [ - ('floating_ip', arglist), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - fip_mock.assert_has_calls( - [ - mock.call(self.compute_client, self._floating_ips[0]['id']), - mock.call(self.compute_client, self._floating_ips[1]['id']), - ] - ) - self.assertIsNone(result) - - def test_floating_ip_delete_multi_exception(self, fip_mock): - fip_mock.return_value = mock.Mock(return_value=None) - fip_mock.side_effect = [ - mock.Mock(return_value=None), - exceptions.CommandError, - ] - arglist = [ - self._floating_ips[0]['id'], - 'unexist_floating_ip', - ] - verifylist = [ - ( - 'floating_ip', - [self._floating_ips[0]['id'], 'unexist_floating_ip'], - ) - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('1 of 2 floating_ips failed to delete.', str(e)) - - fip_mock.assert_any_call( - self.compute_client, self._floating_ips[0]['id'] - ) - fip_mock.assert_any_call(self.compute_client, 'unexist_floating_ip') - - -@mock.patch.object(compute_v2, 'list_floating_ips') -class TestListFloatingIPCompute(compute_fakes.TestComputev2): - _floating_ips = compute_fakes.create_floating_ips(count=3) - - columns = ( - 'ID', - 'Floating IP Address', - 'Fixed IP Address', - 'Server', - 'Pool', - ) - - data = [] - for ip in _floating_ips: - data.append( - ( - ip['id'], - ip['ip'], - ip['fixed_ip'], - ip['instance_id'], - ip['pool'], - ) - ) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - self.cmd = fip.ListFloatingIP(self.app, None) - - def test_floating_ip_list(self, fip_mock): - fip_mock.return_value = self._floating_ips - arglist = [] - verifylist = [] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - fip_mock.assert_called_once_with(self.compute_client) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - -@mock.patch.object(compute_v2, 'get_floating_ip') -class TestShowFloatingIPCompute(compute_fakes.TestComputev2): - _floating_ip = compute_fakes.create_one_floating_ip() - - columns = ( - 'fixed_ip', - 'id', - 'instance_id', - 'ip', - 'pool', - ) - - data = ( - _floating_ip['fixed_ip'], - _floating_ip['id'], - _floating_ip['instance_id'], - _floating_ip['ip'], - _floating_ip['pool'], - ) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - self.cmd = fip.ShowFloatingIP(self.app, None) - - def test_floating_ip_show(self, fip_mock): - fip_mock.return_value = self._floating_ip - arglist = [ - self._floating_ip['id'], - ] - verifylist = [ - ('floating_ip', self._floating_ip['id']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - fip_mock.assert_called_once_with( - self.compute_client, self._floating_ip['id'] - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_pool_compute.py b/openstackclient/tests/unit/network/v2/test_floating_ip_pool_compute.py deleted file mode 100644 index 90ded06280..0000000000 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_pool_compute.py +++ /dev/null @@ -1,49 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -from unittest import mock - -from openstackclient.api import compute_v2 -from openstackclient.network.v2 import floating_ip_pool -from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes - - -@mock.patch.object(compute_v2, 'list_floating_ip_pools') -class TestListFloatingIPPoolCompute(compute_fakes.TestComputev2): - # The floating ip pools to list up - _floating_ip_pools = compute_fakes.create_floating_ip_pools(count=3) - - columns = ('Name',) - - data = [] - for pool in _floating_ip_pools: - data.append((pool['name'],)) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - self.cmd = floating_ip_pool.ListFloatingIPPool(self.app, None) - - def test_floating_ip_list(self, fipp_mock): - fipp_mock.return_value = self._floating_ip_pools - arglist = [] - verifylist = [] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - fipp_mock.assert_called_once_with(self.compute_client) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_pool_network.py b/openstackclient/tests/unit/network/v2/test_floating_ip_pool_network.py deleted file mode 100644 index fee8f43907..0000000000 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_pool_network.py +++ /dev/null @@ -1,39 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -from osc_lib import exceptions - -from openstackclient.network.v2 import floating_ip_pool -from openstackclient.tests.unit.network.v2 import fakes as network_fakes - - -class TestFloatingIPPoolNetwork(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - -class TestListFloatingIPPoolNetwork(TestFloatingIPPoolNetwork): - def setUp(self): - super().setUp() - - # Get the command object to test - self.cmd = floating_ip_pool.ListFloatingIPPool(self.app, None) - - def test_floating_ip_list(self): - arglist = [] - verifylist = [] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.assertRaises( - exceptions.CommandError, self.cmd.take_action, parsed_args - ) diff --git a/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py b/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py index 2e4c93f8a5..3df01db9dd 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip_port_forwarding.py @@ -16,10 +16,11 @@ from unittest import mock from unittest.mock import call +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network.v2 import floating_ip_port_forwarding -from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes_v2 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -32,21 +33,21 @@ def setUp(self): network_fakes.FakeFloatingIP.create_one_floating_ip() ) self.port = network_fakes.create_one_port() - self.project = identity_fakes_v2.FakeProject.create_one_project() - self.network_client.find_port = mock.Mock(return_value=self.port) + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.network_client.find_port.return_value = self.port class TestCreateFloatingIPPortForwarding(TestFloatingIPPortForwarding): def setUp(self): super().setUp() - self.new_port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( # noqa: E501 + self.new_port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( attrs={ 'internal_port_id': self.port.id, 'floatingip_id': self.floating_ip.id, } ) - self.new_port_forwarding_with_ranges = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( # noqa: E501 + self.new_port_forwarding_with_ranges = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( use_range=True, attrs={ 'internal_port_id': self.port.id, @@ -54,11 +55,11 @@ def setUp(self): }, ) - self.network_client.create_floating_ip_port_forwarding = mock.Mock( - return_value=self.new_port_forwarding + self.network_client.create_floating_ip_port_forwarding.return_value = ( + self.new_port_forwarding ) - self.network_client.find_ip = mock.Mock(return_value=self.floating_ip) + self.network_client.find_ip.return_value = self.floating_ip # Get the command object to test self.cmd = floating_ip_port_forwarding.CreateFloatingIPPortForwarding( @@ -144,15 +145,15 @@ def test_create_all_options_with_range(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network_client.create_floating_ip_port_forwarding.assert_called_once_with( # noqa: E501 + self.network_client.create_floating_ip_port_forwarding.assert_called_once_with( self.new_port_forwarding.floatingip_id, **{ - 'external_port_range': self.new_port_forwarding_with_ranges.external_port_range, # noqa: E501 - 'internal_ip_address': self.new_port_forwarding_with_ranges.internal_ip_address, # noqa: E501 - 'internal_port_range': self.new_port_forwarding_with_ranges.internal_port_range, # noqa: E501 - 'internal_port_id': self.new_port_forwarding_with_ranges.internal_port_id, # noqa: E501 + 'external_port_range': self.new_port_forwarding_with_ranges.external_port_range, + 'internal_ip_address': self.new_port_forwarding_with_ranges.internal_ip_address, + 'internal_port_range': self.new_port_forwarding_with_ranges.internal_port_range, + 'internal_port_id': self.new_port_forwarding_with_ranges.internal_port_id, 'protocol': self.new_port_forwarding_with_ranges.protocol, - 'description': self.new_port_forwarding_with_ranges.description, # noqa: E501 + 'description': self.new_port_forwarding_with_ranges.description, }, ) self.assertEqual(self.columns, columns) @@ -325,11 +326,11 @@ def test_create_all_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network_client.create_floating_ip_port_forwarding.assert_called_once_with( # noqa: E501 + self.network_client.create_floating_ip_port_forwarding.assert_called_once_with( self.new_port_forwarding.floatingip_id, **{ 'external_port': self.new_port_forwarding.external_port, - 'internal_ip_address': self.new_port_forwarding.internal_ip_address, # noqa: E501 + 'internal_ip_address': self.new_port_forwarding.internal_ip_address, 'internal_port': self.new_port_forwarding.internal_port, 'internal_port_id': self.new_port_forwarding.internal_port_id, 'protocol': self.new_port_forwarding.protocol, @@ -351,11 +352,11 @@ def setUp(self): }, ) ) - self.network_client.delete_floating_ip_port_forwarding = mock.Mock( - return_value=None + self.network_client.delete_floating_ip_port_forwarding.return_value = ( + None ) - self.network_client.find_ip = mock.Mock(return_value=self.floating_ip) + self.network_client.find_ip.return_value = self.floating_ip # Get the command object to test self.cmd = floating_ip_port_forwarding.DeleteFloatingIPPortForwarding( self.app, None @@ -375,7 +376,7 @@ def test_port_forwarding_delete(self): result = self.cmd.take_action(parsed_args) - self.network_client.delete_floating_ip_port_forwarding.assert_called_once_with( # noqa: E501 + self.network_client.delete_floating_ip_port_forwarding.assert_called_once_with( self.floating_ip.id, self._port_forwarding[0].id, ignore_missing=False, @@ -490,10 +491,11 @@ def setUp(self): port_forwarding.description, ) ) - self.network_client.floating_ip_port_forwardings = mock.Mock( - return_value=self.port_forwardings + self.network_client.floating_ip_port_forwardings.return_value = ( + self.port_forwardings ) - self.network_client.find_ip = mock.Mock(return_value=self.floating_ip) + + self.network_client.find_ip.return_value = self.floating_ip # Get the command object to test self.cmd = floating_ip_port_forwarding.ListFloatingIPPortForwarding( self.app, None @@ -512,6 +514,30 @@ def test_port_forwarding_list(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_port_forwarding_list_pagination(self): + arglist = [ + '--marker', + self.port_forwardings[0].id, + '--limit', + '1', + self.floating_ip.id, + ] + verifylist = [ + ('marker', self.port_forwardings[0].id), + ('limit', 1), + ('floating_ip', self.floating_ip.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.floating_ip_port_forwardings.assert_called_once_with( + self.floating_ip, + **{'marker': self.port_forwardings[0].id, 'limit': 1}, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_port_forwarding_list_all_options(self): arglist = [ '--port', @@ -552,19 +578,20 @@ class TestSetFloatingIPPortForwarding(TestFloatingIPPortForwarding): # The Port Forwarding to set. def setUp(self): super().setUp() - self._port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( # noqa: E501 + self._port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( attrs={ 'floatingip_id': self.floating_ip.id, } ) - self.network_client.update_floating_ip_port_forwarding = mock.Mock( - return_value=None + self.network_client.update_floating_ip_port_forwarding.return_value = ( + None ) - self.network_client.find_floating_ip_port_forwarding = mock.Mock( - return_value=self._port_forwarding + self.network_client.find_floating_ip_port_forwarding.return_value = ( + self._port_forwarding ) - self.network_client.find_ip = mock.Mock(return_value=self.floating_ip) + + self.network_client.find_ip.return_value = self.floating_ip # Get the command object to test self.cmd = floating_ip_port_forwarding.SetFloatingIPPortForwarding( self.app, None @@ -673,7 +700,7 @@ class TestShowFloatingIPPortForwarding(TestFloatingIPPortForwarding): def setUp(self): super().setUp() - self._port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( # noqa: E501 + self._port_forwarding = network_fakes.FakeFloatingIPPortForwarding.create_one_port_forwarding( attrs={ 'floatingip_id': self.floating_ip.id, } @@ -690,10 +717,11 @@ def setUp(self): self._port_forwarding.internal_port_range, self._port_forwarding.protocol, ) - self.network_client.find_floating_ip_port_forwarding = mock.Mock( - return_value=self._port_forwarding + self.network_client.find_floating_ip_port_forwarding.return_value = ( + self._port_forwarding ) - self.network_client.find_ip = mock.Mock(return_value=self.floating_ip) + + self.network_client.find_ip.return_value = self.floating_ip # Get the command object to test self.cmd = floating_ip_port_forwarding.ShowFloatingIPPortForwarding( self.app, None diff --git a/openstackclient/tests/unit/network/v2/test_ip_availability.py b/openstackclient/tests/unit/network/v2/test_ip_availability.py index 4224d12666..31e9186b80 100644 --- a/openstackclient/tests/unit/network/v2/test_ip_availability.py +++ b/openstackclient/tests/unit/network/v2/test_ip_availability.py @@ -9,30 +9,20 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# - -from unittest import mock +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from openstackclient.network.v2 import ip_availability -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils -class TestIPAvailability(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - - self.project = identity_fakes.FakeProject.create_one_project() - self.projects_mock.get.return_value = self.project - - -class TestListIPAvailability(TestIPAvailability): +class TestListIPAvailability(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) _ip_availability = network_fakes.create_ip_availability(count=3) columns = ( 'Network ID', @@ -54,11 +44,13 @@ class TestListIPAvailability(TestIPAvailability): def setUp(self): super().setUp() - self.cmd = ip_availability.ListIPAvailability(self.app, None) - self.network_client.network_ip_availabilities = mock.Mock( - return_value=self._ip_availability + self.identity_sdk_client.find_project.return_value = self.project + self.network_client.network_ip_availabilities.return_value = ( + self._ip_availability ) + self.cmd = ip_availability.ListIPAvailability(self.app, None) + def test_list_no_options(self): arglist = [] verifylist = [] @@ -74,6 +66,31 @@ def test_list_no_options(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) + def test_list_pagination(self): + arglist = [ + '--marker', + self._ip_availability[0].network_id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self._ip_availability[0].network_id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.network_ip_availabilities.assert_called_once_with( + **{ + 'ip_version': 4, + 'marker': self._ip_availability[0].network_id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_list_ip_version(self): arglist = [ '--ip-version', @@ -108,7 +125,7 @@ def test_list_project(self): self.assertCountEqual(self.data, list(data)) -class TestShowIPAvailability(TestIPAvailability): +class TestShowIPAvailability(network_fakes.TestNetworkV2): _network = network_fakes.create_one_network() _ip_availability = network_fakes.create_one_ip_availability( attrs={'network_id': _network.id} @@ -134,13 +151,12 @@ class TestShowIPAvailability(TestIPAvailability): def setUp(self): super().setUp() - self.network_client.find_network_ip_availability = mock.Mock( - return_value=self._ip_availability - ) - self.network_client.find_network = mock.Mock( - return_value=self._network + self.network_client.find_network_ip_availability.return_value = ( + self._ip_availability ) + self.network_client.find_network.return_value = self._network + # Get the command object to test self.cmd = ip_availability.ShowIPAvailability(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py b/openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py index b358024ffd..a6a0c648b2 100644 --- a/openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py +++ b/openstackclient/tests/unit/network/v2/test_l3_conntrack_helper.py @@ -9,9 +9,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# - -from unittest import mock from osc_lib import exceptions @@ -46,8 +43,8 @@ def setUp(self): self.ct_helper.protocol, self.ct_helper.router_id, ) - self.network_client.create_conntrack_helper = mock.Mock( - return_value=self.ct_helper + self.network_client.create_conntrack_helper.return_value = ( + self.ct_helper ) # Get the command object to test @@ -119,9 +116,7 @@ def setUp(self): attrs ) ) - self.network_client.delete_conntrack_helper = mock.Mock( - return_value=None - ) + self.network_client.delete_conntrack_helper.return_value = None # Get the command object to test self.cmd = l3_conntrack_helper.DeleteConntrackHelper(self.app, None) @@ -158,7 +153,7 @@ class TestListL3ConntrackHelper(TestConntrackHelper): def setUp(self): super().setUp() attrs = {'router_id': self.router.id} - ct_helpers = ( + self.conntrack_helpers = ( network_fakes.FakeL3ConntrackHelper.create_l3_conntrack_helpers( attrs, count=3 ) @@ -171,7 +166,7 @@ def setUp(self): 'Port', ) self.data = [] - for ct_helper in ct_helpers: + for ct_helper in self.conntrack_helpers: self.data.append( ( ct_helper.id, @@ -181,8 +176,8 @@ def setUp(self): ct_helper.port, ) ) - self.network_client.conntrack_helpers = mock.Mock( - return_value=ct_helpers + self.network_client.conntrack_helpers.return_value = ( + self.conntrack_helpers ) # Get the command object to test @@ -206,6 +201,30 @@ def test_conntrack_helpers_list(self): for index in range(len(list_data)): self.assertEqual(self.data[index], list_data[index]) + def test_conntrack_helpers_list_pagination(self): + arglist = [ + '--marker', + self.conntrack_helpers[0].id, + '--limit', + '1', + self.router.id, + ] + verifylist = [ + ('marker', self.conntrack_helpers[0].id), + ('limit', 1), + ('router', self.router.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.conntrack_helpers.assert_called_once_with( + self.router.id, + **{'marker': self.conntrack_helpers[0].id, 'limit': 1}, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestSetL3ConntrackHelper(TestConntrackHelper): def setUp(self): @@ -216,9 +235,7 @@ def setUp(self): attrs ) ) - self.network_client.update_conntrack_helper = mock.Mock( - return_value=None - ) + self.network_client.update_conntrack_helper.return_value = None # Get the command object to test self.cmd = l3_conntrack_helper.SetConntrackHelper(self.app, None) @@ -281,9 +298,7 @@ def setUp(self): self.ct_helper.protocol, self.ct_helper.router_id, ) - self.network_client.get_conntrack_helper = mock.Mock( - return_value=self.ct_helper - ) + self.network_client.get_conntrack_helper.return_value = self.ct_helper # Get the command object to test self.cmd = l3_conntrack_helper.ShowConntrackHelper(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_local_ip.py b/openstackclient/tests/unit/network/v2/test_local_ip.py index cf4105b02d..58b6bc56cf 100644 --- a/openstackclient/tests/unit/network/v2/test_local_ip.py +++ b/openstackclient/tests/unit/network/v2/test_local_ip.py @@ -11,32 +11,22 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network.v2 import local_ip -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils -class TestLocalIP(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - - -class TestCreateLocalIP(TestLocalIP): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() +class TestCreateLocalIP(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) local_ip_network = network_fakes.create_one_network() port = network_fakes.create_one_port() # The new local ip created. @@ -77,19 +67,16 @@ class TestCreateLocalIP(TestLocalIP): def setUp(self): super().setUp() - self.network_client.create_local_ip = mock.Mock( - return_value=self.new_local_ip - ) - self.network_client.find_network = mock.Mock( - return_value=self.local_ip_network - ) - self.network_client.find_port = mock.Mock(return_value=self.port) + self.network_client.create_local_ip.return_value = self.new_local_ip + + self.network_client.find_network.return_value = self.local_ip_network + + self.network_client.find_port.return_value = self.port # Get the command object to test self.cmd = local_ip.CreateLocalIP(self.app, None) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project def test_create_no_options(self): parsed_args = self.check_parser(self.cmd, [], []) @@ -143,13 +130,13 @@ def test_create_all_options(self): self.assertCountEqual(self.data, data) -class TestDeleteLocalIP(TestLocalIP): +class TestDeleteLocalIP(network_fakes.TestNetworkV2): # The local ip to delete. _local_ips = network_fakes.create_local_ips(count=2) def setUp(self): super().setUp() - self.network_client.delete_local_ip = mock.Mock(return_value=None) + self.network_client.delete_local_ip.return_value = None self.network_client.find_local_ip = network_fakes.get_local_ips( local_ips=self._local_ips ) @@ -205,9 +192,7 @@ def test_multi_local_ips_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self._local_ips[0], exceptions.CommandError] - self.network_client.find_local_ip = mock.Mock( - side_effect=find_mock_result - ) + self.network_client.find_local_ip.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -226,7 +211,7 @@ def test_multi_local_ips_delete_with_exception(self): ) -class TestListLocalIP(TestLocalIP): +class TestListLocalIP(network_fakes.TestNetworkV2): # The local ip to list up. local_ips = network_fakes.create_local_ips(count=3) fake_network = network_fakes.create_one_network({'id': 'fake_network_id'}) @@ -258,10 +243,8 @@ class TestListLocalIP(TestLocalIP): def setUp(self): super().setUp() - self.network_client.local_ips = mock.Mock(return_value=self.local_ips) - self.network_client.find_network = mock.Mock( - return_value=self.fake_network - ) + self.network_client.local_ips.return_value = self.local_ips + self.network_client.find_network.return_value = self.fake_network # Get the command object to test self.cmd = local_ip.ListLocalIP(self.app, None) @@ -295,8 +278,8 @@ def test_local_ip_list_name(self): self.assertCountEqual(self.data, list(data)) def test_local_ip_list_project(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -314,8 +297,8 @@ def test_local_ip_list_project(self): self.assertCountEqual(self.data, list(data)) def test_local_ip_project_domain(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -396,16 +379,14 @@ def test_local_ip_list_ip_mode(self): self.assertEqual(self.data, list(data)) -class TestSetLocalIP(TestLocalIP): +class TestSetLocalIP(network_fakes.TestNetworkV2): # The local ip to set. _local_ip = network_fakes.create_one_local_ip() def setUp(self): super().setUp() - self.network_client.update_local_ip = mock.Mock(return_value=None) - self.network_client.find_local_ip = mock.Mock( - return_value=self._local_ip - ) + self.network_client.update_local_ip.return_value = None + self.network_client.find_local_ip.return_value = self._local_ip # Get the command object to test self.cmd = local_ip.SetLocalIP(self.app, None) @@ -450,7 +431,7 @@ def test_set_name_and_description(self): self.assertIsNone(result) -class TestShowLocalIP(TestLocalIP): +class TestShowLocalIP(network_fakes.TestNetworkV2): # The local ip to show. _local_ip = network_fakes.create_one_local_ip() columns = ( @@ -482,9 +463,7 @@ class TestShowLocalIP(TestLocalIP): def setUp(self): super().setUp() - self.network_client.find_local_ip = mock.Mock( - return_value=self._local_ip - ) + self.network_client.find_local_ip.return_value = self._local_ip # Get the command object to test self.cmd = local_ip.ShowLocalIP(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_local_ip_association.py b/openstackclient/tests/unit/network/v2/test_local_ip_association.py index 2c1c0ff313..782d513361 100644 --- a/openstackclient/tests/unit/network/v2/test_local_ip_association.py +++ b/openstackclient/tests/unit/network/v2/test_local_ip_association.py @@ -11,7 +11,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# from unittest import mock from unittest.mock import call @@ -19,7 +18,6 @@ from osc_lib import exceptions from openstackclient.network.v2 import local_ip_association -from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes_v2 from openstackclient.tests.unit.network.v2 import fakes as network_fakes @@ -28,8 +26,7 @@ def setUp(self): super().setUp() self.local_ip = network_fakes.create_one_local_ip() self.fixed_port = network_fakes.create_one_port() - self.project = identity_fakes_v2.FakeProject.create_one_project() - self.network_client.find_port = mock.Mock(return_value=self.fixed_port) + self.network_client.find_port.return_value = self.fixed_port class TestCreateLocalIPAssociation(TestLocalIPAssociation): @@ -43,13 +40,11 @@ def setUp(self): } ) ) - self.network_client.create_local_ip_association = mock.Mock( - return_value=self.new_local_ip_association + self.network_client.create_local_ip_association.return_value = ( + self.new_local_ip_association ) - self.network_client.find_local_ip = mock.Mock( - return_value=self.local_ip - ) + self.network_client.find_local_ip.return_value = self.local_ip # Get the command object to test self.cmd = local_ip_association.CreateLocalIPAssociation( @@ -128,13 +123,10 @@ def setUp(self): }, ) ) - self.network_client.delete_local_ip_association = mock.Mock( - return_value=None - ) + self.network_client.delete_local_ip_association.return_value = None + + self.network_client.find_local_ip.return_value = self.local_ip - self.network_client.find_local_ip = mock.Mock( - return_value=self.local_ip - ) # Get the command object to test self.cmd = local_ip_association.DeleteLocalIPAssociation( self.app, None @@ -262,13 +254,13 @@ def setUp(self): lip_assoc.host, ) ) - self.network_client.local_ip_associations = mock.Mock( - return_value=self.local_ip_associations - ) - self.network_client.find_local_ip = mock.Mock( - return_value=self.local_ip + self.network_client.local_ip_associations.return_value = ( + self.local_ip_associations ) - self.network_client.find_port = mock.Mock(return_value=self.fixed_port) + + self.network_client.find_local_ip.return_value = self.local_ip + + self.network_client.find_port.return_value = self.fixed_port # Get the command object to test self.cmd = local_ip_association.ListLocalIPAssociation(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_ndp_proxy.py b/openstackclient/tests/unit/network/v2/test_ndp_proxy.py index 13d3f3d996..e84de82aca 100644 --- a/openstackclient/tests/unit/network/v2/test_ndp_proxy.py +++ b/openstackclient/tests/unit/network/v2/test_ndp_proxy.py @@ -9,15 +9,14 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network.v2 import ndp_proxy -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -25,15 +24,11 @@ class TestNDPProxy(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains self.router = network_fakes.create_one_router({'id': 'fake-router-id'}) - self.network_client.find_router = mock.Mock(return_value=self.router) + self.network_client.find_router.return_value = self.router self.port = network_fakes.create_one_port() - self.network_client.find_port = mock.Mock(return_value=self.port) + self.network_client.find_port.return_value = self.port class TestCreateNDPProxy(TestNDPProxy): @@ -66,9 +61,7 @@ def setUp(self): self.ndp_proxy.router_id, self.ndp_proxy.updated_at, ) - self.network_client.create_ndp_proxy = mock.Mock( - return_value=self.ndp_proxy - ) + self.network_client.create_ndp_proxy.return_value = self.ndp_proxy # Get the command object to test self.cmd = ndp_proxy.CreateNDPProxy(self.app, None) @@ -127,10 +120,8 @@ def setUp(self): attrs = {'router_id': self.router.id, 'port_id': self.port.id} self.ndp_proxies = network_fakes.create_ndp_proxies(attrs) self.ndp_proxy = self.ndp_proxies[0] - self.network_client.delete_ndp_proxy = mock.Mock(return_value=None) - self.network_client.find_ndp_proxy = mock.Mock( - return_value=self.ndp_proxy - ) + self.network_client.delete_ndp_proxy.return_value = None + self.network_client.find_ndp_proxy.return_value = self.ndp_proxy # Get the command object to test self.cmd = ndp_proxy.DeleteNDPProxy(self.app, None) @@ -203,7 +194,7 @@ def setUp(self): ) ) - self.network_client.ndp_proxies = mock.Mock(return_value=ndp_proxies) + self.network_client.ndp_proxies.return_value = ndp_proxies # Get the command object to test self.cmd = ndp_proxy.ListNDPProxy(self.app, None) @@ -295,8 +286,8 @@ def test_ndp_proxy_list_ip_address(self): self.assertCountEqual(self.data, list(data)) def test_ndp_proxy_list_project(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -311,11 +302,11 @@ def test_ndp_proxy_list_project(self): **{'project_id': project.id} ) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) def test_ndp_proxy_list_project_domain(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -332,7 +323,7 @@ def test_ndp_proxy_list_project_domain(self): self.network_client.ndp_proxies.assert_called_once_with(**filters) self.assertEqual(self.columns, columns) - self.assertItemsEqual(self.data, list(data)) + self.assertCountEqual(self.data, list(data)) class TestSetNDPProxy(TestNDPProxy): @@ -340,10 +331,8 @@ def setUp(self): super().setUp() attrs = {'router_id': self.router.id, 'port_id': self.port.id} self.ndp_proxy = network_fakes.create_one_ndp_proxy(attrs) - self.network_client.update_ndp_proxy = mock.Mock(return_value=None) - self.network_client.find_ndp_proxy = mock.Mock( - return_value=self.ndp_proxy - ) + self.network_client.update_ndp_proxy.return_value = None + self.network_client.find_ndp_proxy.return_value = self.ndp_proxy # Get the command object to test self.cmd = ndp_proxy.SetNDPProxy(self.app, None) @@ -434,12 +423,9 @@ def setUp(self): self.ndp_proxy.router_id, self.ndp_proxy.updated_at, ) - self.network_client.get_ndp_proxy = mock.Mock( - return_value=self.ndp_proxy - ) - self.network_client.find_ndp_proxy = mock.Mock( - return_value=self.ndp_proxy - ) + self.network_client.get_ndp_proxy.return_value = self.ndp_proxy + + self.network_client.find_ndp_proxy.return_value = self.ndp_proxy # Get the command object to test self.cmd = ndp_proxy.ShowNDPProxy(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py index f809cad0af..d36652aec7 100644 --- a/openstackclient/tests/unit/network/v2/test_network.py +++ b/openstackclient/tests/unit/network/v2/test_network.py @@ -9,37 +9,24 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# import random -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from openstackclient.network.v2 import network -from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes_v2 -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils -# Tests for Neutron network -# -class TestNetwork(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - - -class TestCreateNetworkIdentityV3(TestNetwork): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() +class TestCreateNetworkIdentityV3(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) # The new network created. _network = network_fakes.create_one_network( attrs={ @@ -71,6 +58,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): 'provider:network_type', 'provider:physical_network', 'provider:segmentation_id', + 'pvlan', 'qos_policy_id', 'router:external', 'shared', @@ -100,6 +88,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): _network.provider_network_type, _network.provider_physical_network, _network.provider_segmentation_id, + _network.pvlan, _network.qos_policy_id, network.RouterExternalColumn(_network.is_router_external), _network.is_shared, @@ -116,19 +105,15 @@ class TestCreateNetworkIdentityV3(TestNetwork): def setUp(self): super().setUp() - self.network_client.create_network = mock.Mock( - return_value=self._network - ) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.create_network.return_value = self._network + + self.network_client.set_tags.return_value = None # Get the command object to test self.cmd = network.CreateNetwork(self.app, None) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain - self.network_client.find_qos_policy = mock.Mock( - return_value=self.qos_policy - ) + self.identity_sdk_client.find_project.return_value = self.project + self.network_client.find_qos_policy.return_value = self.qos_policy def test_create_no_options(self): arglist = [] @@ -193,6 +178,7 @@ def test_create_all_options(self): self.qos_policy.id, "--transparent-vlan", "--no-qinq-vlan", + "--no-pvlan", "--enable-port-security", "--dns-domain", "example.org.", @@ -214,6 +200,7 @@ def test_create_all_options(self): ('qos_policy', self.qos_policy.id), ('transparent_vlan', True), ('qinq_vlan', False), + ('pvlan', False), ('enable_port_security', True), ('name', self._network.name), ('dns_domain', 'example.org.'), @@ -239,6 +226,7 @@ def test_create_all_options(self): 'qos_policy_id': self.qos_policy.id, 'vlan_transparent': True, 'vlan_qinq': False, + 'pvlan': False, 'port_security_enabled': True, 'dns_domain': 'example.org.', } @@ -330,158 +318,45 @@ def test_create_with_vlan_qinq_and_transparency_enabled(self): exceptions.CommandError, self.cmd.take_action, parsed_args ) - -class TestCreateNetworkIdentityV2( - identity_fakes_v2.FakeClientMixin, - network_fakes.FakeClientMixin, - tests_utils.TestCommand, -): - project = identity_fakes_v2.FakeProject.create_one_project() - # The new network created. - _network = network_fakes.create_one_network( - attrs={'project_id': project.id} - ) - columns = ( - 'admin_state_up', - 'availability_zone_hints', - 'availability_zones', - 'created_at', - 'description', - 'dns_domain', - 'id', - 'ipv4_address_scope', - 'ipv6_address_scope', - 'is_default', - 'is_vlan_transparent', - 'is_vlan_qinq', - 'mtu', - 'name', - 'port_security_enabled', - 'project_id', - 'provider:network_type', - 'provider:physical_network', - 'provider:segmentation_id', - 'qos_policy_id', - 'router:external', - 'shared', - 'status', - 'segments', - 'subnets', - 'tags', - 'revision_number', - 'updated_at', - ) - - data = ( - network.AdminStateColumn(_network.is_admin_state_up), - format_columns.ListColumn(_network.availability_zone_hints), - format_columns.ListColumn(_network.availability_zones), - _network.created_at, - _network.description, - _network.dns_domain, - _network.id, - _network.ipv4_address_scope_id, - _network.ipv6_address_scope_id, - _network.is_default, - _network.mtu, - _network.name, - _network.is_port_security_enabled, - _network.project_id, - _network.provider_network_type, - _network.provider_physical_network, - _network.provider_segmentation_id, - _network.qos_policy_id, - network.RouterExternalColumn(_network.is_router_external), - _network.is_shared, - _network.is_vlan_transparent, - _network.is_vlan_qinq, - _network.status, - _network.segments, - format_columns.ListColumn(_network.subnet_ids), - format_columns.ListColumn(_network.tags), - _network.revision_number, - _network.updated_at, - ) - - def setUp(self): - super().setUp() - - self.network_client.create_network = mock.Mock( - return_value=self._network - ) - self.network_client.set_tags = mock.Mock(return_value=None) - - # Get the command object to test - self.cmd = network.CreateNetwork(self.app, None) - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.tenants - self.projects_mock.get.return_value = self.project - - # There is no DomainManager Mock in fake identity v2. - - def test_create_with_project_identityv2(self): + def test_create_with_pvlan_and_port_security_disabled(self): arglist = [ - "--project", - self.project.name, + "--disable-port-security", + "--pvlan", self._network.name, ] - verifylist = [ - ('enable', True), - ('share', None), - ('name', self._network.name), - ('project', self.project.name), - ('external', False), - ] + verifylist = [('disable_port_security', True), ('pvlan', True)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - self.network_client.create_network.assert_called_once_with( - **{ - 'admin_state_up': True, - 'name': self._network.name, - 'project_id': self.project.id, - } + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args ) - self.assertFalse(self.network_client.set_tags.called) - self.assertEqual(set(self.columns), set(columns)) - self.assertCountEqual(self.data, data) - def test_create_with_domain_identityv2(self): + def test_create_with_provider_segment_without_provider_type(self): arglist = [ - "--project", - self.project.name, - "--project-domain", - "domain-name", + "--provider-segment", + "123", self._network.name, ] verifylist = [ - ('enable', True), - ('share', None), - ('project', self.project.name), - ('project_domain', "domain-name"), + ('provider_network_type', None), + ('segmentation_id', "123"), ('name', self._network.name), - ('external', False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.assertRaises( - AttributeError, - self.cmd.take_action, - parsed_args, + exceptions.CommandError, self.cmd.take_action, parsed_args ) -class TestDeleteNetwork(TestNetwork): +class TestDeleteNetwork(network_fakes.TestNetworkV2): def setUp(self): super().setUp() # The networks to delete self._networks = network_fakes.create_networks(count=3) - self.network_client.delete_network = mock.Mock(return_value=None) + self.network_client.delete_network.return_value = None self.network_client.find_network = network_fakes.get_networks( networks=self._networks @@ -540,14 +415,14 @@ def test_delete_multiple_networks_exception(self): exceptions.NotFound('404'), self._networks[1], ] - self.network_client.find_network = mock.Mock(side_effect=ret_find) + self.network_client.find_network.side_effect = ret_find # Fake exception in delete_network() ret_delete = [ None, exceptions.NotFound('404'), ] - self.network_client.delete_network = mock.Mock(side_effect=ret_delete) + self.network_client.delete_network.side_effect = ret_delete self.assertRaises( exceptions.CommandError, self.cmd.take_action, parsed_args @@ -562,9 +437,9 @@ def test_delete_multiple_networks_exception(self): self.network_client.delete_network.assert_has_calls(calls) -class TestListNetwork(TestNetwork): +class TestListNetwork(network_fakes.TestNetworkV2): # The networks going to be listed up. - _network = network_fakes.create_networks(count=3) + _networks = network_fakes.create_networks(count=3) columns = ( 'ID', @@ -586,7 +461,7 @@ class TestListNetwork(TestNetwork): ) data = [] - for net in _network: + for net in _networks: data.append( ( net.id, @@ -596,7 +471,7 @@ class TestListNetwork(TestNetwork): ) data_long = [] - for net in _network: + for net in _networks: data_long.append( ( net.id, @@ -619,13 +494,13 @@ def setUp(self): # Get the command object to test self.cmd = network.ListNetwork(self.app, None) - self.network_client.networks = mock.Mock(return_value=self._network) + self.network_client.networks.return_value = self._networks self._agent = network_fakes.create_one_network_agent() self.network_client.get_agent.return_value = self._agent - self.network_client.dhcp_agent_hosting_networks = mock.Mock( - return_value=self._network + self.network_client.dhcp_agent_hosting_networks.return_value = ( + self._networks ) # TestListTagMixin @@ -648,7 +523,31 @@ def test_network_list_no_options(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) - def test_list_external(self): + def test_network_list_pagination(self): + arglist = [ + '--marker', + self._networks[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self._networks[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.networks.assert_called_once_with( + **{ + 'marker': self._networks[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_network_list_external(self): arglist = [ '--external', ] @@ -669,7 +568,7 @@ def test_list_external(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) - def test_list_internal(self): + def test_network_list_internal(self): arglist = [ '--internal', ] @@ -705,7 +604,7 @@ def test_network_list_long(self): self.assertEqual(self.columns_long, columns) self.assertCountEqual(self.data_long, list(data)) - def test_list_name(self): + def test_network_list_name(self): test_name = "fakename" arglist = [ '--name', @@ -758,8 +657,8 @@ def test_network_list_disable(self): self.assertCountEqual(self.data, list(data)) def test_network_list_project(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -778,8 +677,8 @@ def test_network_list_project(self): self.assertCountEqual(self.data, list(data)) def test_network_list_project_domain(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -853,7 +752,7 @@ def test_network_list_status(self): self.assertCountEqual(self.data, list(data)) def test_network_list_provider_network_type(self): - network_type = self._network[0].provider_network_type + network_type = self._networks[0].provider_network_type arglist = [ '--provider-network-type', network_type, @@ -874,7 +773,7 @@ def test_network_list_provider_network_type(self): self.assertCountEqual(self.data, list(data)) def test_network_list_provider_physical_network(self): - physical_network = self._network[0].provider_physical_network + physical_network = self._networks[0].provider_physical_network arglist = [ '--provider-physical-network', physical_network, @@ -895,7 +794,7 @@ def test_network_list_provider_physical_network(self): self.assertCountEqual(self.data, list(data)) def test_network_list_provider_segment(self): - segmentation_id = self._network[0].provider_segmentation_id + segmentation_id = self._networks[0].provider_segmentation_id arglist = [ '--provider-segment', segmentation_id, @@ -915,6 +814,38 @@ def test_network_list_provider_segment(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) + def test_network_list_pvlan(self): + arglist = [ + '--pvlan', + ] + verifylist = [ + ('long', False), + ('pvlan', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.networks.assert_called_once_with(**{'pvlan': True}) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, list(data)) + + def test_network_list_no_pvlan(self): + arglist = [ + '--no-pvlan', + ] + verifylist = [ + ('long', False), + ('no_pvlan', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.networks.assert_called_once_with( + **{'pvlan': False} + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, list(data)) + def test_network_list_dhcp_agent(self): arglist = ['--agent', self._agent.id] verifylist = [ @@ -964,7 +895,7 @@ def test_list_with_tag_options(self): self.assertCountEqual(self.data, list(data)) -class TestSetNetwork(TestNetwork): +class TestSetNetwork(network_fakes.TestNetworkV2): # The network to set. _network = network_fakes.create_one_network({'tags': ['green', 'red']}) qos_policy = network_fakes.create_one_qos_policy( @@ -974,19 +905,33 @@ class TestSetNetwork(TestNetwork): def setUp(self): super().setUp() - self.network_client.update_network = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.update_network.return_value = None + self.network_client.set_tags.return_value = None - self.network_client.find_network = mock.Mock( - return_value=self._network - ) - self.network_client.find_qos_policy = mock.Mock( - return_value=self.qos_policy - ) + self.network_client.find_network.return_value = self._network + + self.network_client.find_qos_policy.return_value = self.qos_policy # Get the command object to test self.cmd = network.SetNetwork(self.app, None) + def test_set_with_pvlan_and_port_security_disabled(self): + arglist = [ + self._network.name, + '--disable-port-security', + '--pvlan', + ] + verifylist = [ + ('network', self._network.name), + ('disable_port_security', True), + ('pvlan', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + def test_set_this(self): arglist = [ self._network.name, @@ -1157,7 +1102,7 @@ def test_set_with_no_tag(self): self._test_set_tags(with_tags=False) -class TestShowNetwork(TestNetwork): +class TestShowNetwork(network_fakes.TestNetworkV2): # The network to show. _network = network_fakes.create_one_network() columns = ( @@ -1180,6 +1125,7 @@ class TestShowNetwork(TestNetwork): 'provider:network_type', 'provider:physical_network', 'provider:segmentation_id', + 'pvlan', 'qos_policy_id', 'router:external', 'shared', @@ -1209,6 +1155,7 @@ class TestShowNetwork(TestNetwork): _network.provider_network_type, _network.provider_physical_network, _network.provider_segmentation_id, + _network.pvlan, _network.qos_policy_id, network.RouterExternalColumn(_network.is_router_external), _network.is_shared, @@ -1225,9 +1172,7 @@ class TestShowNetwork(TestNetwork): def setUp(self): super().setUp() - self.network_client.find_network = mock.Mock( - return_value=self._network - ) + self.network_client.find_network.return_value = self._network # Get the command object to test self.cmd = network.ShowNetwork(self.app, None) @@ -1258,12 +1203,11 @@ def test_show_all_options(self): self.network_client.find_network.assert_called_once_with( self._network.name, ignore_missing=False ) - self.assertEqual(set(self.columns), set(columns)) self.assertCountEqual(self.data, data) -class TestUnsetNetwork(TestNetwork): +class TestUnsetNetwork(network_fakes.TestNetworkV2): # The network to set. _network = network_fakes.create_one_network({'tags': ['green', 'red']}) qos_policy = network_fakes.create_one_qos_policy( @@ -1273,15 +1217,12 @@ class TestUnsetNetwork(TestNetwork): def setUp(self): super().setUp() - self.network_client.update_network = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.update_network.return_value = None + self.network_client.set_tags.return_value = None - self.network_client.find_network = mock.Mock( - return_value=self._network - ) - self.network_client.find_qos_policy = mock.Mock( - return_value=self.qos_policy - ) + self.network_client.find_network.return_value = self._network + + self.network_client.find_qos_policy.return_value = self.qos_policy # Get the command object to test self.cmd = network.UnsetNetwork(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_network_agent.py b/openstackclient/tests/unit/network/v2/test_network_agent.py index bd79950d52..a7cae620f6 100644 --- a/openstackclient/tests/unit/network/v2/test_network_agent.py +++ b/openstackclient/tests/unit/network/v2/test_network_agent.py @@ -9,9 +9,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -from unittest import mock from unittest.mock import call from osc_lib.cli import format_columns @@ -34,8 +32,8 @@ class TestAddNetworkToAgent(TestNetworkAgent): def setUp(self): super().setUp() - self.network_client.get_agent = mock.Mock(return_value=self.agent) - self.network_client.find_network = mock.Mock(return_value=self.net) + self.network_client.get_agent.return_value = self.agent + self.network_client.find_network.return_value = self.net self.network_client.name = self.network_client.find_network.name self.cmd = network_agent.AddNetworkToAgent(self.app, None) @@ -76,8 +74,8 @@ class TestAddRouterAgent(TestNetworkAgent): def setUp(self): super().setUp() - self.network_client.get_agent = mock.Mock(return_value=self._agent) - self.network_client.find_router = mock.Mock(return_value=self._router) + self.network_client.get_agent.return_value = self._agent + self.network_client.find_router.return_value = self._router self.cmd = network_agent.AddRouterToAgent(self.app, None) @@ -110,7 +108,32 @@ def test_add_router_required_options(self): result = self.cmd.take_action(parsed_args) self.network_client.add_router_to_agent.assert_called_with( - self._agent, self._router + self._agent, self._router, ha_chassis_priority=None + ) + self.assertIsNone(result) + + def test_add_router_with_ha_chassis_priority(self): + arglist = [ + self._agent.id, + self._router.id, + '--l3', + '--ha-chassis-priority', + '100', + ] + verifylist = [ + ('l3', True), + ('agent_id', self._agent.id), + ('router', self._router.id), + ('ha_chassis_priority', 100), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.add_router_to_agent.assert_called_with( + self._agent, + self._router, + ha_chassis_priority=100, ) self.assertIsNone(result) @@ -120,7 +143,7 @@ class TestDeleteNetworkAgent(TestNetworkAgent): def setUp(self): super().setUp() - self.network_client.delete_agent = mock.Mock(return_value=None) + self.network_client.delete_agent.return_value = None # Get the command object to test self.cmd = network_agent.DeleteNetworkAgent(self.app, None) @@ -173,9 +196,7 @@ def test_multi_network_agents_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) delete_mock_result = [True, exceptions.CommandError] - self.network_client.delete_agent = mock.Mock( - side_effect=delete_mock_result - ) + self.network_client.delete_agent.side_effect = delete_mock_result try: self.cmd.take_action(parsed_args) @@ -252,6 +273,30 @@ def test_network_agents_list(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) + def test_network_agents_list_pagination(self): + arglist = [ + '--marker', + self.network_agents[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self.network_agents[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.agents.assert_called_once_with( + **{ + 'marker': self.network_agents[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_network_agents_list_agent_type(self): arglist = [ '--agent-type', @@ -343,8 +388,8 @@ def test_network_agents_list_routers_with_long_option(self): ) # Add a column 'HA State' and corresponding data. - router_agent_columns = self.columns + ('HA State',) - router_agent_data = [d + ('',) for d in self.data] + router_agent_columns = (*self.columns, 'HA State') + router_agent_data = [(*d, '') for d in self.data] self.assertEqual(router_agent_columns, columns) self.assertEqual(len(router_agent_data), len(list(data))) @@ -357,8 +402,8 @@ class TestRemoveNetworkFromAgent(TestNetworkAgent): def setUp(self): super().setUp() - self.network_client.get_agent = mock.Mock(return_value=self.agent) - self.network_client.find_network = mock.Mock(return_value=self.net) + self.network_client.get_agent.return_value = self.agent + self.network_client.find_network.return_value = self.net self.network_client.name = self.network_client.find_network.name self.cmd = network_agent.RemoveNetworkFromAgent(self.app, None) @@ -414,8 +459,8 @@ class TestRemoveRouterAgent(TestNetworkAgent): def setUp(self): super().setUp() - self.network_client.get_agent = mock.Mock(return_value=self._agent) - self.network_client.find_router = mock.Mock(return_value=self._router) + self.network_client.get_agent.return_value = self._agent + self.network_client.find_router.return_value = self._router self.cmd = network_agent.RemoveRouterFromAgent(self.app, None) @@ -453,15 +498,73 @@ def test_remove_router_required_options(self): self.assertIsNone(result) +class TestSetNetworkAgentRouter(TestNetworkAgent): + def setUp(self): + super().setUp() + self._router = network_fakes.create_one_router() + self._agent = network_fakes.create_one_network_agent() + self.network_client.get_agent.return_value = self._agent + self.network_client.find_router.return_value = self._router + self.cmd = network_agent.SetNetworkAgentRouter(self.app, None) + + def test_set_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_set_router_missing_ha_chassis_priority(self): + arglist = [ + self._agent.id, + self._router.id, + ] + verifylist = [] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_set_router_with_ha_chassis_priority(self): + arglist = [ + '--ha-chassis-priority', + '200', + self._agent.id, + self._router.id, + ] + verifylist = [ + ('agent_id', self._agent.id), + ('router', self._router.id), + ('ha_chassis_priority', 200), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_router_in_agent.assert_called_with( + self._agent, + self._router, + ha_chassis_priority=200, + ) + self.assertIsNone(result) + + class TestSetNetworkAgent(TestNetworkAgent): _network_agent = network_fakes.create_one_network_agent() def setUp(self): super().setUp() - self.network_client.update_agent = mock.Mock(return_value=None) - self.network_client.get_agent = mock.Mock( - return_value=self._network_agent - ) + self.network_client.update_agent.return_value = None + self.network_client.get_agent.return_value = self._network_agent # Get the command object to test self.cmd = network_agent.SetNetworkAgent(self.app, None) @@ -546,8 +649,9 @@ class TestShowNetworkAgent(TestNetworkAgent): 'configuration', 'created_at', 'description', - 'host', + 'ha_chassis_priority', 'ha_state', + 'host', 'id', 'last_heartbeat_at', 'resources_synced', @@ -563,6 +667,9 @@ class TestShowNetworkAgent(TestNetworkAgent): format_columns.DictColumn(_network_agent.configuration), _network_agent.created_at, _network_agent.description, + # NOTE(ralonsoh): `ha_chassis_priority` column is still not supported + # by the API. See LP#2103521. + None, _network_agent.ha_state, _network_agent.host, _network_agent.id, @@ -574,9 +681,7 @@ class TestShowNetworkAgent(TestNetworkAgent): def setUp(self): super().setUp() - self.network_client.get_agent = mock.Mock( - return_value=self._network_agent - ) + self.network_client.get_agent.return_value = self._network_agent # Get the command object to test self.cmd = network_agent.ShowNetworkAgent(self.app, None) @@ -608,5 +713,5 @@ def test_show_all_options(self): self.network_client.get_agent.assert_called_once_with( self._network_agent.id ) - self.assertEqual(set(self.columns), set(columns)) + self.assertCountEqual(self.columns, columns) self.assertEqual(len(list(self.data)), len(list(data))) diff --git a/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py b/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py index 20c645bd4c..cea57903c5 100644 --- a/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py +++ b/openstackclient/tests/unit/network/v2/test_network_auto_allocated_topology.py @@ -13,21 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest import mock +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from openstackclient.network.v2 import network_auto_allocated_topology -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.network.v2 import fakes as network_fakes -class TestAutoAllocatedTopology(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - self.projects_mock = self.identity_client.projects - - -class TestCreateAutoAllocatedTopology(TestAutoAllocatedTopology): - project = identity_fakes.FakeProject.create_one_project() +class TestCreateAutoAllocatedTopology(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) network_object = network_fakes.create_one_network() topology = network_fakes.create_one_topology( @@ -50,8 +44,8 @@ def setUp(self): self.cmd = network_auto_allocated_topology.CreateAutoAllocatedTopology( self.app, None ) - self.network_client.get_auto_allocated_topology = mock.Mock( - return_value=self.topology + self.network_client.get_auto_allocated_topology.return_value = ( + self.topology ) def test_create_no_options(self): @@ -131,8 +125,8 @@ def test_create_or_show_option(self): self.assertEqual(self.data, data) -class TestValidateAutoAllocatedTopology(TestAutoAllocatedTopology): - project = identity_fakes.FakeProject.create_one_project() +class TestValidateAutoAllocatedTopology(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) network_object = network_fakes.create_one_network() topology = network_fakes.create_one_topology( @@ -155,8 +149,8 @@ def setUp(self): self.cmd = network_auto_allocated_topology.CreateAutoAllocatedTopology( self.app, None ) - self.network_client.validate_auto_allocated_topology = mock.Mock( - return_value=self.topology + self.network_client.validate_auto_allocated_topology.return_value = ( + self.topology ) def test_show_dry_run_no_project(self): @@ -168,7 +162,7 @@ def test_show_dry_run_no_project(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + _columns, _data = self.cmd.take_action(parsed_args) self.network_client.validate_auto_allocated_topology.assert_called_with( None @@ -186,7 +180,7 @@ def test_show_dry_run_project_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + _columns, _data = self.cmd.take_action(parsed_args) self.network_client.validate_auto_allocated_topology.assert_called_with( self.project.id @@ -207,15 +201,15 @@ def test_show_dry_run_project_domain_option(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + _columns, _data = self.cmd.take_action(parsed_args) self.network_client.validate_auto_allocated_topology.assert_called_with( self.project.id ) -class TestDeleteAutoAllocatedTopology(TestAutoAllocatedTopology): - project = identity_fakes.FakeProject.create_one_project() +class TestDeleteAutoAllocatedTopology(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) network_object = network_fakes.create_one_network() topology = network_fakes.create_one_topology( @@ -228,9 +222,7 @@ def setUp(self): self.cmd = network_auto_allocated_topology.DeleteAutoAllocatedTopology( self.app, None ) - self.network_client.delete_auto_allocated_topology = mock.Mock( - return_value=None - ) + self.network_client.delete_auto_allocated_topology.return_value = None def test_delete_no_project(self): arglist = [] diff --git a/openstackclient/tests/unit/network/v2/test_network_compute.py b/openstackclient/tests/unit/network/v2/test_network_compute.py deleted file mode 100644 index e08e6baf0c..0000000000 --- a/openstackclient/tests/unit/network/v2/test_network_compute.py +++ /dev/null @@ -1,404 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -from unittest import mock - -from osc_lib import exceptions - -from openstackclient.api import compute_v2 -from openstackclient.network.v2 import network -from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -from openstackclient.tests.unit import utils as tests_utils - - -@mock.patch.object(compute_v2, 'create_network') -class TestCreateNetworkCompute(compute_fakes.TestComputev2): - _network = compute_fakes.create_one_network() - - columns = ( - 'bridge', - 'bridge_interface', - 'broadcast', - 'cidr', - 'cidr_v6', - 'created_at', - 'deleted', - 'deleted_at', - 'dhcp_server', - 'dhcp_start', - 'dns1', - 'dns2', - 'enable_dhcp', - 'gateway', - 'gateway_v6', - 'host', - 'id', - 'injected', - 'label', - 'mtu', - 'multi_host', - 'netmask', - 'netmask_v6', - 'priority', - 'project_id', - 'rxtx_base', - 'share_address', - 'updated_at', - 'vlan', - 'vpn_private_address', - 'vpn_public_address', - 'vpn_public_port', - ) - - data = ( - _network['bridge'], - _network['bridge_interface'], - _network['broadcast'], - _network['cidr'], - _network['cidr_v6'], - _network['created_at'], - _network['deleted'], - _network['deleted_at'], - _network['dhcp_server'], - _network['dhcp_start'], - _network['dns1'], - _network['dns2'], - _network['enable_dhcp'], - _network['gateway'], - _network['gateway_v6'], - _network['host'], - _network['id'], - _network['injected'], - _network['label'], - _network['mtu'], - _network['multi_host'], - _network['netmask'], - _network['netmask_v6'], - _network['priority'], - _network['project_id'], - _network['rxtx_base'], - _network['share_address'], - _network['updated_at'], - _network['vlan'], - _network['vpn_private_address'], - _network['vpn_public_address'], - _network['vpn_public_port'], - ) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - self.cmd = network.CreateNetwork(self.app, None) - - def test_network_create_no_options(self, net_mock): - net_mock.return_value = self._network - arglist = [] - verifylist = [] - - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - verifylist, - ) - - def test_network_create_missing_options(self, net_mock): - net_mock.return_value = self._network - arglist = [ - self._network['label'], - ] - verifylist = [ - ('name', self._network['label']), - ] - - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - verifylist, - ) - - def test_network_create_default_options(self, net_mock): - net_mock.return_value = self._network - arglist = [ - "--subnet", - self._network['cidr'], - self._network['label'], - ] - verifylist = [ - ('subnet', self._network['cidr']), - ('name', self._network['label']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - net_mock.assert_called_once_with( - self.compute_client, - subnet=self._network['cidr'], - name=self._network['label'], - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) - - -@mock.patch.object(compute_v2, 'delete_network') -@mock.patch.object(compute_v2, 'find_network') -class TestDeleteNetworkCompute(compute_fakes.TestComputev2): - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - self._networks = compute_fakes.create_networks(count=3) - - self.cmd = network.DeleteNetwork(self.app, None) - - def test_network_delete_one(self, find_net_mock, delete_net_mock): - find_net_mock.side_effect = self._networks - delete_net_mock.return_value = mock.Mock(return_value=None) - arglist = [ - self._networks[0]['label'], - ] - verifylist = [ - ('network', [self._networks[0]['label']]), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - delete_net_mock.assert_called_once_with( - self.compute_client, - self._networks[0]['id'], - ) - self.assertIsNone(result) - - def test_network_delete_multi(self, find_net_mock, delete_net_mock): - find_net_mock.side_effect = self._networks - delete_net_mock.return_value = mock.Mock(return_value=None) - arglist = [ - self._networks[0]['id'], - self._networks[1]['id'], - ] - verifylist = [ - ('network', arglist), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - delete_net_mock.assert_has_calls( - [ - mock.call(self.compute_client, self._networks[0]['id']), - mock.call(self.compute_client, self._networks[1]['id']), - ] - ) - self.assertIsNone(result) - - def test_network_delete_multi_with_exception( - self, find_net_mock, delete_net_mock - ): - find_net_mock.side_effect = [ - self._networks[0], - exceptions.NotFound('foo'), - self._networks[1], - ] - delete_net_mock.return_value = mock.Mock(return_value=None) - - arglist = [ - self._networks[0]['id'], - 'xxxx-yyyy-zzzz', - self._networks[1]['id'], - ] - verifylist = [ - ('network', arglist), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - exc = self.assertRaises( - exceptions.CommandError, - self.cmd.take_action, - parsed_args, - ) - self.assertEqual('1 of 3 networks failed to delete.', str(exc)) - - find_net_mock.assert_has_calls( - [ - mock.call(self.compute_client, self._networks[0]['id']), - mock.call(self.compute_client, 'xxxx-yyyy-zzzz'), - mock.call(self.compute_client, self._networks[1]['id']), - ] - ) - delete_net_mock.assert_has_calls( - [ - mock.call(self.compute_client, self._networks[0]['id']), - mock.call(self.compute_client, self._networks[1]['id']), - ] - ) - - -@mock.patch.object(compute_v2, 'list_networks') -class TestListNetworkCompute(compute_fakes.TestComputev2): - _networks = compute_fakes.create_networks(count=3) - - columns = ( - 'ID', - 'Name', - 'Subnet', - ) - - data = [] - for net in _networks: - data.append( - ( - net['id'], - net['label'], - net['cidr'], - ) - ) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - self.cmd = network.ListNetwork(self.app, None) - - def test_network_list_no_options(self, net_mock): - net_mock.return_value = self._networks - arglist = [] - verifylist = [] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - net_mock.assert_called_once_with(self.compute_client) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, list(data)) - - -@mock.patch.object(compute_v2, 'find_network') -class TestShowNetworkCompute(compute_fakes.TestComputev2): - _network = compute_fakes.create_one_network() - - columns = ( - 'bridge', - 'bridge_interface', - 'broadcast', - 'cidr', - 'cidr_v6', - 'created_at', - 'deleted', - 'deleted_at', - 'dhcp_server', - 'dhcp_start', - 'dns1', - 'dns2', - 'enable_dhcp', - 'gateway', - 'gateway_v6', - 'host', - 'id', - 'injected', - 'label', - 'mtu', - 'multi_host', - 'netmask', - 'netmask_v6', - 'priority', - 'project_id', - 'rxtx_base', - 'share_address', - 'updated_at', - 'vlan', - 'vpn_private_address', - 'vpn_public_address', - 'vpn_public_port', - ) - - data = ( - _network['bridge'], - _network['bridge_interface'], - _network['broadcast'], - _network['cidr'], - _network['cidr_v6'], - _network['created_at'], - _network['deleted'], - _network['deleted_at'], - _network['dhcp_server'], - _network['dhcp_start'], - _network['dns1'], - _network['dns2'], - _network['enable_dhcp'], - _network['gateway'], - _network['gateway_v6'], - _network['host'], - _network['id'], - _network['injected'], - _network['label'], - _network['mtu'], - _network['multi_host'], - _network['netmask'], - _network['netmask_v6'], - _network['priority'], - _network['project_id'], - _network['rxtx_base'], - _network['share_address'], - _network['updated_at'], - _network['vlan'], - _network['vpn_private_address'], - _network['vpn_public_address'], - _network['vpn_public_port'], - ) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - self.cmd = network.ShowNetwork(self.app, None) - - def test_show_no_options(self, net_mock): - net_mock.return_value = self._network - arglist = [] - verifylist = [] - - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - verifylist, - ) - - def test_show_all_options(self, net_mock): - net_mock.return_value = self._network - arglist = [ - self._network['label'], - ] - verifylist = [ - ('network', self._network['label']), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - net_mock.assert_called_once_with( - self.compute_client, self._network['label'] - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_network_flavor.py b/openstackclient/tests/unit/network/v2/test_network_flavor.py index cfbe1f7b03..20aa2d1393 100644 --- a/openstackclient/tests/unit/network/v2/test_network_flavor.py +++ b/openstackclient/tests/unit/network/v2/test_network_flavor.py @@ -12,40 +12,29 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# from unittest import mock +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network.v2 import network_flavor -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils -class TestNetworkFlavor(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - - -class TestAddNetworkFlavorToProfile(TestNetworkFlavor): +class TestAddNetworkFlavorToProfile(network_fakes.TestNetworkV2): network_flavor = network_fakes.create_one_network_flavor() service_profile = network_fakes.create_one_service_profile() def setUp(self): super().setUp() - self.network_client.find_flavor = mock.Mock( - return_value=self.network_flavor - ) - self.network_client.find_service_profile = mock.Mock( - return_value=self.service_profile + self.network_client.find_flavor.return_value = self.network_flavor + self.network_client.find_service_profile.return_value = ( + self.service_profile ) self.cmd = network_flavor.AddNetworkFlavorToProfile(self.app, None) @@ -73,14 +62,14 @@ def test_add_flavor_to_service_profile(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.network_client.associate_flavor_with_service_profile.assert_called_once_with( # noqa: E501 + self.network_client.associate_flavor_with_service_profile.assert_called_once_with( self.network_flavor, self.service_profile ) -class TestCreateNetworkFlavor(TestNetworkFlavor): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() +class TestCreateNetworkFlavor(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) # The new network flavor created. new_network_flavor = network_fakes.create_one_network_flavor() columns = ( @@ -102,15 +91,14 @@ class TestCreateNetworkFlavor(TestNetworkFlavor): def setUp(self): super().setUp() - self.network_client.create_flavor = mock.Mock( - return_value=self.new_network_flavor + self.network_client.create_flavor.return_value = ( + self.new_network_flavor ) # Get the command object to test self.cmd = network_flavor.CreateNetworkFlavor(self.app, None) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project def test_create_no_options(self): arglist = [] @@ -212,13 +200,13 @@ def test_create_disable(self): self.assertEqual(set(self.data), set(data)) -class TestDeleteNetworkFlavor(TestNetworkFlavor): +class TestDeleteNetworkFlavor(network_fakes.TestNetworkV2): # The network flavor to delete. _network_flavors = network_fakes.create_flavor(count=2) def setUp(self): super().setUp() - self.network_client.delete_flavor = mock.Mock(return_value=None) + self.network_client.delete_flavor.return_value = None self.network_client.find_flavor = network_fakes.get_flavor( network_flavors=self._network_flavors ) @@ -278,9 +266,7 @@ def test_multi_network_flavors_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self._network_flavors[0], exceptions.CommandError] - self.network_client.find_flavor = mock.Mock( - side_effect=find_mock_result - ) + self.network_client.find_flavor.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -299,7 +285,7 @@ def test_multi_network_flavors_delete_with_exception(self): ) -class TestListNetworkFlavor(TestNetworkFlavor): +class TestListNetworkFlavor(network_fakes.TestNetworkV2): # The network flavors to list up. _network_flavors = network_fakes.create_flavor(count=2) columns = ( @@ -323,9 +309,7 @@ class TestListNetworkFlavor(TestNetworkFlavor): def setUp(self): super().setUp() - self.network_client.flavors = mock.Mock( - return_value=self._network_flavors - ) + self.network_client.flavors.return_value = self._network_flavors # Get the command object to test self.cmd = network_flavor.ListNetworkFlavor(self.app, None) @@ -341,18 +325,40 @@ def test_network_flavor_list(self): self.assertEqual(set(self.columns), set(columns)) self.assertEqual(self.data, list(data)) + def test_network_flavor_list_pagination(self): + arglist = [ + '--marker', + self._network_flavors[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self._network_flavors[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.flavors.assert_called_once_with( + **{ + 'marker': self._network_flavors[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + -class TestRemoveNetworkFlavorFromProfile(TestNetworkFlavor): +class TestRemoveNetworkFlavorFromProfile(network_fakes.TestNetworkV2): network_flavor = network_fakes.create_one_network_flavor() service_profile = network_fakes.create_one_service_profile() def setUp(self): super().setUp() - self.network_client.find_flavor = mock.Mock( - return_value=self.network_flavor - ) - self.network_client.find_service_profile = mock.Mock( - return_value=self.service_profile + self.network_client.find_flavor.return_value = self.network_flavor + self.network_client.find_service_profile.return_value = ( + self.service_profile ) self.network_client.disassociate_flavor_from_service_profile = ( mock.Mock() @@ -385,12 +391,12 @@ def test_remove_flavor_from_service_profile(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) - self.network_client.disassociate_flavor_from_service_profile.assert_called_once_with( # noqa: E501 + self.network_client.disassociate_flavor_from_service_profile.assert_called_once_with( self.network_flavor, self.service_profile ) -class TestShowNetworkFlavor(TestNetworkFlavor): +class TestShowNetworkFlavor(network_fakes.TestNetworkV2): # The network flavor to show. new_network_flavor = network_fakes.create_one_network_flavor() columns = ( @@ -412,9 +418,7 @@ class TestShowNetworkFlavor(TestNetworkFlavor): def setUp(self): super().setUp() - self.network_client.find_flavor = mock.Mock( - return_value=self.new_network_flavor - ) + self.network_client.find_flavor.return_value = self.new_network_flavor # Get the command object to test self.cmd = network_flavor.ShowNetworkFlavor(self.app, None) @@ -450,16 +454,14 @@ def test_show_all_options(self): self.assertEqual(set(self.data), set(data)) -class TestSetNetworkFlavor(TestNetworkFlavor): +class TestSetNetworkFlavor(network_fakes.TestNetworkV2): # The network flavor to set. new_network_flavor = network_fakes.create_one_network_flavor() def setUp(self): super().setUp() - self.network_client.update_flavor = mock.Mock(return_value=None) - self.network_client.find_flavor = mock.Mock( - return_value=self.new_network_flavor - ) + self.network_client.update_flavor.return_value = None + self.network_client.find_flavor.return_value = self.new_network_flavor # Get the command object to test self.cmd = network_flavor.SetNetworkFlavor(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_network_flavor_profile.py b/openstackclient/tests/unit/network/v2/test_network_flavor_profile.py index d94926c4cc..3e2e3f3ff3 100644 --- a/openstackclient/tests/unit/network/v2/test_network_flavor_profile.py +++ b/openstackclient/tests/unit/network/v2/test_network_flavor_profile.py @@ -15,20 +15,10 @@ from osc_lib import exceptions from openstackclient.network.v2 import network_flavor_profile -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes -class TestFlavorProfile(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - # Get the DomainManager Mock - self.domains_mock = self.identity_client.domains - - -class TestCreateFlavorProfile(TestFlavorProfile): - domain = identity_fakes_v3.FakeDomain.create_one_domain() +class TestCreateFlavorProfile(network_fakes.TestNetworkV2): new_flavor_profile = network_fakes.create_one_service_profile() columns = ( @@ -49,9 +39,10 @@ class TestCreateFlavorProfile(TestFlavorProfile): def setUp(self): super().setUp() - self.network_client.create_service_profile = mock.Mock( - return_value=self.new_flavor_profile + self.network_client.create_service_profile.return_value = ( + self.new_flavor_profile ) + # Get the command object to test self.cmd = network_flavor_profile.CreateNetworkFlavorProfile( self.app, None @@ -189,15 +180,14 @@ def test_create_disable(self): self.assertEqual(self.data, data) -class TestDeleteFlavorProfile(TestFlavorProfile): +class TestDeleteFlavorProfile(network_fakes.TestNetworkV2): # The network flavor_profiles to delete. _network_flavor_profiles = network_fakes.create_service_profile(count=2) def setUp(self): super().setUp() - self.network_client.delete_service_profile = mock.Mock( - return_value=None - ) + self.network_client.delete_service_profile.return_value = None + self.network_client.find_service_profile = ( network_fakes.get_service_profile( flavor_profile=self._network_flavor_profiles @@ -266,9 +256,7 @@ def test_multi_network_flavor_profiles_delete_with_exception(self): self._network_flavor_profiles[0], exceptions.CommandError, ] - self.network_client.find_service_profile = mock.Mock( - side_effect=find_mock_result - ) + self.network_client.find_service_profile.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -289,7 +277,7 @@ def test_multi_network_flavor_profiles_delete_with_exception(self): ) -class TestListFlavorProfile(TestFlavorProfile): +class TestListFlavorProfile(network_fakes.TestNetworkV2): # The network flavor profiles list _network_flavor_profiles = network_fakes.create_service_profile(count=2) @@ -314,8 +302,8 @@ class TestListFlavorProfile(TestFlavorProfile): def setUp(self): super().setUp() - self.network_client.service_profiles = mock.Mock( - return_value=self._network_flavor_profiles + self.network_client.service_profiles.return_value = ( + self._network_flavor_profiles ) # Get the command object to test @@ -334,8 +322,32 @@ def test_network_flavor_profile_list(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_network_flavor_profile_list_pagination(self): + arglist = [ + '--marker', + self._network_flavor_profiles[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self._network_flavor_profiles[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.service_profiles.assert_called_once_with( + **{ + 'marker': self._network_flavor_profiles[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + -class TestShowFlavorProfile(TestFlavorProfile): +class TestShowFlavorProfile(network_fakes.TestNetworkV2): # The network flavor profile to show. network_flavor_profile = network_fakes.create_one_service_profile() columns = ( @@ -355,8 +367,8 @@ class TestShowFlavorProfile(TestFlavorProfile): def setUp(self): super().setUp() - self.network_client.find_service_profile = mock.Mock( - return_value=self.network_flavor_profile + self.network_client.find_service_profile.return_value = ( + self.network_flavor_profile ) # Get the command object to test @@ -382,17 +394,16 @@ def test_show_all_options(self): self.assertEqual(self.data, data) -class TestSetFlavorProfile(TestFlavorProfile): +class TestSetFlavorProfile(network_fakes.TestNetworkV2): # The network flavor profile to set. network_flavor_profile = network_fakes.create_one_service_profile() def setUp(self): super().setUp() - self.network_client.update_service_profile = mock.Mock( - return_value=None - ) - self.network_client.find_service_profile = mock.Mock( - return_value=self.network_flavor_profile + self.network_client.update_service_profile.return_value = None + + self.network_client.find_service_profile.return_value = ( + self.network_flavor_profile ) # Get the command object to test diff --git a/openstackclient/tests/unit/network/v2/test_network_meter.py b/openstackclient/tests/unit/network/v2/test_network_meter.py index a92035d6d6..008bcee8b3 100644 --- a/openstackclient/tests/unit/network/v2/test_network_meter.py +++ b/openstackclient/tests/unit/network/v2/test_network_meter.py @@ -13,29 +13,21 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network.v2 import network_meter -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils -class TestMeter(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - self.projects_mock = self.identity_client.projects - self.domains_mock = self.identity_client.domains - - -class TestCreateMeter(TestMeter): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() - +class TestCreateMeter(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) new_meter = network_fakes.FakeNetworkMeter.create_one_meter() columns = ( 'description', @@ -55,10 +47,9 @@ class TestCreateMeter(TestMeter): def setUp(self): super().setUp() - self.network_client.create_metering_label = mock.Mock( - return_value=self.new_meter - ) - self.projects_mock.get.return_value = self.project + self.network_client.create_metering_label.return_value = self.new_meter + + self.identity_sdk_client.find_project.return_value = self.project self.cmd = network_meter.CreateMeter(self.app, None) def test_create_no_options(self): @@ -126,15 +117,13 @@ def test_create_all_options(self): self.assertEqual(self.data, data) -class TestDeleteMeter(TestMeter): +class TestDeleteMeter(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self.meter_list = network_fakes.FakeNetworkMeter.create_meter(count=2) - self.network_client.delete_metering_label = mock.Mock( - return_value=None - ) + self.network_client.delete_metering_label.return_value = None self.network_client.find_metering_label = ( network_fakes.FakeNetworkMeter.get_meter(meter=self.meter_list) @@ -194,15 +183,13 @@ def test_delete_multiple_meter_exception(self): exceptions.NotFound('404'), self.meter_list[1], ] - self.network_client.find_meter = mock.Mock(side_effect=return_find) + self.network_client.find_metering_label.side_effect = return_find ret_delete = [ None, exceptions.NotFound('404'), ] - self.network_client.delete_metering_label = mock.Mock( - side_effect=ret_delete - ) + self.network_client.delete_metering_label.side_effect = ret_delete self.assertRaises( exceptions.CommandError, self.cmd.take_action, parsed_args @@ -215,7 +202,7 @@ def test_delete_multiple_meter_exception(self): self.network_client.delete_metering_label.assert_has_calls(calls) -class TestListMeter(TestMeter): +class TestListMeter(network_fakes.TestNetworkV2): meter_list = network_fakes.FakeNetworkMeter.create_meter(count=2) columns = ( @@ -240,9 +227,7 @@ class TestListMeter(TestMeter): def setUp(self): super().setUp() - self.network_client.metering_labels = mock.Mock( - return_value=self.meter_list - ) + self.network_client.metering_labels.return_value = self.meter_list self.cmd = network_meter.ListMeter(self.app, None) @@ -258,8 +243,32 @@ def test_meter_list(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_meter_list_pagination(self): + arglist = [ + '--marker', + self.meter_list[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self.meter_list[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) -class TestShowMeter(TestMeter): + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.metering_labels.assert_called_once_with( + **{ + 'marker': self.meter_list[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + +class TestShowMeter(network_fakes.TestNetworkV2): new_meter = network_fakes.FakeNetworkMeter.create_one_meter() columns = ( 'description', @@ -282,9 +291,7 @@ def setUp(self): self.cmd = network_meter.ShowMeter(self.app, None) - self.network_client.find_metering_label = mock.Mock( - return_value=self.new_meter - ) + self.network_client.find_metering_label.return_value = self.new_meter def test_show_no_options(self): arglist = [] diff --git a/openstackclient/tests/unit/network/v2/test_network_meter_rule.py b/openstackclient/tests/unit/network/v2/test_network_meter_rule.py index e90ed05f3b..4212bd336a 100644 --- a/openstackclient/tests/unit/network/v2/test_network_meter_rule.py +++ b/openstackclient/tests/unit/network/v2/test_network_meter_rule.py @@ -13,29 +13,21 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network.v2 import network_meter_rule -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils -class TestMeterRule(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - self.projects_mock = self.identity_client.projects - self.domains_mock = self.identity_client.domains - - -class TestCreateMeterRule(TestMeterRule): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() - +class TestCreateMeterRule(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) new_rule = network_fakes.FakeNetworkMeterRule.create_one_rule() columns = ( @@ -64,15 +56,13 @@ def setUp(self): fake_meter = network_fakes.FakeNetworkMeter.create_one_meter( {'id': self.new_rule.metering_label_id} ) - - self.network_client.create_metering_label_rule = mock.Mock( - return_value=self.new_rule + self.network_client.create_metering_label_rule.return_value = ( + self.new_rule ) - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = self.project + self.network_client.find_metering_label.return_value = fake_meter + self.cmd = network_meter_rule.CreateMeterRule(self.app, None) - self.network_client.find_metering_label = mock.Mock( - return_value=fake_meter - ) def test_create_no_options(self): arglist = [] @@ -140,15 +130,13 @@ def test_create_all_options(self): self.assertEqual(self.data, data) -class TestDeleteMeterRule(TestMeterRule): +class TestDeleteMeterRule(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self.rule_list = network_fakes.FakeNetworkMeterRule.create_meter_rule( count=2 ) - self.network_client.delete_metering_label_rule = mock.Mock( - return_value=None - ) + self.network_client.delete_metering_label_rule.return_value = None self.network_client.find_metering_label_rule = ( network_fakes.FakeNetworkMeterRule.get_meter_rule( @@ -210,17 +198,13 @@ def test_delete_multiple_rules_exception(self): exceptions.NotFound('404'), self.rule_list[1], ] - self.network_client.find_metering_label_rule = mock.Mock( - side_effect=return_find - ) + self.network_client.find_metering_label_rule.side_effect = return_find ret_delete = [ None, exceptions.NotFound('404'), ] - self.network_client.delete_metering_label_rule = mock.Mock( - side_effect=ret_delete - ) + self.network_client.delete_metering_label_rule.side_effect = ret_delete self.assertRaises( exceptions.CommandError, self.cmd.take_action, parsed_args @@ -233,7 +217,7 @@ def test_delete_multiple_rules_exception(self): self.network_client.delete_metering_label_rule.assert_has_calls(calls) -class TestListMeterRule(TestMeterRule): +class TestListMeterRule(network_fakes.TestNetworkV2): rule_list = network_fakes.FakeNetworkMeterRule.create_meter_rule(count=2) columns = ( @@ -262,9 +246,7 @@ class TestListMeterRule(TestMeterRule): def setUp(self): super().setUp() - self.network_client.metering_label_rules = mock.Mock( - return_value=self.rule_list - ) + self.network_client.metering_label_rules.return_value = self.rule_list self.cmd = network_meter_rule.ListMeterRule(self.app, None) @@ -281,7 +263,7 @@ def test_rule_list(self): self.assertEqual(self.data, list(data)) -class TestShowMeterRule(TestMeterRule): +class TestShowMeterRule(network_fakes.TestNetworkV2): new_rule = network_fakes.FakeNetworkMeterRule.create_one_rule() columns = ( @@ -311,8 +293,8 @@ def setUp(self): self.cmd = network_meter_rule.ShowMeterRule(self.app, None) - self.network_client.find_metering_label_rule = mock.Mock( - return_value=self.new_rule + self.network_client.find_metering_label_rule.return_value = ( + self.new_rule ) def test_show_no_options(self): diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_policy.py b/openstackclient/tests/unit/network/v2/test_network_qos_policy.py index fae5b69d53..7a86249e05 100644 --- a/openstackclient/tests/unit/network/v2/test_network_qos_policy.py +++ b/openstackclient/tests/unit/network/v2/test_network_qos_policy.py @@ -16,10 +16,11 @@ from unittest import mock from unittest.mock import call +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network.v2 import network_qos_policy -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -27,12 +28,10 @@ class TestQosPolicy(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.app.client_manager.identity.projects class TestCreateNetworkQosPolicy(TestQosPolicy): - project = identity_fakes_v3.FakeProject.create_one_project() + project = sdk_fakes.generate_fake_resource(_project.Project) # The new qos policy created. new_qos_policy = network_fakes.create_one_qos_policy( @@ -63,14 +62,14 @@ class TestCreateNetworkQosPolicy(TestQosPolicy): def setUp(self): super().setUp() - self.network_client.create_qos_policy = mock.Mock( - return_value=self.new_qos_policy + self.network_client.create_qos_policy.return_value = ( + self.new_qos_policy ) # Get the command object to test self.cmd = network_qos_policy.CreateNetworkQosPolicy(self.app, None) - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = self.project def test_create_no_options(self): arglist = [] @@ -163,7 +162,7 @@ class TestDeleteNetworkQosPolicy(TestQosPolicy): def setUp(self): super().setUp() - self.network_client.delete_qos_policy = mock.Mock(return_value=None) + self.network_client.delete_qos_policy.return_value = None self.network_client.find_qos_policy = network_fakes.get_qos_policies( qos_policies=self._qos_policies ) @@ -264,9 +263,7 @@ class TestListNetworkQosPolicy(TestQosPolicy): def setUp(self): super().setUp() - self.network_client.qos_policies = mock.Mock( - return_value=self.qos_policies - ) + self.network_client.qos_policies.return_value = self.qos_policies # Get the command object to test self.cmd = network_qos_policy.ListNetworkQosPolicy(self.app, None) @@ -282,6 +279,30 @@ def test_qos_policy_list(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_qos_policy_list_pagination(self): + arglist = [ + '--marker', + self.qos_policies[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self.qos_policies[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.qos_policies.assert_called_once_with( + **{ + 'marker': self.qos_policies[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_qos_policy_list_share(self): arglist = [ '--share', @@ -316,8 +337,8 @@ def test_qos_policy_list_no_share(self): self.assertEqual(self.data, list(data)) def test_network_qos_list_project(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -344,10 +365,8 @@ class TestSetNetworkQosPolicy(TestQosPolicy): def setUp(self): super().setUp() - self.network_client.update_qos_policy = mock.Mock(return_value=None) - self.network_client.find_qos_policy = mock.Mock( - return_value=self._qos_policy - ) + self.network_client.update_qos_policy.return_value = None + self.network_client.find_qos_policy.return_value = self._qos_policy # Get the command object to test self.cmd = network_qos_policy.SetNetworkQosPolicy(self.app, None) @@ -447,9 +466,7 @@ class TestShowNetworkQosPolicy(TestQosPolicy): def setUp(self): super().setUp() - self.network_client.find_qos_policy = mock.Mock( - return_value=self._qos_policy - ) + self.network_client.find_qos_policy.return_value = self._qos_policy # Get the command object to test self.cmd = network_qos_policy.ShowNetworkQosPolicy(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_rule.py b/openstackclient/tests/unit/network/v2/test_network_qos_rule.py index e6586849ec..3448589fa2 100644 --- a/openstackclient/tests/unit/network/v2/test_network_qos_rule.py +++ b/openstackclient/tests/unit/network/v2/test_network_qos_rule.py @@ -56,9 +56,7 @@ class TestNetworkQosRule(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self.qos_policy = network_fakes.create_one_qos_policy() - self.network_client.find_qos_policy = mock.Mock( - return_value=self.qos_policy - ) + self.network_client.find_qos_policy.return_value = self.qos_policy class TestCreateNetworkQosRuleMinimumBandwidth(TestNetworkQosRule): @@ -87,8 +85,8 @@ def setUp(self): self.new_rule.project_id, self.new_rule.type, ) - self.network_client.create_qos_minimum_bandwidth_rule = mock.Mock( - return_value=self.new_rule + self.network_client.create_qos_minimum_bandwidth_rule.return_value = ( + self.new_rule ) # Get the command object to test @@ -190,9 +188,7 @@ def setUp(self): self.new_rule.project_id, self.new_rule.type, ) - self.network_client.create_qos_minimum_packet_rate_rule = mock.Mock( - return_value=self.new_rule - ) + self.network_client.create_qos_minimum_packet_rate_rule.return_value = self.new_rule # Get the command object to test self.cmd = network_qos_rule.CreateNetworkQosRule(self.app, None) @@ -230,7 +226,7 @@ def test_create_default_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network_client.create_qos_minimum_packet_rate_rule.assert_called_once_with( # noqa: E501 + self.network_client.create_qos_minimum_packet_rate_rule.assert_called_once_with( self.qos_policy.id, **{ 'min_kpps': self.new_rule.min_kpps, @@ -291,8 +287,8 @@ def setUp(self): self.new_rule.project_id, self.new_rule.type, ) - self.network_client.create_qos_dscp_marking_rule = mock.Mock( - return_value=self.new_rule + self.network_client.create_qos_dscp_marking_rule.return_value = ( + self.new_rule ) # Get the command object to test @@ -389,8 +385,8 @@ def setUp(self): self.new_rule.project_id, self.new_rule.type, ) - self.network_client.create_qos_bandwidth_limit_rule = mock.Mock( - return_value=self.new_rule + self.network_client.create_qos_bandwidth_limit_rule.return_value = ( + self.new_rule ) # Get the command object to test @@ -529,9 +525,10 @@ def setUp(self): } self.new_rule = network_fakes.create_one_qos_rule(attrs) self.qos_policy.rules = [self.new_rule] - self.network_client.delete_qos_minimum_bandwidth_rule = mock.Mock( - return_value=None + self.network_client.delete_qos_minimum_bandwidth_rule.return_value = ( + None ) + self.network_client.find_qos_minimum_bandwidth_rule = ( network_fakes.get_qos_rules(qos_rules=self.new_rule) ) @@ -592,9 +589,8 @@ def setUp(self): } self.new_rule = network_fakes.create_one_qos_rule(attrs) self.qos_policy.rules = [self.new_rule] - self.network_client.delete_qos_minimum_packet_rate_rule = mock.Mock( - return_value=None - ) + self.network_client.delete_qos_minimum_packet_rate_rule.return_value = None + self.network_client.find_qos_minimum_packet_rate_rule = ( network_fakes.get_qos_rules(qos_rules=self.new_rule) ) @@ -617,7 +613,7 @@ def test_qos_policy_delete(self): self.network_client.find_qos_policy.assert_called_once_with( self.qos_policy.id, ignore_missing=False ) - self.network_client.delete_qos_minimum_packet_rate_rule.assert_called_once_with( # noqa: E501 + self.network_client.delete_qos_minimum_packet_rate_rule.assert_called_once_with( self.new_rule.id, self.qos_policy.id ) self.assertIsNone(result) @@ -655,9 +651,8 @@ def setUp(self): } self.new_rule = network_fakes.create_one_qos_rule(attrs) self.qos_policy.rules = [self.new_rule] - self.network_client.delete_qos_dscp_marking_rule = mock.Mock( - return_value=None - ) + self.network_client.delete_qos_dscp_marking_rule.return_value = None + self.network_client.find_qos_dscp_marking_rule = ( network_fakes.get_qos_rules(qos_rules=self.new_rule) ) @@ -718,9 +713,8 @@ def setUp(self): } self.new_rule = network_fakes.create_one_qos_rule(attrs) self.qos_policy.rules = [self.new_rule] - self.network_client.delete_qos_bandwidth_limit_rule = mock.Mock( - return_value=None - ) + self.network_client.delete_qos_bandwidth_limit_rule.return_value = None + self.network_client.find_qos_bandwidth_limit_rule = ( network_fakes.get_qos_rules(qos_rules=self.new_rule) ) @@ -781,16 +775,16 @@ def setUp(self): } self.new_rule = network_fakes.create_one_qos_rule(attrs) self.qos_policy.rules = [self.new_rule] - self.network_client.update_qos_minimum_bandwidth_rule = mock.Mock( - return_value=None + self.network_client.update_qos_minimum_bandwidth_rule.return_value = ( + None ) - self.network_client.find_qos_minimum_bandwidth_rule = mock.Mock( - return_value=self.new_rule - ) - self.network_client.find_qos_policy = mock.Mock( - return_value=self.qos_policy + + self.network_client.find_qos_minimum_bandwidth_rule.return_value = ( + self.new_rule ) + self.network_client.find_qos_policy.return_value = self.qos_policy + # Get the command object to test self.cmd = network_qos_rule.SetNetworkQosRule(self.app, None) @@ -883,16 +877,14 @@ def setUp(self): } self.new_rule = network_fakes.create_one_qos_rule(attrs) self.qos_policy.rules = [self.new_rule] - self.network_client.update_qos_minimum_packet_rate_rule = mock.Mock( - return_value=None - ) - self.network_client.find_qos_minimum_packet_rate_rule = mock.Mock( - return_value=self.new_rule - ) - self.network_client.find_qos_policy = mock.Mock( - return_value=self.qos_policy + self.network_client.update_qos_minimum_packet_rate_rule.return_value = None + + self.network_client.find_qos_minimum_packet_rate_rule.return_value = ( + self.new_rule ) + self.network_client.find_qos_policy.return_value = self.qos_policy + # Get the command object to test self.cmd = network_qos_rule.SetNetworkQosRule(self.app, None) @@ -985,16 +977,14 @@ def setUp(self): } self.new_rule = network_fakes.create_one_qos_rule(attrs) self.qos_policy.rules = [self.new_rule] - self.network_client.update_qos_dscp_marking_rule = mock.Mock( - return_value=None - ) - self.network_client.find_qos_dscp_marking_rule = mock.Mock( - return_value=self.new_rule - ) - self.network_client.find_qos_policy = mock.Mock( - return_value=self.qos_policy + self.network_client.update_qos_dscp_marking_rule.return_value = None + + self.network_client.find_qos_dscp_marking_rule.return_value = ( + self.new_rule ) + self.network_client.find_qos_policy.return_value = self.qos_policy + # Get the command object to test self.cmd = network_qos_rule.SetNetworkQosRule(self.app, None) @@ -1351,8 +1341,8 @@ def setUp(self): self.new_rule.type, ) - self.network_client.get_qos_minimum_bandwidth_rule = mock.Mock( - return_value=self.new_rule + self.network_client.get_qos_minimum_bandwidth_rule.return_value = ( + self.new_rule ) # Get the command object to test @@ -1415,8 +1405,8 @@ def setUp(self): self.new_rule.type, ) - self.network_client.get_qos_minimum_packet_rate_rule = mock.Mock( - return_value=self.new_rule + self.network_client.get_qos_minimum_packet_rate_rule.return_value = ( + self.new_rule ) # Get the command object to test @@ -1477,8 +1467,8 @@ def setUp(self): self.new_rule.type, ) - self.network_client.get_qos_dscp_marking_rule = mock.Mock( - return_value=self.new_rule + self.network_client.get_qos_dscp_marking_rule.return_value = ( + self.new_rule ) # Get the command object to test @@ -1543,8 +1533,8 @@ def setUp(self): self.new_rule.type, ) - self.network_client.get_qos_bandwidth_limit_rule = mock.Mock( - return_value=self.new_rule + self.network_client.get_qos_bandwidth_limit_rule.return_value = ( + self.new_rule ) # Get the command object to test diff --git a/openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py b/openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py index c0aafa0bc0..2754b5dcbb 100644 --- a/openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py +++ b/openstackclient/tests/unit/network/v2/test_network_qos_rule_type.py @@ -13,19 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest import mock - from openstackclient.network.v2 import network_qos_rule_type as _qos_rule_type from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils -class TestNetworkQosRuleType(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - -class TestShowNetworkQosRuleType(TestNetworkQosRuleType): +class TestShowNetworkQosRuleType(network_fakes.TestNetworkV2): attrs = {'drivers': [{'name': 'driver 1', 'supported_parameters': []}]} # The QoS policies to show. qos_rule_type = network_fakes.create_one_qos_rule_type(attrs) @@ -35,9 +28,7 @@ class TestShowNetworkQosRuleType(TestNetworkQosRuleType): def setUp(self): super().setUp() - self.network_client.get_qos_rule_type = mock.Mock( - return_value=self.qos_rule_type - ) + self.network_client.get_qos_rule_type.return_value = self.qos_rule_type # Get the command object to test self.cmd = _qos_rule_type.ShowNetworkQosRuleType(self.app, None) @@ -73,7 +64,7 @@ def test_show_all_options(self): self.assertEqual(list(self.data), list(data)) -class TestListNetworkQosRuleType(TestNetworkQosRuleType): +class TestListNetworkQosRuleType(network_fakes.TestNetworkV2): # The QoS policies to list up. qos_rule_types = network_fakes.create_qos_rule_types(count=3) @@ -84,9 +75,7 @@ class TestListNetworkQosRuleType(TestNetworkQosRuleType): def setUp(self): super().setUp() - self.network_client.qos_rule_types = mock.Mock( - return_value=self.qos_rule_types - ) + self.network_client.qos_rule_types.return_value = self.qos_rule_types # Get the command object to test self.cmd = _qos_rule_type.ListNetworkQosRuleType(self.app, None) @@ -131,3 +120,27 @@ def test_qos_rule_type_list_all_rules(self): ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + + def test_qos_rule_type_list_pagination(self): + arglist = [ + '--marker', + self.qos_rule_types[0].type, + '--limit', + '1', + ] + verifylist = [ + ('marker', self.qos_rule_types[0].type), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.qos_rule_types.assert_called_once_with( + **{ + 'marker': self.qos_rule_types[0].type, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) diff --git a/openstackclient/tests/unit/network/v2/test_network_rbac.py b/openstackclient/tests/unit/network/v2/test_network_rbac.py index 816e932371..2ff86fe8b1 100644 --- a/openstackclient/tests/unit/network/v2/test_network_rbac.py +++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py @@ -11,14 +11,14 @@ # under the License. # -from unittest import mock from unittest.mock import call import ddt +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network.v2 import network_rbac -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -27,9 +27,6 @@ class TestNetworkRBAC(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - @ddt.ddt class TestCreateNetworkRBAC(TestNetworkRBAC): @@ -39,7 +36,7 @@ class TestCreateNetworkRBAC(TestNetworkRBAC): as_object = network_fakes.create_one_address_scope() snp_object = network_fakes.FakeSubnetPool.create_one_subnet_pool() ag_object = network_fakes.create_one_address_group() - project = identity_fakes_v3.FakeProject.create_one_project() + project = sdk_fakes.generate_fake_resource(_project.Project) rbac_policy = network_fakes.create_one_network_rbac( attrs={ 'project_id': project.id, @@ -72,28 +69,21 @@ def setUp(self): # Get the command object to test self.cmd = network_rbac.CreateNetworkRBAC(self.app, None) - self.network_client.create_rbac_policy = mock.Mock( - return_value=self.rbac_policy - ) - self.network_client.find_network = mock.Mock( - return_value=self.network_object - ) - self.network_client.find_qos_policy = mock.Mock( - return_value=self.qos_object - ) - self.network_client.find_security_group = mock.Mock( - return_value=self.sg_object - ) - self.network_client.find_address_scope = mock.Mock( - return_value=self.as_object - ) - self.network_client.find_subnet_pool = mock.Mock( - return_value=self.snp_object - ) - self.network_client.find_address_group = mock.Mock( - return_value=self.ag_object - ) - self.projects_mock.get.return_value = self.project + self.network_client.create_rbac_policy.return_value = self.rbac_policy + + self.network_client.find_network.return_value = self.network_object + + self.network_client.find_qos_policy.return_value = self.qos_object + + self.network_client.find_security_group.return_value = self.sg_object + + self.network_client.find_address_scope.return_value = self.as_object + + self.network_client.find_subnet_pool.return_value = self.snp_object + + self.network_client.find_address_group.return_value = self.ag_object + + self.identity_sdk_client.find_project.return_value = self.project def test_network_rbac_create_no_type(self): arglist = [ @@ -232,7 +222,7 @@ def test_network_rbac_create_with_target_all_projects(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) + _columns, _data = self.cmd.take_action(parsed_args) self.network_client.create_rbac_policy.assert_called_with( **{ @@ -343,7 +333,7 @@ class TestDeleteNetworkRBAC(TestNetworkRBAC): def setUp(self): super().setUp() - self.network_client.delete_rbac_policy = mock.Mock(return_value=None) + self.network_client.delete_rbac_policy.return_value = None self.network_client.find_rbac_policy = network_fakes.get_network_rbacs( rbac_policies=self.rbac_policies ) @@ -400,9 +390,7 @@ def test_multi_network_policies_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self.rbac_policies[0], exceptions.CommandError] - self.network_client.find_rbac_policy = mock.Mock( - side_effect=find_mock_result - ) + self.network_client.find_rbac_policy.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -462,12 +450,10 @@ def setUp(self): # Get the command object to test self.cmd = network_rbac.ListNetworkRBAC(self.app, None) - self.network_client.rbac_policies = mock.Mock( - return_value=self.rbac_policies - ) + self.network_client.rbac_policies.return_value = self.rbac_policies - self.project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = self.project + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project def test_network_rbac_list(self): arglist = [] @@ -481,6 +467,30 @@ def test_network_rbac_list(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_network_rbac_list_pagination(self): + arglist = [ + '--marker', + self.rbac_policies[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self.rbac_policies[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.rbac_policies.assert_called_once_with( + **{ + 'marker': self.rbac_policies[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_network_rbac_list_type_opt(self): arglist = [ '--type', @@ -553,7 +563,7 @@ def test_network_rbac_list_target_project_opt(self): class TestSetNetworkRBAC(TestNetworkRBAC): - project = identity_fakes_v3.FakeProject.create_one_project() + project = sdk_fakes.generate_fake_resource(_project.Project) rbac_policy = network_fakes.create_one_network_rbac( attrs={'target_tenant': project.id} ) @@ -564,11 +574,10 @@ def setUp(self): # Get the command object to test self.cmd = network_rbac.SetNetworkRBAC(self.app, None) - self.network_client.find_rbac_policy = mock.Mock( - return_value=self.rbac_policy - ) - self.network_client.update_rbac_policy = mock.Mock(return_value=None) - self.projects_mock.get.return_value = self.project + self.network_client.find_rbac_policy.return_value = self.rbac_policy + + self.network_client.update_rbac_policy.return_value = None + self.identity_sdk_client.find_project.return_value = self.project def test_network_rbac_set_nothing(self): arglist = [ @@ -639,9 +648,7 @@ def setUp(self): # Get the command object to test self.cmd = network_rbac.ShowNetworkRBAC(self.app, None) - self.network_client.find_rbac_policy = mock.Mock( - return_value=self.rbac_policy - ) + self.network_client.find_rbac_policy.return_value = self.rbac_policy def test_show_no_options(self): arglist = [] diff --git a/openstackclient/tests/unit/network/v2/test_network_segment.py b/openstackclient/tests/unit/network/v2/test_network_segment.py index 936a20aefd..bd6324c980 100644 --- a/openstackclient/tests/unit/network/v2/test_network_segment.py +++ b/openstackclient/tests/unit/network/v2/test_network_segment.py @@ -9,9 +9,7 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -from unittest import mock from unittest.mock import call from osc_lib import exceptions @@ -58,12 +56,9 @@ class TestCreateNetworkSegment(TestNetworkSegment): def setUp(self): super().setUp() - self.network_client.create_segment = mock.Mock( - return_value=self._network_segment - ) - self.network_client.find_network = mock.Mock( - return_value=self._network - ) + self.network_client.create_segment.return_value = self._network_segment + + self.network_client.find_network.return_value = self._network # Get the command object to test self.cmd = network_segment.CreateNetworkSegment(self.app, None) @@ -172,10 +167,8 @@ class TestDeleteNetworkSegment(TestNetworkSegment): def setUp(self): super().setUp() - self.network_client.delete_segment = mock.Mock(return_value=None) - self.network_client.find_segment = mock.Mock( - side_effect=self._network_segments - ) + self.network_client.delete_segment.return_value = None + self.network_client.find_segment.side_effect = self._network_segments # Get the command object to test self.cmd = network_segment.DeleteNetworkSegment(self.app, None) @@ -224,9 +217,7 @@ def test_delete_multiple_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self._network_segments[0], exceptions.CommandError] - self.network_client.find_segment = mock.Mock( - side_effect=find_mock_result - ) + self.network_client.find_segment.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -258,7 +249,7 @@ class TestListNetworkSegment(TestNetworkSegment): 'Network Type', 'Segment', ) - columns_long = columns + ('Physical Network',) + columns_long = (*columns, 'Physical Network') data = [] for _network_segment in _network_segments: @@ -291,12 +282,9 @@ def setUp(self): # Get the command object to test self.cmd = network_segment.ListNetworkSegment(self.app, None) - self.network_client.find_network = mock.Mock( - return_value=self._network - ) - self.network_client.segments = mock.Mock( - return_value=self._network_segments - ) + self.network_client.find_network.return_value = self._network + + self.network_client.segments.return_value = self._network_segments def test_list_no_option(self): arglist = [] @@ -344,6 +332,30 @@ def test_list_network(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_list_pagination(self): + arglist = [ + '--marker', + self._network_segments[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self._network_segments[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.segments.assert_called_once_with( + **{ + 'marker': self._network_segments[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + class TestSetNetworkSegment(TestNetworkSegment): # The network segment to show. @@ -352,12 +364,9 @@ class TestSetNetworkSegment(TestNetworkSegment): def setUp(self): super().setUp() - self.network_client.find_segment = mock.Mock( - return_value=self._network_segment - ) - self.network_client.update_segment = mock.Mock( - return_value=self._network_segment - ) + self.network_client.find_segment.return_value = self._network_segment + + self.network_client.update_segment.return_value = self._network_segment # Get the command object to test self.cmd = network_segment.SetNetworkSegment(self.app, None) @@ -432,9 +441,7 @@ class TestShowNetworkSegment(TestNetworkSegment): def setUp(self): super().setUp() - self.network_client.find_segment = mock.Mock( - return_value=self._network_segment - ) + self.network_client.find_segment.return_value = self._network_segment # Get the command object to test self.cmd = network_segment.ShowNetworkSegment(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_network_segment_range.py b/openstackclient/tests/unit/network/v2/test_network_segment_range.py index d5e406f396..207fe2a186 100644 --- a/openstackclient/tests/unit/network/v2/test_network_segment_range.py +++ b/openstackclient/tests/unit/network/v2/test_network_segment_range.py @@ -12,7 +12,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# from unittest import mock from unittest.mock import call @@ -38,12 +37,7 @@ def test__get_ranges(self): ) -class TestNetworkSegmentRange(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - -class TestCreateNetworkSegmentRange(TestNetworkSegmentRange): +class TestCreateNetworkSegmentRange(network_fakes.TestNetworkV2): # The network segment range to create. _network_segment_range = network_fakes.create_one_network_segment_range() @@ -78,8 +72,8 @@ class TestCreateNetworkSegmentRange(TestNetworkSegmentRange): def setUp(self): super().setUp() - self.network_client.create_network_segment_range = mock.Mock( - return_value=self._network_segment_range + self.network_client.create_network_segment_range.return_value = ( + self._network_segment_range ) # Get the command object to test @@ -333,7 +327,7 @@ def test_create_all_options(self): 'shared': self._network_segment_range.shared, 'project_id': mock.ANY, 'network_type': self._network_segment_range.network_type, - 'physical_network': self._network_segment_range.physical_network, # noqa: E501 + 'physical_network': self._network_segment_range.physical_network, 'minimum': self._network_segment_range.minimum, 'maximum': self._network_segment_range.maximum, 'name': self._network_segment_range.name, @@ -344,18 +338,17 @@ def test_create_all_options(self): self.assertEqual(self.data, data) -class TestDeleteNetworkSegmentRange(TestNetworkSegmentRange): +class TestDeleteNetworkSegmentRange(network_fakes.TestNetworkV2): # The network segment ranges to delete. _network_segment_ranges = network_fakes.create_network_segment_ranges() def setUp(self): super().setUp() - self.network_client.delete_network_segment_range = mock.Mock( - return_value=None - ) - self.network_client.find_network_segment_range = mock.Mock( - side_effect=self._network_segment_ranges + self.network_client.delete_network_segment_range.return_value = None + + self.network_client.find_network_segment_range.side_effect = ( + self._network_segment_ranges ) # Get the command object to test @@ -412,8 +405,8 @@ def test_delete_multiple_with_exception(self): self._network_segment_ranges[0], exceptions.CommandError, ] - self.network_client.find_network_segment_range = mock.Mock( - side_effect=find_mock_result + self.network_client.find_network_segment_range.side_effect = ( + find_mock_result ) try: @@ -435,7 +428,7 @@ def test_delete_multiple_with_exception(self): ) -class TestListNetworkSegmentRange(TestNetworkSegmentRange): +class TestListNetworkSegmentRange(network_fakes.TestNetworkV2): _network_segment_ranges = network_fakes.create_network_segment_ranges( count=3 ) @@ -451,10 +444,7 @@ class TestListNetworkSegmentRange(TestNetworkSegmentRange): 'Minimum ID', 'Maximum ID', ) - columns_long = columns + ( - 'Used', - 'Available', - ) + columns_long = (*columns, 'Used', 'Available') data = [] for _network_segment_range in _network_segment_ranges: @@ -493,8 +483,8 @@ class TestListNetworkSegmentRange(TestNetworkSegmentRange): def setUp(self): super().setUp() - self.network_client.network_segment_ranges = mock.Mock( - return_value=self._network_segment_ranges + self.network_client.network_segment_ranges.return_value = ( + self._network_segment_ranges ) # Get the command object to test @@ -519,6 +509,30 @@ def test_list_no_option(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) + def test_list_pagination(self): + arglist = [ + '--marker', + self._network_segment_ranges[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self._network_segment_ranges[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.network_segment_ranges.assert_called_once_with( + **{ + 'marker': self._network_segment_ranges[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_list_long(self): arglist = [ '--long', @@ -539,17 +553,17 @@ def test_list_long(self): self.assertEqual(self.data_long, list(data)) -class TestSetNetworkSegmentRange(TestNetworkSegmentRange): +class TestSetNetworkSegmentRange(network_fakes.TestNetworkV2): # The network segment range to set. _network_segment_range = network_fakes.create_one_network_segment_range() # The network segment range updated. minimum_updated = _network_segment_range.minimum - 5 maximum_updated = _network_segment_range.maximum + 5 - available_updated = ( - list(range(minimum_updated, 104)) - + [105] - + list(range(107, maximum_updated + 1)) - ) + available_updated = [ + *list(range(minimum_updated, 104)), + 105, + *list(range(107, maximum_updated + 1)), + ] _network_segment_range_updated = ( network_fakes.create_one_network_segment_range( attrs={ @@ -567,8 +581,8 @@ class TestSetNetworkSegmentRange(TestNetworkSegmentRange): def setUp(self): super().setUp() - self.network_client.find_network_segment_range = mock.Mock( - return_value=self._network_segment_range + self.network_client.find_network_segment_range.return_value = ( + self._network_segment_range ) # Get the command object to test @@ -583,9 +597,10 @@ def test_set_no_options(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.network_client.update_network_segment_range = mock.Mock( - return_value=self._network_segment_range + self.network_client.update_network_segment_range.return_value = ( + self._network_segment_range ) + result = self.cmd.take_action(parsed_args) self.network_client.update_network_segment_range.assert_called_once_with( @@ -611,9 +626,10 @@ def test_set_all_options(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.network_client.update_network_segment_range = mock.Mock( - return_value=self._network_segment_range_updated + self.network_client.update_network_segment_range.return_value = ( + self._network_segment_range_updated ) + result = self.cmd.take_action(parsed_args) attrs = { @@ -627,7 +643,7 @@ def test_set_all_options(self): self.assertIsNone(result) -class TestShowNetworkSegmentRange(TestNetworkSegmentRange): +class TestShowNetworkSegmentRange(network_fakes.TestNetworkV2): # The network segment range to show. _network_segment_range = network_fakes.create_one_network_segment_range() @@ -662,8 +678,8 @@ class TestShowNetworkSegmentRange(TestNetworkSegmentRange): def setUp(self): super().setUp() - self.network_client.find_network_segment_range = mock.Mock( - return_value=self._network_segment_range + self.network_client.find_network_segment_range.return_value = ( + self._network_segment_range ) # Get the command object to test diff --git a/openstackclient/tests/unit/network/v2/test_network_service_provider.py b/openstackclient/tests/unit/network/v2/test_network_service_provider.py index 9f61bce593..ec1f664605 100644 --- a/openstackclient/tests/unit/network/v2/test_network_service_provider.py +++ b/openstackclient/tests/unit/network/v2/test_network_service_provider.py @@ -13,20 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest import mock - from openstackclient.network.v2 import ( network_service_provider as service_provider, ) from openstackclient.tests.unit.network.v2 import fakes -class TestNetworkServiceProvider(fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - -class TestListNetworkServiceProvider(TestNetworkServiceProvider): +class TestListNetworkServiceProvider(fakes.TestNetworkV2): provider_list = ( fakes.FakeNetworkServiceProvider.create_network_service_providers( count=2 @@ -52,9 +45,7 @@ class TestListNetworkServiceProvider(TestNetworkServiceProvider): def setUp(self): super().setUp() - self.network_client.service_providers = mock.Mock( - return_value=self.provider_list - ) + self.network_client.service_providers.return_value = self.provider_list self.cmd = service_provider.ListNetworkServiceProvider(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_network_trunk.py b/openstackclient/tests/unit/network/v2/test_network_trunk.py index 180e80de50..d1177565bd 100644 --- a/openstackclient/tests/unit/network/v2/test_network_trunk.py +++ b/openstackclient/tests/unit/network/v2/test_network_trunk.py @@ -11,34 +11,23 @@ # under the License. import copy -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions import testtools from openstackclient.network.v2 import network_trunk -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as test_utils -# Tests for Neutron trunks -# -class TestNetworkTrunk(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - - -class TestCreateNetworkTrunk(TestNetworkTrunk): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() +class TestCreateNetworkTrunk(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) trunk_networks = network_fakes.create_networks(count=2) parent_port = network_fakes.create_one_port( attrs={'project_id': project.id, 'network_id': trunk_networks[0]['id']} @@ -84,18 +73,16 @@ class TestCreateNetworkTrunk(TestNetworkTrunk): def setUp(self): super().setUp() - self.network_client.create_trunk = mock.Mock( - return_value=self.new_trunk - ) - self.network_client.find_port = mock.Mock( - side_effect=[self.parent_port, self.sub_port] - ) - # Get the command object to test - self.cmd = network_trunk.CreateNetworkTrunk(self.app, None) + self.identity_sdk_client.find_project.return_value = self.project + self.identity_sdk_client.find_domain.return_value = self.domain + self.network_client.create_trunk.return_value = self.new_trunk + self.network_client.find_port.side_effect = [ + self.parent_port, + self.sub_port, + ] - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.cmd = network_trunk.CreateNetworkTrunk(self.app, None) def test_create_no_options(self): arglist = [] @@ -287,10 +274,10 @@ def test_create_network_trunk_subports_without_required_key_fail(self): self.check_parser(self.cmd, arglist, verifylist) -class TestDeleteNetworkTrunk(TestNetworkTrunk): +class TestDeleteNetworkTrunk(network_fakes.TestNetworkV2): # The trunk to be deleted. - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) trunk_networks = network_fakes.create_networks(count=2) parent_port = network_fakes.create_one_port( attrs={'project_id': project.id, 'network_id': trunk_networks[0]['id']} @@ -313,16 +300,19 @@ class TestDeleteNetworkTrunk(TestNetworkTrunk): def setUp(self): super().setUp() - self.network_client.find_trunk = mock.Mock( - side_effect=[self.new_trunks[0], self.new_trunks[1]] - ) - self.network_client.delete_trunk = mock.Mock(return_value=None) - self.network_client.find_port = mock.Mock( - side_effect=[self.parent_port, self.sub_port] - ) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.network_client.find_trunk.side_effect = [ + self.new_trunks[0], + self.new_trunks[1], + ] + self.network_client.delete_trunk.return_value = None + self.network_client.find_port.side_effect = [ + self.parent_port, + self.sub_port, + ] + + self.identity_sdk_client.find_project.return_value = self.project + self.identity_sdk_client.find_domain.return_value = self.domain # Get the command object to test self.cmd = network_trunk.DeleteNetworkTrunk(self.app, None) @@ -371,9 +361,11 @@ def test_delete_trunk_multiple_with_exception(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.network_client.find_trunk = mock.Mock( - side_effect=[self.new_trunks[0], exceptions.CommandError] - ) + self.network_client.find_trunk.side_effect = [ + self.new_trunks[0], + exceptions.CommandError, + ] + with testtools.ExpectedException(exceptions.CommandError) as e: self.cmd.take_action(parsed_args) self.assertEqual('1 of 2 trunks failed to delete.', str(e)) @@ -382,9 +374,9 @@ def test_delete_trunk_multiple_with_exception(self): ) -class TestShowNetworkTrunk(TestNetworkTrunk): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() +class TestShowNetworkTrunk(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) # The trunk to set. new_trunk = network_fakes.create_one_trunk() columns = ( @@ -412,11 +404,11 @@ class TestShowNetworkTrunk(TestNetworkTrunk): def setUp(self): super().setUp() - self.network_client.find_trunk = mock.Mock(return_value=self.new_trunk) - self.network_client.get_trunk = mock.Mock(return_value=self.new_trunk) + self.network_client.find_trunk.return_value = self.new_trunk + self.network_client.get_trunk.return_value = self.new_trunk - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project + self.identity_sdk_client.find_domain.return_value = self.domain # Get the command object to test self.cmd = network_trunk.ShowNetworkTrunk(self.app, None) @@ -451,9 +443,9 @@ def test_show_all_options(self): self.assertEqual(self.data, data) -class TestListNetworkTrunk(TestNetworkTrunk): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() +class TestListNetworkTrunk(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) # Create trunks to be listed. new_trunks = network_fakes.create_trunks( { @@ -464,7 +456,7 @@ class TestListNetworkTrunk(TestNetworkTrunk): ) columns = ('ID', 'Name', 'Parent Port', 'Description') - columns_long = columns + ('Status', 'State', 'Created At', 'Updated At') + columns_long = (*columns, 'Status', 'State', 'Created At', 'Updated At') data = [] for t in new_trunks: data.append((t['id'], t['name'], t['port_id'], t['description'])) @@ -485,10 +477,10 @@ class TestListNetworkTrunk(TestNetworkTrunk): def setUp(self): super().setUp() - self.network_client.trunks = mock.Mock(return_value=self.new_trunks) + self.network_client.trunks.return_value = self.new_trunks - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project + self.identity_sdk_client.find_domain.return_value = self.domain # Get the command object to test self.cmd = network_trunk.ListNetworkTrunk(self.app, None) @@ -520,9 +512,9 @@ def test_trunk_list_long(self): self.assertEqual(self.data_long, list(data)) -class TestSetNetworkTrunk(TestNetworkTrunk): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() +class TestSetNetworkTrunk(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) trunk_networks = network_fakes.create_networks(count=2) parent_port = network_fakes.create_one_port( attrs={'project_id': project.id, 'network_id': trunk_networks[0]['id']} @@ -564,17 +556,17 @@ class TestSetNetworkTrunk(TestNetworkTrunk): def setUp(self): super().setUp() - self.network_client.update_trunk = mock.Mock(return_value=self._trunk) - self.network_client.add_trunk_subports = mock.Mock( - return_value=self._trunk - ) - self.network_client.find_trunk = mock.Mock(return_value=self._trunk) - self.network_client.find_port = mock.Mock( - side_effect=[self.sub_port, self.sub_port] - ) + self.network_client.update_trunk.return_value = self._trunk + self.network_client.add_trunk_subports.return_value = self._trunk - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.network_client.find_trunk.return_value = self._trunk + self.network_client.find_port.side_effect = [ + self.sub_port, + self.sub_port, + ] + + self.identity_sdk_client.find_project.return_value = self.project + self.identity_sdk_client.find_domain.return_value = self.domain # Get the command object to test self.cmd = network_trunk.SetNetworkTrunk(self.app, None) @@ -763,9 +755,8 @@ def test_set_trunk_attrs_with_exception(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.network_client.update_trunk = mock.Mock( - side_effect=exceptions.CommandError - ) + self.network_client.update_trunk.side_effect = exceptions.CommandError + with testtools.ExpectedException(exceptions.CommandError) as e: self.cmd.take_action(parsed_args) self.assertEqual( @@ -790,12 +781,14 @@ def test_set_trunk_add_subport_with_exception(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.network_client.add_trunk_subports = mock.Mock( - side_effect=exceptions.CommandError - ) - self.network_client.find_port = mock.Mock( - return_value={'id': 'invalid_subport'} + self.network_client.add_trunk_subports.side_effect = ( + exceptions.CommandError ) + + self.network_client.find_port.side_effect = [ + network_fakes.create_one_port({'id': 'invalid_subport'}) + ] + with testtools.ExpectedException(exceptions.CommandError) as e: self.cmd.take_action(parsed_args) self.assertEqual( @@ -810,7 +803,7 @@ def test_set_trunk_add_subport_with_exception(self): ) -class TestListNetworkSubport(TestNetworkTrunk): +class TestListNetworkSubport(network_fakes.TestNetworkV2): _trunk = network_fakes.create_one_trunk() _subports = _trunk['sub_ports'] @@ -832,10 +825,10 @@ class TestListNetworkSubport(TestNetworkTrunk): def setUp(self): super().setUp() - self.network_client.find_trunk = mock.Mock(return_value=self._trunk) - self.network_client.get_trunk_subports = mock.Mock( - return_value={network_trunk.SUB_PORTS: self._subports} - ) + self.network_client.find_trunk.return_value = self._trunk + self.network_client.get_trunk_subports.return_value = { + network_trunk.SUB_PORTS: self._subports + } # Get the command object to test self.cmd = network_trunk.ListNetworkSubport(self.app, None) @@ -858,9 +851,9 @@ def test_subport_list(self): self.assertEqual(self.data, list(data)) -class TestUnsetNetworkTrunk(TestNetworkTrunk): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() +class TestUnsetNetworkTrunk(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) trunk_networks = network_fakes.create_networks(count=2) parent_port = network_fakes.create_one_port( attrs={'project_id': project.id, 'network_id': trunk_networks[0]['id']} @@ -902,13 +895,13 @@ class TestUnsetNetworkTrunk(TestNetworkTrunk): def setUp(self): super().setUp() - self.network_client.find_trunk = mock.Mock(return_value=self._trunk) - self.network_client.find_port = mock.Mock( - side_effect=[self.sub_port, self.sub_port] - ) - self.network_client.delete_trunk_subports = mock.Mock( - return_value=None - ) + self.network_client.find_trunk.return_value = self._trunk + self.network_client.find_port.side_effect = [ + self.sub_port, + self.sub_port, + ] + + self.network_client.delete_trunk_subports.return_value = None # Get the command object to test self.cmd = network_trunk.UnsetNetworkTrunk(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index 9221f2fa38..1adbdca59e 100644 --- a/openstackclient/tests/unit/network/v2/test_port.py +++ b/openstackclient/tests/unit/network/v2/test_port.py @@ -14,12 +14,13 @@ from unittest.mock import call import uuid +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from openstackclient.network.v2 import port from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as test_utils @@ -37,9 +38,6 @@ class TestPort(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - @staticmethod def _get_common_cols_data(fake_port): columns = ( @@ -72,6 +70,8 @@ def _get_common_cols_data(fake_port): 'port_security_enabled', 'project_id', 'propagate_uplink_status', + 'pvlan_type', + 'pvlan_community', 'resource_request', 'revision_number', 'qos_network_policy_id', @@ -114,6 +114,8 @@ def _get_common_cols_data(fake_port): fake_port.is_port_security_enabled, fake_port.project_id, fake_port.propagate_uplink_status, + fake_port.pvlan_type, + fake_port.pvlan_community, fake_port.resource_request, fake_port.revision_number, fake_port.qos_network_policy_id, @@ -136,19 +138,18 @@ class TestCreatePort(TestPort): def setUp(self): super().setUp() - self.network_client.create_port = mock.Mock(return_value=self._port) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.create_port.return_value = self._port + self.network_client.set_tags.return_value = None fake_net = network_fakes.create_one_network( { 'id': self._port.network_id, } ) - self.network_client.find_network = mock.Mock(return_value=fake_net) + self.network_client.find_network.return_value = fake_net self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet() - self.network_client.find_subnet = mock.Mock( - return_value=self.fake_subnet - ) - self.network_client.find_extension = mock.Mock(return_value=[]) + self.network_client.find_subnet.return_value = self.fake_subnet + + self.network_client.find_extension.return_value = [] # Get the command object to test self.cmd = port.CreatePort(self.app, None) @@ -208,6 +209,10 @@ def test_create_full_options(self): '--dns-name', '8.8.8.8', 'test-port', + '--pvlan-type', + network_fakes.PVLAN_TYPE_COMMUNITY, + '--pvlan-community', + network_fakes.PVLAN_COMMUNITY_NAME, ] verifylist = [ ('mac_address', 'aa:aa:aa:aa:aa:aa'), @@ -225,6 +230,8 @@ def test_create_full_options(self): ('dns_domain', 'example.org'), ('dns_name', '8.8.8.8'), ('name', 'test-port'), + ('pvlan_type', network_fakes.PVLAN_TYPE_COMMUNITY), + ('pvlan_community', network_fakes.PVLAN_COMMUNITY_NAME), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -248,6 +255,8 @@ def test_create_full_options(self): 'network_id': self._port.network_id, 'dns_domain': 'example.org', 'dns_name': '8.8.8.8', + 'pvlan_type': network_fakes.PVLAN_TYPE_COMMUNITY, + 'pvlan_community': network_fakes.PVLAN_COMMUNITY_NAME, 'name': 'test-port', } ) @@ -321,9 +330,8 @@ def test_create_json_binding_profile(self): def test_create_with_security_group(self): secgroup = network_fakes.create_one_security_group() - self.network_client.find_security_group = mock.Mock( - return_value=secgroup - ) + self.network_client.find_security_group.return_value = secgroup + arglist = [ '--network', self._port.network_id, @@ -393,9 +401,8 @@ def test_create_port_with_dns_name(self): def test_create_with_security_groups(self): sg_1 = network_fakes.create_one_security_group() sg_2 = network_fakes.create_one_security_group() - self.network_client.find_security_group = mock.Mock( - side_effect=[sg_1, sg_2] - ) + self.network_client.find_security_group.side_effect = [sg_1, sg_2] + arglist = [ '--network', self._port.network_id, @@ -583,9 +590,8 @@ def test_create_port_with_allowed_address_pair(self): def test_create_port_with_qos(self): qos_policy = network_fakes.create_one_qos_policy() - self.network_client.find_qos_policy = mock.Mock( - return_value=qos_policy - ) + self.network_client.find_qos_policy.return_value = qos_policy + arglist = [ '--network', self._port.network_id, @@ -701,9 +707,7 @@ def _test_create_with_tag(self, add_tags=True, add_tags_in_post=True): else: verifylist.append(('no_tag', True)) - self.network_client.find_extension = mock.Mock( - return_value=add_tags_in_post - ) + self.network_client.find_extension.return_value = add_tags_in_post parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -1162,6 +1166,78 @@ def test_create_with_trusted_true(self): def test_create_with_trusted_false(self): self._test_create_with_trusted_field(False) + def test_create_pvlan_community_without_name(self): + arglist = [ + '--network', + self._port.network_id, + '--pvlan-type', + 'community', + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id), + ('enable', True), + ('pvlan_type', 'community'), + ('name', 'test-port'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + def test_create_pvlan_with_port_security_disabled(self): + arglist = [ + '--network', + self._port.network_id, + '--disable-port-security', + '--pvlan-type', + 'isolated', + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id), + ('enable', True), + ('disable_port_security', True), + ('pvlan_type', 'isolated'), + ('name', 'test-port'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + def test_create_pvlan_on_non_pvlan_network(self): + fake_net = network_fakes.create_one_network( + {'id': self._port.network_id, 'pvlan': False} + ) + self.network_client.find_network.return_value = fake_net + arglist = [ + '--network', + self._port.network_id, + '--pvlan-type', + 'isolated', + 'test-port', + ] + verifylist = [ + ('network', self._port.network_id), + ('enable', True), + ('pvlan_type', 'isolated'), + ('name', 'test-port'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + class TestDeletePort(TestPort): # Ports to delete. @@ -1170,7 +1246,7 @@ class TestDeletePort(TestPort): def setUp(self): super().setUp() - self.network_client.delete_port = mock.Mock(return_value=None) + self.network_client.delete_port.return_value = None self.network_client.find_port = network_fakes.get_ports( ports=self._ports ) @@ -1223,7 +1299,7 @@ def test_multi_ports_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self._ports[0], exceptions.CommandError] - self.network_client.find_port = mock.Mock(side_effect=find_mock_result) + self.network_client.find_port.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -1241,7 +1317,7 @@ def test_multi_ports_delete_with_exception(self): class TestListPort(compute_fakes.FakeClientMixin, TestPort): - _project = identity_fakes.FakeProject.create_one_project() + _project = sdk_fakes.generate_fake_resource(_project.Project) _networks = network_fakes.create_networks(count=3) _sport1 = network_fakes.create_one_port( attrs={'project_id': _project.id, 'network_id': _networks[1]['id']} @@ -1324,7 +1400,7 @@ class TestListPort(compute_fakes.FakeClientMixin, TestPort): def setUp(self): super().setUp() - self.network_client.ports = mock.Mock(return_value=self._ports) + self.network_client.ports.return_value = self._ports fake_router = network_fakes.create_one_router( { 'id': 'fake-router-id', @@ -1335,8 +1411,8 @@ def setUp(self): 'id': 'fake-network-id', } ) - self.network_client.find_router = mock.Mock(return_value=fake_router) - self.network_client.find_network = mock.Mock(return_value=fake_network) + self.network_client.find_router.return_value = fake_router + self.network_client.find_network.return_value = fake_network # Get the command object to test self.cmd = port.ListPort(self.app, None) @@ -1355,6 +1431,28 @@ def test_port_list_no_options(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) + def test_port_list_pagination(self): + arglist = [ + '--marker', + self._ports[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self._ports[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.ports.assert_called_once_with( + fields=LIST_FIELDS_TO_RETRIEVE, + **{'marker': self._ports[0].id, 'limit': 1}, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_port_list_router_opt(self): arglist = [ '--router', @@ -1550,9 +1648,8 @@ def test_port_list_fixed_ip_opt_subnet_id(self): self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet( {'id': subnet_id} ) - self.network_client.find_subnet = mock.Mock( - return_value=self.fake_subnet - ) + self.network_client.find_subnet.return_value = self.fake_subnet + parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -1579,9 +1676,8 @@ def test_port_list_fixed_ip_opts(self): self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet( {'id': subnet_id} ) - self.network_client.find_subnet = mock.Mock( - return_value=self.fake_subnet - ) + self.network_client.find_subnet.return_value = self.fake_subnet + parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -1616,9 +1712,8 @@ def test_port_list_fixed_ips(self): 'fields': LIST_FIELDS_TO_RETRIEVE, } ) - self.network_client.find_subnet = mock.Mock( - return_value=self.fake_subnet - ) + self.network_client.find_subnet.return_value = self.fake_subnet + parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -1674,8 +1769,8 @@ def test_port_list_host(self): self.assertCountEqual(self.data, list(data)) def test_port_list_project(self): - project = identity_fakes.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -1696,8 +1791,8 @@ def test_port_list_project(self): self.assertCountEqual(self.data, list(data)) def test_port_list_project_domain(self): - project = identity_fakes.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -1795,6 +1890,130 @@ def test_port_list_security_group(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) + def test_port_list_pvlan_type(self): + arglist = [ + '--pvlan-type', + 'community', + ] + verifylist = [ + ('pvlan_type', 'community'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, _data = self.cmd.take_action(parsed_args) + expected_fields = [ + *LIST_FIELDS_TO_RETRIEVE, + 'pvlan_type', + 'pvlan_community', + ] + filters = { + 'pvlan_type': 'community', + 'fields': expected_fields, + } + + self.network_client.ports.assert_called_once_with(**filters) + expected_columns = [*self.columns, 'PVLAN Type', 'PVLAN Community'] + self.assertEqual(expected_columns, columns) + + def test_port_list_pvlan_community(self): + arglist = [ + '--pvlan-community', + 'community_1', + ] + verifylist = [ + ('pvlan_community', 'community_1'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, _data = self.cmd.take_action(parsed_args) + expected_fields = [ + *LIST_FIELDS_TO_RETRIEVE, + 'pvlan_type', + 'pvlan_community', + ] + filters = { + 'pvlan_community': 'community_1', + 'fields': expected_fields, + } + + self.network_client.ports.assert_called_once_with(**filters) + expected_columns = [*self.columns, 'PVLAN Type', 'PVLAN Community'] + self.assertEqual(expected_columns, columns) + + def test_port_list_pvlan(self): + port_pvlan = network_fakes.create_one_port( + attrs={ + 'pvlan_type': 'community', + 'pvlan_community': 'community_1', + } + ) + port_no_pvlan = network_fakes.create_one_port( + attrs={'pvlan_type': None, 'pvlan_community': None} + ) + self.network_client.ports.return_value = [port_pvlan, port_no_pvlan] + + arglist = [ + '--pvlan', + ] + verifylist = [ + ('pvlan', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + expected_fields = [ + *LIST_FIELDS_TO_RETRIEVE, + 'pvlan_type', + 'pvlan_community', + ] + + self.network_client.ports.assert_called_once_with( + fields=expected_fields + ) + expected_columns = [*self.columns, 'PVLAN Type', 'PVLAN Community'] + self.assertEqual(expected_columns, columns) + result = list(data) + self.assertEqual(1, len(result)) + # First column is the port ID + self.assertEqual(port_pvlan.id, result[0][0]) + + def test_port_list_no_pvlan(self): + port_pvlan = network_fakes.create_one_port( + attrs={ + 'pvlan_type': 'community', + 'pvlan_community': 'community_1', + } + ) + port_no_pvlan = network_fakes.create_one_port( + attrs={'pvlan_type': None, 'pvlan_community': None} + ) + self.network_client.ports.return_value = [port_pvlan, port_no_pvlan] + + arglist = [ + '--no-pvlan', + ] + verifylist = [ + ('no_pvlan', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + expected_fields = [ + *LIST_FIELDS_TO_RETRIEVE, + 'pvlan_type', + 'pvlan_community', + ] + + self.network_client.ports.assert_called_once_with( + fields=expected_fields + ) + expected_columns = [*self.columns, 'PVLAN Type', 'PVLAN Community'] + self.assertEqual(expected_columns, columns) + result = list(data) + self.assertEqual(1, len(result)) + # First column is the port ID + self.assertEqual(port_no_pvlan.id, result[0][0]) + def test_port_list_status(self): arglist = [ '--status', @@ -1825,12 +2044,11 @@ class TestSetPort(TestPort): def setUp(self): super().setUp() self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet() - self.network_client.find_subnet = mock.Mock( - return_value=self.fake_subnet - ) - self.network_client.find_port = mock.Mock(return_value=self._port) - self.network_client.update_port = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.find_subnet.return_value = self.fake_subnet + + self.network_client.find_port.return_value = self._port + self.network_client.update_port.return_value = None + self.network_client.set_tags.return_value = None # Get the command object to test self.cmd = port.SetPort(self.app, None) @@ -1853,7 +2071,7 @@ def test_set_port_fixed_ip(self): _testport = network_fakes.create_one_port( {'fixed_ips': [{'ip_address': '0.0.0.1'}]} ) - self.network_client.find_port = mock.Mock(return_value=_testport) + self.network_client.find_port.return_value = _testport arglist = [ '--fixed-ip', 'ip-address=10.0.0.12', @@ -1881,7 +2099,7 @@ def test_set_port_fixed_ip_clear(self): _testport = network_fakes.create_one_port( {'fixed_ips': [{'ip_address': '0.0.0.1'}]} ) - self.network_client.find_port = mock.Mock(return_value=_testport) + self.network_client.find_port.return_value = _testport arglist = [ '--fixed-ip', 'ip-address=10.0.0.12', @@ -1931,7 +2149,7 @@ def test_set_port_overwrite_binding_profile(self): _testport = network_fakes.create_one_port( {'binding_profile': {'lok_i': 'visi_on'}} ) - self.network_client.find_port = mock.Mock(return_value=_testport) + self.network_client.find_port.return_value = _testport arglist = [ '--binding-profile', 'lok_i=than_os', @@ -1956,7 +2174,7 @@ def test_overwrite_mac_address(self): _testport = network_fakes.create_one_port( {'mac_address': '11:22:33:44:55:66'} ) - self.network_client.find_port = mock.Mock(return_value=_testport) + self.network_client.find_port.return_value = _testport arglist = [ '--mac-address', '66:55:44:33:22:11', @@ -2097,7 +2315,7 @@ def test_set_port_mixed_binding_profile(self): def test_set_port_security_group(self): sg = network_fakes.create_one_security_group() - self.network_client.find_security_group = mock.Mock(return_value=sg) + self.network_client.find_security_group.return_value = sg arglist = [ '--security-group', sg.id, @@ -2122,13 +2340,12 @@ def test_set_port_security_group_append(self): sg_1 = network_fakes.create_one_security_group() sg_2 = network_fakes.create_one_security_group() sg_3 = network_fakes.create_one_security_group() - self.network_client.find_security_group = mock.Mock( - side_effect=[sg_2, sg_3] - ) + self.network_client.find_security_group.side_effect = [sg_2, sg_3] + _testport = network_fakes.create_one_port( {'security_group_ids': [sg_1.id]} ) - self.network_client.find_port = mock.Mock(return_value=_testport) + self.network_client.find_port.return_value = _testport arglist = [ '--security-group', sg_2.id, @@ -2177,8 +2394,8 @@ def test_set_port_security_group_replace(self): _testport = network_fakes.create_one_port( {'security_group_ids': [sg1.id]} ) - self.network_client.find_port = mock.Mock(return_value=_testport) - self.network_client.find_security_group = mock.Mock(return_value=sg2) + self.network_client.find_port.return_value = _testport + self.network_client.find_security_group.return_value = sg2 arglist = [ '--security-group', sg2.id, @@ -2226,7 +2443,7 @@ def test_set_port_append_allowed_address_pair(self): _testport = network_fakes.create_one_port( {'allowed_address_pairs': [{'ip_address': '192.168.1.123'}]} ) - self.network_client.find_port = mock.Mock(return_value=_testport) + self.network_client.find_port.return_value = _testport arglist = [ '--allowed-address', 'ip-address=192.168.1.45', @@ -2255,7 +2472,7 @@ def test_set_port_overwrite_allowed_address_pair(self): _testport = network_fakes.create_one_port( {'allowed_address_pairs': [{'ip_address': '192.168.1.123'}]} ) - self.network_client.find_port = mock.Mock(return_value=_testport) + self.network_client.find_port.return_value = _testport arglist = [ '--allowed-address', 'ip-address=192.168.1.45', @@ -2372,11 +2589,10 @@ def test_set_port_security_disabled(self): def test_set_port_with_qos(self): qos_policy = network_fakes.create_one_qos_policy() - self.network_client.find_qos_policy = mock.Mock( - return_value=qos_policy - ) + self.network_client.find_qos_policy.return_value = qos_policy + _testport = network_fakes.create_one_port({'qos_policy_id': None}) - self.network_client.find_port = mock.Mock(return_value=_testport) + self.network_client.find_port.return_value = _testport arglist = [ '--qos-policy', qos_policy.id, @@ -2400,7 +2616,7 @@ def test_set_port_with_qos(self): def test_set_port_data_plane_status(self): _testport = network_fakes.create_one_port({'data_plane_status': None}) - self.network_client.find_port = mock.Mock(return_value=_testport) + self.network_client.find_port.return_value = _testport arglist = [ '--data-plane-status', 'ACTIVE', @@ -2546,10 +2762,12 @@ def test_set_hints_invalid_value(self): def test_set_hints_valid_alias_value(self): testport = network_fakes.create_one_port() - self.network_client.find_port = mock.Mock(return_value=testport) - self.network_client.find_extension = mock.Mock( - return_value=['port-hints', 'port-hint-ovs-tx-steering'] - ) + self.network_client.find_port.return_value = testport + self.network_client.find_extension.return_value = [ + 'port-hints', + 'port-hint-ovs-tx-steering', + ] + arglist = [ '--hint', 'ovs-tx-steering=hash', @@ -2574,10 +2792,12 @@ def test_set_hints_valid_alias_value(self): def test_set_hints_valid_json(self): testport = network_fakes.create_one_port() - self.network_client.find_port = mock.Mock(return_value=testport) - self.network_client.find_extension = mock.Mock( - return_value=['port-hints', 'port-hint-ovs-tx-steering'] - ) + self.network_client.find_port.return_value = testport + self.network_client.find_extension.return_value = [ + 'port-hints', + 'port-hint-ovs-tx-steering', + ] + arglist = [ '--hint', '{"openvswitch": {"other_config": {"tx-steering": "hash"}}}', @@ -2661,6 +2881,66 @@ def test_set_uplink_status_propagation_true(self): def test_set_uplink_status_propagation_false(self): self._test_set_uplink_status_propagation(False) + def test_set_pvlan_community_without_name(self): + arglist = [ + '--pvlan-type', + 'community', + self._port.name, + ] + verifylist = [ + ('pvlan_type', 'community'), + ('port', self._port.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + def test_set_pvlan_with_port_security_disabled(self): + arglist = [ + '--disable-port-security', + '--pvlan-type', + 'isolated', + self._port.name, + ] + verifylist = [ + ('disable_port_security', True), + ('pvlan_type', 'isolated'), + ('port', self._port.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + def test_set_pvlan_on_non_pvlan_network(self): + fake_net = network_fakes.create_one_network( + {'id': self._port.network_id, 'pvlan': False} + ) + self.network_client.find_network.return_value = fake_net + arglist = [ + '--pvlan-type', + 'isolated', + self._port.name, + ] + verifylist = [ + ('pvlan_type', 'isolated'), + ('port', self._port.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + class TestShowPort(TestPort): # The port to show. @@ -2670,7 +2950,7 @@ class TestShowPort(TestPort): def setUp(self): super().setUp() - self.network_client.find_port = mock.Mock(return_value=self._port) + self.network_client.find_port.return_value = self._port # Get the command object to test self.cmd = port.ShowPort(self.app, None) @@ -2731,12 +3011,11 @@ def setUp(self): self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet( {'id': '042eb10a-3a18-4658-ab-cf47c8d03152'} ) - self.network_client.find_subnet = mock.Mock( - return_value=self.fake_subnet - ) - self.network_client.find_port = mock.Mock(return_value=self._testport) - self.network_client.update_port = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.find_subnet.return_value = self.fake_subnet + + self.network_client.find_port.return_value = self._testport + self.network_client.update_port.return_value = None + self.network_client.set_tags.return_value = None # Get the command object to test self.cmd = port.UnsetPort(self.app, None) @@ -2826,10 +3105,9 @@ def test_unset_security_group(self): _fake_port = network_fakes.create_one_port( {'security_group_ids': [_fake_sg1.id, _fake_sg2.id]} ) - self.network_client.find_port = mock.Mock(return_value=_fake_port) - self.network_client.find_security_group = mock.Mock( - return_value=_fake_sg2 - ) + self.network_client.find_port.return_value = _fake_port + self.network_client.find_security_group.return_value = _fake_sg2 + arglist = [ '--security-group', _fake_sg2.id, @@ -2854,9 +3132,8 @@ def test_unset_port_security_group_not_existent(self): _fake_port = network_fakes.create_one_port( {'security_group_ids': [_fake_sg1.id]} ) - self.network_client.find_security_group = mock.Mock( - return_value=_fake_sg2 - ) + self.network_client.find_security_group.return_value = _fake_sg2 + arglist = [ '--security-group', _fake_sg2.id, @@ -2875,7 +3152,7 @@ def test_unset_port_allowed_address_pair(self): _fake_port = network_fakes.create_one_port( {'allowed_address_pairs': [{'ip_address': '192.168.1.123'}]} ) - self.network_client.find_port = mock.Mock(return_value=_fake_port) + self.network_client.find_port.return_value = _fake_port arglist = [ '--allowed-address', 'ip-address=192.168.1.123', @@ -2901,7 +3178,7 @@ def test_unset_port_allowed_address_pair_not_existent(self): _fake_port = network_fakes.create_one_port( {'allowed_address_pairs': [{'ip_address': '192.168.1.123'}]} ) - self.network_client.find_port = mock.Mock(return_value=_fake_port) + self.network_client.find_port.return_value = _fake_port arglist = [ '--allowed-address', 'ip-address=192.168.1.45', @@ -2920,7 +3197,7 @@ def test_unset_port_data_plane_status(self): _fake_port = network_fakes.create_one_port( {'data_plane_status': 'ACTIVE'} ) - self.network_client.find_port = mock.Mock(return_value=_fake_port) + self.network_client.find_port.return_value = _fake_port arglist = [ '--data-plane-status', _fake_port.name, @@ -2973,7 +3250,7 @@ def test_unset_numa_affinity_policy(self): _fake_port = network_fakes.create_one_port( {'numa_affinity_policy': 'required'} ) - self.network_client.find_port = mock.Mock(return_value=_fake_port) + self.network_client.find_port.return_value = _fake_port arglist = [ '--numa-policy', _fake_port.name, @@ -2997,7 +3274,7 @@ def test_unset_numa_affinity_policy(self): def test_unset_hints(self): testport = network_fakes.create_one_port() - self.network_client.find_port = mock.Mock(return_value=testport) + self.network_client.find_port.return_value = testport arglist = [ '--hints', testport.name, @@ -3014,3 +3291,43 @@ def test_unset_hints(self): **{'hints': None}, ) self.assertIsNone(result) + + def test_unset_device(self): + testport = network_fakes.create_one_port() + self.network_client.find_port.return_value = testport + arglist = [ + '--device', + testport.name, + ] + verifylist = [ + ('device', True), + ('port', testport.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_port.assert_called_once_with( + testport, + **{'device_id': ''}, + ) + self.assertIsNone(result) + + def test_unset_device_owner(self): + testport = network_fakes.create_one_port() + self.network_client.find_port.return_value = testport + arglist = [ + '--device-owner', + testport.name, + ] + verifylist = [ + ('device_owner', True), + ('port', testport.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_port.assert_called_once_with( + testport, + **{'device_owner': ''}, + ) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 3536583305..db2057a7a7 100644 --- a/openstackclient/tests/unit/network/v2/test_router.py +++ b/openstackclient/tests/unit/network/v2/test_router.py @@ -9,29 +9,22 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# from unittest import mock from unittest.mock import call +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from openstackclient.network.v2 import router -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils -class TestRouter(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - self.projects_mock = self.identity_client.projects - - -class TestAddPortToRouter(TestRouter): - '''Add port to Router''' +class TestAddPortToRouter(network_fakes.TestNetworkV2): + """Add port to Router""" _port = network_fakes.create_one_port() _router = network_fakes.create_one_router(attrs={'port': _port.id}) @@ -39,8 +32,8 @@ class TestAddPortToRouter(TestRouter): def setUp(self): super().setUp() - self.network_client.find_router = mock.Mock(return_value=self._router) - self.network_client.find_port = mock.Mock(return_value=self._port) + self.network_client.find_router.return_value = self._router + self.network_client.find_port.return_value = self._port self.cmd = router.AddPortToRouter(self.app, None) @@ -70,16 +63,13 @@ def test_add_port_required_options(self): result = self.cmd.take_action(parsed_args) self.network_client.add_interface_to_router.assert_called_with( - self._router, - **{ - 'port_id': self._router.port, - }, + self._router, port=self._router.port ) self.assertIsNone(result) -class TestAddSubnetToRouter(TestRouter): - '''Add subnet to Router''' +class TestAddSubnetToRouter(network_fakes.TestNetworkV2): + """Add subnet to Router""" _subnet = network_fakes.FakeSubnet.create_one_subnet() _router = network_fakes.create_one_router(attrs={'subnet': _subnet.id}) @@ -87,8 +77,9 @@ class TestAddSubnetToRouter(TestRouter): def setUp(self): super().setUp() - self.network_client.find_router = mock.Mock(return_value=self._router) - self.network_client.find_subnet = mock.Mock(return_value=self._subnet) + self.network_client.find_router.return_value = self._router + self.network_client.find_subnet.return_value = self._subnet + self._router.add_interface = mock.Mock() self.cmd = router.AddSubnetToRouter(self.app, None) @@ -117,13 +108,52 @@ def test_add_subnet_required_options(self): result = self.cmd.take_action(parsed_args) self.network_client.add_interface_to_router.assert_called_with( - self._router, **{'subnet_id': self._router.subnet} + self._router, subnet=self._router.subnet, advertise_host=False ) self.assertIsNone(result) + def test_add_subnet_with_advertise_host(self): + arglist = [ + self._router.id, + self._router.subnet, + '--advertise-host', + ] + verifylist = [ + ('router', self._router.id), + ('subnet', self._router.subnet), + ('advertise_host', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network_client.add_interface_to_router.assert_called_once_with( + self._router, subnet=self._router.subnet, advertise_host=True + ) + + self.assertIsNone(result) -class TestCreateRouter(TestRouter): + def test_add_subnet_without_advertise_host(self): + arglist = [ + self._router.id, + self._router.subnet, + ] + verifylist = [ + ('router', self._router.id), + ('subnet', self._router.subnet), + ('advertise_host', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.network_client.add_interface_to_router.assert_called_once_with( + self._router, subnet=self._subnet.id, advertise_host=False + ) + + self.assertIsNone(result) + + +class TestCreateRouter(network_fakes.TestNetworkV2): # The new router created. new_router = network_fakes.create_one_router() _extensions = {'fake': network_fakes.create_one_extension()} @@ -172,12 +202,11 @@ class TestCreateRouter(TestRouter): def setUp(self): super().setUp() - self.network_client.create_router = mock.Mock( - return_value=self.new_router - ) - self.network_client.set_tags = mock.Mock(return_value=None) - self.network_client.find_extension = mock.Mock( - side_effect=lambda name: self._extensions.get(name) + self.network_client.create_router.return_value = self.new_router + + self.network_client.set_tags.return_value = None + self.network_client.find_extension.side_effect = ( + lambda name, ignore_missing=True: self._extensions.get(name) ) # Get the command object to test self.cmd = router.CreateRouter(self.app, None) @@ -222,8 +251,8 @@ def test_create_default_options(self): def test_create_with_gateway(self): _network = network_fakes.create_one_network() _subnet = network_fakes.FakeSubnet.create_one_subnet() - self.network_client.find_network = mock.Mock(return_value=_network) - self.network_client.find_subnet = mock.Mock(return_value=_subnet) + self.network_client.find_network.return_value = _network + self.network_client.find_subnet.return_value = _subnet arglist = [ self.new_router.name, '--external-gateway', @@ -392,7 +421,7 @@ def test_create_with_no_tag(self): def test_create_with_flavor_id_id(self): _flavor = network_fakes.create_one_network_flavor() - self.network_client.find_flavor = mock.Mock(return_value=_flavor) + self.network_client.find_flavor.return_value = _flavor arglist = [ self.new_router.name, '--flavor-id', @@ -419,7 +448,7 @@ def test_create_with_flavor_id_id(self): def test_create_with_flavor_id_name(self): _flavor = network_fakes.create_one_network_flavor() - self.network_client.find_flavor = mock.Mock(return_value=_flavor) + self.network_client.find_flavor.return_value = _flavor arglist = [self.new_router.name, '--flavor-id', _flavor.name] verifylist = [ ('name', self.new_router.name), @@ -442,7 +471,7 @@ def test_create_with_flavor_id_name(self): def test_create_with_flavor_id(self): _flavor = network_fakes.create_one_network_flavor() - self.network_client.find_flavor = mock.Mock(return_value=_flavor) + self.network_client.find_flavor.return_value = _flavor arglist = [ self.new_router.name, '--flavor', @@ -469,7 +498,7 @@ def test_create_with_flavor_id(self): def test_create_with_flavor_name(self): _flavor = network_fakes.create_one_network_flavor() - self.network_client.find_flavor = mock.Mock(return_value=_flavor) + self.network_client.find_flavor.return_value = _flavor arglist = [self.new_router.name, '--flavor', _flavor.name] verifylist = [ ('name', self.new_router.name), @@ -491,11 +520,12 @@ def test_create_with_flavor_name(self): self.assertCountEqual(self.data, data) def test_create_with_enable_default_route_bfd(self): - self.network_client.find_extension = mock.Mock( - return_value=network_fakes.create_one_extension( + self._extensions = { + 'external-gateway-multihoming': network_fakes.create_one_extension( attrs={'name': 'external-gateway-multihoming'} - ) - ) + ), + } + arglist = [self.new_router.name, '--enable-default-route-bfd'] verifylist = [ ('name', self.new_router.name), @@ -525,11 +555,12 @@ def test_create_with_enable_default_route_bfd_no_extension(self): ) def test_create_with_enable_default_route_ecmp(self): - self.network_client.find_extension = mock.Mock( - return_value=network_fakes.create_one_extension( + self._extensions = { + 'external-gateway-multihoming': network_fakes.create_one_extension( attrs={'name': 'external-gateway-multihoming'} - ) - ) + ), + } + arglist = [self.new_router.name, '--enable-default-route-ecmp'] verifylist = [ ('name', self.new_router.name), @@ -560,11 +591,10 @@ def test_create_with_enable_default_route_ecmp_no_extension(self): def test_create_with_qos_policy(self): _network = network_fakes.create_one_network() - self.network_client.find_network = mock.Mock(return_value=_network) + self.network_client.find_network.return_value = _network _qos_policy = network_fakes.create_one_qos_policy() - self.network_client.find_qos_policy = mock.Mock( - return_value=_qos_policy - ) + self.network_client.find_qos_policy.return_value = _qos_policy + arglist = [ self.new_router.name, '--external-gateway', @@ -595,9 +625,8 @@ def test_create_with_qos_policy(self): def test_create_with_qos_policy_no_external_gateway(self): _qos_policy = network_fakes.create_one_qos_policy() - self.network_client.find_qos_policy = mock.Mock( - return_value=_qos_policy - ) + self.network_client.find_qos_policy.return_value = _qos_policy + arglist = [ self.new_router.name, '--qos-policy', @@ -617,15 +646,159 @@ def test_create_with_qos_policy_no_external_gateway(self): parsed_args, ) + def test_create_with_evpn_vni_auto(self): + arglist = [ + '--auto-evpn-vni', + self.new_router.name, + ] + verifylist = [ + ('name', self.new_router.name), + ('enable', True), + ('distributed', False), + ('ha', False), + ('evpn_vni', 0), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.create_router.assert_called_once_with( + **{ + 'admin_state_up': True, + 'name': self.new_router.name, + 'evpn_vni': 0, + } + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_create_with_evpn_vni_explicit(self): + arglist = [ + '--evpn-vni', + '10000', + self.new_router.name, + ] + verifylist = [ + ('name', self.new_router.name), + ('enable', True), + ('distributed', False), + ('ha', False), + ('evpn_vni', 10000), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.create_router.assert_called_once_with( + **{ + 'admin_state_up': True, + 'name': self.new_router.name, + 'evpn_vni': 10000, + } + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_create_without_evpn_vni(self): + arglist = [ + self.new_router.name, + ] + verifylist = [ + ('name', self.new_router.name), + ('enable', True), + ('distributed', False), + ('ha', False), + ('evpn_vni', None), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + _columns, _data = self.cmd.take_action(parsed_args) + + self.network_client.create_router.assert_called_once_with( + **{ + 'admin_state_up': True, + 'name': self.new_router.name, + } + ) + self.assertNotIn( + 'evpn_vni', + self.network_client.create_router.call_args[1], + ) + + def test_create_with_evpn_vni_invalid_string(self): + arglist = [ + '--evpn-vni', + 'foo', + self.new_router.name, + ] + verifylist = [] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_evpn_vni_zero(self): + arglist = [ + '--evpn-vni', + '0', + self.new_router.name, + ] + verifylist = [] -class TestDeleteRouter(TestRouter): + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_evpn_vni_negative(self): + arglist = [ + '--evpn-vni', + '-1', + self.new_router.name, + ] + verifylist = [] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_evpn_vni_mutually_exclusive_args(self): + arglist = [ + '--evpn-vni', + '10000', + '--auto-evpn-vni', + self.new_router.name, + ] + verifylist = [] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + +class TestDeleteRouter(network_fakes.TestNetworkV2): # The routers to delete. _routers = network_fakes.create_routers(count=2) def setUp(self): super().setUp() - self.network_client.delete_router = mock.Mock(return_value=None) + self.network_client.delete_router.return_value = None self.network_client.find_router = network_fakes.get_routers( self._routers @@ -679,9 +852,7 @@ def test_multi_routers_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self._routers[0], exceptions.CommandError] - self.network_client.find_router = mock.Mock( - side_effect=find_mock_result - ) + self.network_client.find_router.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -700,7 +871,7 @@ def test_multi_routers_delete_with_exception(self): ) -class TestListRouter(TestRouter): +class TestListRouter(network_fakes.TestNetworkV2): # The routers going to be listed up. routers = network_fakes.create_routers(count=3) extensions = network_fakes.create_one_extension() @@ -714,17 +885,14 @@ class TestListRouter(TestRouter): 'Distributed', 'HA', ) - columns_long = columns + ( + columns_long = ( + *columns, 'Routes', 'External gateway info', 'Availability zones', 'Tags', ) - columns_long_no_az = columns + ( - 'Routes', - 'External gateway info', - 'Tags', - ) + columns_long_no_az = (*columns, 'Routes', 'External gateway info', 'Tags') data = [] for r in routers: @@ -786,21 +954,16 @@ def setUp(self): # Get the command object to test self.cmd = router.ListRouter(self.app, None) - self.network_client.agent_hosted_routers = mock.Mock( - return_value=self.routers - ) - self.network_client.routers = mock.Mock(return_value=self.routers) - self.network_client.find_extension = mock.Mock( - return_value=self.extensions - ) - self.network_client.find_router = mock.Mock( - return_value=self.routers[0] - ) + self.network_client.agent_hosted_routers.return_value = self.routers + + self.network_client.routers.return_value = self.routers + self.network_client.find_extension.return_value = self.extensions + + self.network_client.find_router.return_value = self.routers[0] + self._testagent = network_fakes.create_one_network_agent() - self.network_client.get_agent = mock.Mock(return_value=self._testagent) - self.network_client.get_router = mock.Mock( - return_value=self.routers[0] - ) + self.network_client.get_agent.return_value = self._testagent + self.network_client.get_router.return_value = self.routers[0] def test_router_list_no_options(self): arglist = [] @@ -818,6 +981,30 @@ def test_router_list_no_options(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) + def test_router_list_pagination(self): + arglist = [ + '--marker', + self.routers[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self.routers[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.routers.assert_called_once_with( + **{ + 'marker': self.routers[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_router_list_no_ha_no_distributed(self): _routers = network_fakes.create_routers( {'ha': None, 'distributed': None}, count=3 @@ -832,7 +1019,7 @@ def test_router_list_no_ha_no_distributed(self): with mock.patch.object( self.network_client, "routers", return_value=_routers ): - columns, data = self.cmd.take_action(parsed_args) + columns, _data = self.cmd.take_action(parsed_args) self.assertNotIn("is_distributed", columns) self.assertNotIn("is_ha", columns) @@ -865,7 +1052,7 @@ def test_router_list_long_no_az(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) # to mock, that no availability zone - self.network_client.find_extension = mock.Mock(return_value=None) + self.network_client.find_extension.return_value = None # In base command class Lister in cliff, abstract method take_action() # returns a tuple containing the column names and an iterable @@ -928,8 +1115,8 @@ def test_router_list_disable(self): self.assertCountEqual(self.data, list(data)) def test_router_list_project(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -947,8 +1134,8 @@ def test_router_list_project(self): self.assertCountEqual(self.data, list(data)) def test_router_list_project_domain(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -1035,8 +1222,8 @@ def test_list_with_tag_options(self): self.assertCountEqual(self.data, list(data)) -class TestRemovePortFromRouter(TestRouter): - '''Remove port from a Router''' +class TestRemovePortFromRouter(network_fakes.TestNetworkV2): + """Remove port from a Router""" _port = network_fakes.create_one_port() _router = network_fakes.create_one_router(attrs={'port': _port.id}) @@ -1044,8 +1231,8 @@ class TestRemovePortFromRouter(TestRouter): def setUp(self): super().setUp() - self.network_client.find_router = mock.Mock(return_value=self._router) - self.network_client.find_port = mock.Mock(return_value=self._port) + self.network_client.find_router.return_value = self._router + self.network_client.find_port.return_value = self._port self.cmd = router.RemovePortFromRouter(self.app, None) @@ -1075,13 +1262,13 @@ def test_remove_port_required_options(self): result = self.cmd.take_action(parsed_args) self.network_client.remove_interface_from_router.assert_called_with( - self._router, **{'port_id': self._router.port} + self._router, port=self._router.port ) self.assertIsNone(result) -class TestRemoveSubnetFromRouter(TestRouter): - '''Remove subnet from Router''' +class TestRemoveSubnetFromRouter(network_fakes.TestNetworkV2): + """Remove subnet from Router""" _subnet = network_fakes.FakeSubnet.create_one_subnet() _router = network_fakes.create_one_router(attrs={'subnet': _subnet.id}) @@ -1089,8 +1276,8 @@ class TestRemoveSubnetFromRouter(TestRouter): def setUp(self): super().setUp() - self.network_client.find_router = mock.Mock(return_value=self._router) - self.network_client.find_subnet = mock.Mock(return_value=self._subnet) + self.network_client.find_router.return_value = self._router + self.network_client.find_subnet.return_value = self._subnet self.cmd = router.RemoveSubnetFromRouter(self.app, None) @@ -1119,21 +1306,22 @@ def test_remove_subnet_required_options(self): result = self.cmd.take_action(parsed_args) self.network_client.remove_interface_from_router.assert_called_with( - self._router, **{'subnet_id': self._router.subnet} + self._router, subnet=self._router.subnet ) self.assertIsNone(result) -class TestAddExtraRoutesToRouter(TestRouter): +class TestAddExtraRoutesToRouter(network_fakes.TestNetworkV2): _router = network_fakes.create_one_router() def setUp(self): super().setUp() - self.network_client.add_extra_routes_to_router = mock.Mock( - return_value=self._router + self.network_client.add_extra_routes_to_router.return_value = ( + self._router ) + self.cmd = router.AddExtraRoutesToRouter(self.app, None) - self.network_client.find_router = mock.Mock(return_value=self._router) + self.network_client.find_router.return_value = self._router def test_add_no_extra_route(self): arglist = [ @@ -1213,16 +1401,17 @@ def test_add_multiple_extra_routes(self): self.assertEqual(2, len(result)) -class TestRemoveExtraRoutesFromRouter(TestRouter): +class TestRemoveExtraRoutesFromRouter(network_fakes.TestNetworkV2): _router = network_fakes.create_one_router() def setUp(self): super().setUp() - self.network_client.remove_extra_routes_from_router = mock.Mock( - return_value=self._router + self.network_client.remove_extra_routes_from_router.return_value = ( + self._router ) + self.cmd = router.RemoveExtraRoutesFromRouter(self.app, None) - self.network_client.find_router = mock.Mock(return_value=self._router) + self.network_client.find_router.return_value = self._router def test_remove_no_extra_route(self): arglist = [ @@ -1302,7 +1491,7 @@ def test_remove_multiple_extra_routes(self): self.assertEqual(2, len(result)) -class TestSetRouter(TestRouter): +class TestSetRouter(network_fakes.TestNetworkV2): # The router to set. _default_route = {'destination': '10.20.20.0/24', 'nexthop': '10.20.30.1'} _network = network_fakes.create_one_network() @@ -1316,15 +1505,14 @@ class TestSetRouter(TestRouter): def setUp(self): super().setUp() - self.network_client.update_router = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) - self.network_client.find_router = mock.Mock(return_value=self._router) - self.network_client.find_network = mock.Mock( - return_value=self._network - ) - self.network_client.find_subnet = mock.Mock(return_value=self._subnet) - self.network_client.find_extension = mock.Mock( - side_effect=lambda name: self._extensions.get(name) + self.network_client.update_router.return_value = None + self.network_client.set_tags.return_value = None + self.network_client.find_router.return_value = self._router + self.network_client.find_network.return_value = self._network + + self.network_client.find_subnet.return_value = self._subnet + self.network_client.find_extension.side_effect = ( + lambda name, ignore_missing=True: self._extensions.get(name) ) # Get the command object to test self.cmd = router.SetRouter(self.app, None) @@ -1459,7 +1647,7 @@ def test_set_route_overwrite_route(self): _testrouter = network_fakes.create_one_router( {'routes': [{"destination": "10.0.0.2", "nexthop": "1.1.1.1"}]} ) - self.network_client.find_router = mock.Mock(return_value=_testrouter) + self.network_client.find_router.return_value = _testrouter arglist = [ _testrouter.name, '--route', @@ -1667,9 +1855,8 @@ def test_set_with_no_tag(self): def test_set_gateway_ip_qos(self): qos_policy = network_fakes.create_one_qos_policy() - self.network_client.find_qos_policy = mock.Mock( - return_value=qos_policy - ) + self.network_client.find_qos_policy.return_value = qos_policy + arglist = [ "--external-gateway", self._network.id, @@ -1724,9 +1911,8 @@ def test_unset_gateway_ip_qos(self): def test_set_unset_gateway_ip_qos(self): qos_policy = network_fakes.create_one_qos_policy() - self.network_client.find_qos_policy = mock.Mock( - return_value=qos_policy - ) + self.network_client.find_qos_policy.return_value = qos_policy + arglist = [ "--external-gateway", self._network.id, @@ -1752,11 +1938,10 @@ def test_set_unset_gateway_ip_qos(self): def test_set_gateway_ip_qos_no_gateway(self): qos_policy = network_fakes.create_one_qos_policy() - self.network_client.find_qos_policy = mock.Mock( - return_value=qos_policy - ) + self.network_client.find_qos_policy.return_value = qos_policy + router = network_fakes.create_one_router() - self.network_client.find_router = mock.Mock(return_value=router) + self.network_client.find_router.return_value = router arglist = [ "--qos-policy", qos_policy.id, @@ -1774,11 +1959,10 @@ def test_set_gateway_ip_qos_no_gateway(self): def test_unset_gateway_ip_qos_no_gateway(self): qos_policy = network_fakes.create_one_qos_policy() - self.network_client.find_qos_policy = mock.Mock( - return_value=qos_policy - ) + self.network_client.find_qos_policy.return_value = qos_policy + router = network_fakes.create_one_router() - self.network_client.find_router = mock.Mock(return_value=router) + self.network_client.find_router.return_value = router arglist = [ "--no-qos-policy", router.id, @@ -1793,7 +1977,7 @@ def test_unset_gateway_ip_qos_no_gateway(self): ) -class TestShowRouter(TestRouter): +class TestShowRouter(network_fakes.TestNetworkV2): # The router to set. _router = network_fakes.create_one_router() _port = network_fakes.create_one_port( @@ -1857,8 +2041,8 @@ class TestShowRouter(TestRouter): def setUp(self): super().setUp() - self.network_client.find_router = mock.Mock(return_value=self._router) - self.network_client.ports = mock.Mock(return_value=[self._port]) + self.network_client.find_router.return_value = self._router + self.network_client.ports.return_value = [self._port] # Get the command object to test self.cmd = router.ShowRouter(self.app, None) @@ -1911,7 +2095,7 @@ def test_show_no_ha_no_distributed(self): with mock.patch.object( self.network_client, "find_router", return_value=_router ): - columns, data = self.cmd.take_action(parsed_args) + columns, _data = self.cmd.take_action(parsed_args) self.assertNotIn("is_distributed", columns) self.assertNotIn("is_ha", columns) @@ -1933,10 +2117,12 @@ def test_show_no_extra_route_extension(self): columns, data = self.cmd.take_action(parsed_args) self.assertIn("routes", columns) - self.assertIsNone(list(data)[columns.index('routes')].human_readable()) + self.assertEqual( + list(data)[columns.index('routes')].human_readable(), '' + ) -class TestUnsetRouter(TestRouter): +class TestUnsetRouter(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self.fake_network = network_fakes.create_one_network() @@ -1961,18 +2147,16 @@ def setUp(self): } ) self.fake_subnet = network_fakes.FakeSubnet.create_one_subnet() - self.network_client.find_router = mock.Mock( - return_value=self._testrouter - ) - self.network_client.update_router = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.find_router.return_value = self._testrouter + + self.network_client.update_router.return_value = None + self.network_client.set_tags.return_value = None self._extensions = {'fake': network_fakes.create_one_extension()} - self.network_client.find_extension = mock.Mock( - side_effect=lambda name: self._extensions.get(name) - ) - self.network_client.remove_external_gateways = mock.Mock( - return_value=None + self.network_client.find_extension.side_effect = ( + lambda name, ignore_missing=True: self._extensions.get(name) ) + self.network_client.remove_external_gateways.return_value = None + # Get the command object to test self.cmd = router.UnsetRouter(self.app, None) @@ -2109,11 +2293,10 @@ def test_unset_router_qos_policy(self): def test_unset_gateway_ip_qos_no_network(self): qos_policy = network_fakes.create_one_qos_policy() - self.network_client.find_qos_policy = mock.Mock( - return_value=qos_policy - ) + self.network_client.find_qos_policy.return_value = qos_policy + router = network_fakes.create_one_router() - self.network_client.find_router = mock.Mock(return_value=router) + self.network_client.find_router.return_value = router arglist = [ "--qos-policy", router.id, @@ -2129,13 +2312,12 @@ def test_unset_gateway_ip_qos_no_network(self): def test_unset_gateway_ip_qos_no_qos(self): qos_policy = network_fakes.create_one_qos_policy() - self.network_client.find_qos_policy = mock.Mock( - return_value=qos_policy - ) + self.network_client.find_qos_policy.return_value = qos_policy + router = network_fakes.create_one_router( {"external_gateway_info": {"network_id": "fake-id"}} ) - self.network_client.find_router = mock.Mock(return_value=router) + self.network_client.find_router.return_value = router arglist = [ "--qos-policy", router.id, @@ -2150,7 +2332,7 @@ def test_unset_gateway_ip_qos_no_qos(self): ) -class TestGatewayOps(TestRouter): +class TestGatewayOps(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self._networks = [] @@ -2172,10 +2354,10 @@ def setUp(self): attrs={'name': 'external-gateway-multihoming'} ) } - self.network_client.find_extension = mock.Mock( - side_effect=lambda name: self._extensions.get(name) + self.network_client.find_extension.side_effect = ( + lambda name, ignore_missing=True: self._extensions.get(name) ) - self.network_client.find_router = mock.Mock(return_value=self._router) + self.network_client.find_router.return_value = self._router def _find_network(name_or_id, ignore_missing): for network in self._networks: @@ -2185,15 +2367,12 @@ def _find_network(name_or_id, ignore_missing): return None raise Exception('Test resource not found') - self.network_client.find_network = mock.Mock(side_effect=_find_network) + self.network_client.find_network.side_effect = _find_network - self.network_client.find_subnet = mock.Mock(return_value=self._subnet) - self.network_client.add_external_gateways = mock.Mock( - return_value=None - ) - self.network_client.remove_external_gateways = mock.Mock( - return_value=None - ) + self.network_client.find_subnet.return_value = self._subnet + self.network_client.add_external_gateways.return_value = None + + self.network_client.remove_external_gateways.return_value = None class TestCreateMultipleGateways(TestGatewayOps): @@ -2223,13 +2402,10 @@ def setUp(self): self._second_network = network_fakes.create_one_network() self._networks.append(self._second_network) - self.network_client.create_router = mock.Mock( - return_value=self._router - ) - self.network_client.update_router = mock.Mock(return_value=None) - self.network_client.update_external_gateways = mock.Mock( - return_value=None - ) + self.network_client.create_router.return_value = self._router + + self.network_client.update_router.return_value = None + self.network_client.update_external_gateways.return_value = None self._data = ( router.AdminStateColumn(self._router.is_admin_state_up), @@ -2353,10 +2529,9 @@ def setUp(self): self._second_network = network_fakes.create_one_network() self._networks.append(self._second_network) - self.network_client.update_router = mock.Mock(return_value=None) - self.network_client.update_external_gateways = mock.Mock( - return_value=None - ) + self.network_client.update_router.return_value = None + self.network_client.update_external_gateways.return_value = None + self.cmd = router.SetRouter(self.app, None) def test_update_one_gateway(self): diff --git a/openstackclient/tests/unit/network/v2/test_security_group_network.py b/openstackclient/tests/unit/network/v2/test_security_group.py similarity index 87% rename from openstackclient/tests/unit/network/v2/test_security_group_network.py rename to openstackclient/tests/unit/network/v2/test_security_group.py index 62b87642c0..734462b70e 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group.py @@ -11,30 +11,21 @@ # under the License. # -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network.v2 import security_group -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils -class TestSecurityGroupNetwork(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - - -class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): - project = identity_fakes.FakeProject.create_one_project() - domain = identity_fakes.FakeDomain.create_one_domain() +class TestCreateSecurityGroupNetwork(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) # The security group to be created. _security_group = network_fakes.create_one_security_group() @@ -42,6 +33,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): 'created_at', 'description', 'id', + 'is_shared', 'name', 'project_id', 'revision_number', @@ -55,6 +47,7 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): _security_group.created_at, _security_group.description, _security_group.id, + _security_group.is_shared, _security_group.name, _security_group.project_id, _security_group.revision_number, @@ -67,13 +60,12 @@ class TestCreateSecurityGroupNetwork(TestSecurityGroupNetwork): def setUp(self): super().setUp() - self.network_client.create_security_group = mock.Mock( - return_value=self._security_group + self.network_client.create_security_group.return_value = ( + self._security_group ) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain - self.network_client.set_tags = mock.Mock(return_value=None) + self.identity_sdk_client.find_project.return_value = self.project + self.network_client.set_tags.return_value = None # Get the command object to test self.cmd = security_group.CreateSecurityGroup(self.app, None) @@ -176,16 +168,14 @@ def test_create_with_no_tag(self): self._test_create_with_tag(add_tags=False) -class TestDeleteSecurityGroupNetwork(TestSecurityGroupNetwork): +class TestDeleteSecurityGroupNetwork(network_fakes.TestNetworkV2): # The security groups to be deleted. _security_groups = network_fakes.create_security_groups() def setUp(self): super().setUp() - self.network_client.delete_security_group = mock.Mock( - return_value=None - ) + self.network_client.delete_security_group.return_value = None self.network_client.find_security_group = ( network_fakes.get_security_groups(self._security_groups) @@ -243,9 +233,7 @@ def test_multi_security_groups_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self._security_groups[0], exceptions.CommandError] - self.network_client.find_security_group = mock.Mock( - side_effect=find_mock_result - ) + self.network_client.find_security_group.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -264,7 +252,7 @@ def test_multi_security_groups_delete_with_exception(self): ) -class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): +class TestListSecurityGroupNetwork(network_fakes.TestNetworkV2): # The security group to be listed. _security_groups = network_fakes.create_security_groups(count=3) @@ -274,6 +262,7 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): 'Description', 'Project', 'Tags', + 'Shared', ) data = [] @@ -285,14 +274,15 @@ class TestListSecurityGroupNetwork(TestSecurityGroupNetwork): grp.description, grp.project_id, grp.tags, + grp.is_shared, ) ) def setUp(self): super().setUp() - self.network_client.security_groups = mock.Mock( - return_value=self._security_groups + self.network_client.security_groups.return_value = ( + self._security_groups ) # Get the command object to test @@ -300,9 +290,7 @@ def setUp(self): def test_security_group_list_no_options(self): arglist = [] - verifylist = [ - ('all_projects', False), - ] + verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -313,26 +301,31 @@ def test_security_group_list_no_options(self): self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) - def test_security_group_list_all_projects(self): + def test_security_groups_list_pagination(self): arglist = [ - '--all-projects', + '--marker', + self._security_groups[0].id, + '--limit', + '1', ] verifylist = [ - ('all_projects', True), + ('marker', self._security_groups[0].id), + ('limit', 1), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.network_client.security_groups.assert_called_once_with( - fields=security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE + fields=security_group.ListSecurityGroup.FIELDS_TO_RETRIEVE, + **{'marker': self._security_groups[0].id, 'limit': 1}, ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.data, list(data)) + self.assertEqual(self.data, list(data)) def test_security_group_list_project(self): - project = identity_fakes.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -353,8 +346,8 @@ def test_security_group_list_project(self): self.assertCountEqual(self.data, list(data)) def test_security_group_list_project_domain(self): - project = identity_fakes.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -410,7 +403,7 @@ def test_list_with_tag_options(self): self.assertEqual(self.data, list(data)) -class TestSetSecurityGroupNetwork(TestSecurityGroupNetwork): +class TestSetSecurityGroupNetwork(network_fakes.TestNetworkV2): # The security group to be set. _security_group = network_fakes.create_one_security_group( attrs={'tags': ['green', 'red']} @@ -419,14 +412,13 @@ class TestSetSecurityGroupNetwork(TestSecurityGroupNetwork): def setUp(self): super().setUp() - self.network_client.update_security_group = mock.Mock( - return_value=None - ) + self.network_client.update_security_group.return_value = None - self.network_client.find_security_group = mock.Mock( - return_value=self._security_group + self.network_client.find_security_group.return_value = ( + self._security_group ) - self.network_client.set_tags = mock.Mock(return_value=None) + + self.network_client.set_tags.return_value = None # Get the command object to test self.cmd = security_group.SetSecurityGroup(self.app, None) @@ -511,7 +503,7 @@ def test_set_with_no_tag(self): self._test_set_tags(with_tags=False) -class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork): +class TestShowSecurityGroupNetwork(network_fakes.TestNetworkV2): # The security group rule to be shown with the group. _security_group_rule = network_fakes.create_one_security_group_rule() @@ -524,6 +516,7 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork): 'created_at', 'description', 'id', + 'is_shared', 'name', 'project_id', 'revision_number', @@ -537,6 +530,7 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork): _security_group.created_at, _security_group.description, _security_group.id, + _security_group.is_shared, _security_group.name, _security_group.project_id, _security_group.revision_number, @@ -551,8 +545,8 @@ class TestShowSecurityGroupNetwork(TestSecurityGroupNetwork): def setUp(self): super().setUp() - self.network_client.find_security_group = mock.Mock( - return_value=self._security_group + self.network_client.find_security_group.return_value = ( + self._security_group ) # Get the command object to test @@ -581,7 +575,7 @@ def test_show_all_options(self): self.assertCountEqual(self.data, data) -class TestUnsetSecurityGroupNetwork(TestSecurityGroupNetwork): +class TestUnsetSecurityGroupNetwork(network_fakes.TestNetworkV2): # The security group to be unset. _security_group = network_fakes.create_one_security_group( attrs={'tags': ['green', 'red']} @@ -590,14 +584,13 @@ class TestUnsetSecurityGroupNetwork(TestSecurityGroupNetwork): def setUp(self): super().setUp() - self.network_client.update_security_group = mock.Mock( - return_value=None - ) + self.network_client.update_security_group.return_value = None - self.network_client.find_security_group = mock.Mock( - return_value=self._security_group + self.network_client.find_security_group.return_value = ( + self._security_group ) - self.network_client.set_tags = mock.Mock(return_value=None) + + self.network_client.set_tags.return_value = None # Get the command object to test self.cmd = security_group.UnsetSecurityGroup(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_security_group_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_compute.py deleted file mode 100644 index e23bb24977..0000000000 --- a/openstackclient/tests/unit/network/v2/test_security_group_compute.py +++ /dev/null @@ -1,392 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -from unittest import mock - -from osc_lib import exceptions - -from openstackclient.api import compute_v2 -from openstackclient.network.v2 import security_group -from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -from openstackclient.tests.unit import utils as tests_utils - - -@mock.patch.object(compute_v2, 'create_security_group') -class TestCreateSecurityGroupCompute(compute_fakes.TestComputev2): - project = identity_fakes.FakeProject.create_one_project() - domain = identity_fakes.FakeDomain.create_one_domain() - - # The security group to be shown. - _security_group = compute_fakes.create_one_security_group() - - columns = ( - 'description', - 'id', - 'name', - 'project_id', - 'rules', - ) - - data = ( - _security_group['description'], - _security_group['id'], - _security_group['name'], - _security_group['tenant_id'], - security_group.ComputeSecurityGroupRulesColumn([]), - ) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - # Get the command object to test - self.cmd = security_group.CreateSecurityGroup(self.app, None) - - def test_security_group_create_no_options(self, sg_mock): - self.assertRaises( - tests_utils.ParserException, self.check_parser, self.cmd, [], [] - ) - - def test_security_group_create_min_options(self, sg_mock): - sg_mock.return_value = self._security_group - arglist = [ - self._security_group['name'], - ] - verifylist = [ - ('name', self._security_group['name']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - sg_mock.assert_called_once_with( - self.compute_client, - self._security_group['name'], - self._security_group['name'], - ) - self.assertEqual(self.columns, columns) - self.assertCountEqual(self.data, data) - - def test_security_group_create_all_options(self, sg_mock): - sg_mock.return_value = self._security_group - arglist = [ - '--description', - self._security_group['description'], - self._security_group['name'], - ] - verifylist = [ - ('description', self._security_group['description']), - ('name', self._security_group['name']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - sg_mock.assert_called_once_with( - self.compute_client, - self._security_group['name'], - self._security_group['description'], - ) - self.assertEqual(self.columns, columns) - self.assertCountEqual(self.data, data) - - -@mock.patch.object(compute_v2, 'delete_security_group') -class TestDeleteSecurityGroupCompute(compute_fakes.TestComputev2): - # The security groups to be deleted. - _security_groups = compute_fakes.create_security_groups() - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - compute_v2.find_security_group = mock.Mock( - side_effect=self._security_groups - ) - - # Get the command object to test - self.cmd = security_group.DeleteSecurityGroup(self.app, None) - - def test_security_group_delete(self, sg_mock): - sg_mock.return_value = mock.Mock(return_value=None) - arglist = [ - self._security_groups[0]['id'], - ] - verifylist = [ - ('group', [self._security_groups[0]['id']]), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - sg_mock.assert_called_once_with( - self.compute_client, - self._security_groups[0]['id'], - ) - self.assertIsNone(result) - - def test_security_group_multi_delete(self, sg_mock): - sg_mock.return_value = mock.Mock(return_value=None) - arglist = [ - self._security_groups[0]['id'], - self._security_groups[1]['id'], - ] - verifylist = [ - ('group', arglist), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - sg_mock.assert_has_calls( - [ - mock.call(self.compute_client, self._security_groups[0]['id']), - mock.call(self.compute_client, self._security_groups[1]['id']), - ] - ) - self.assertIsNone(result) - - def test_security_group_multi_delete_with_exception(self, sg_mock): - sg_mock.return_value = mock.Mock(return_value=None) - compute_v2.find_security_group.side_effect = [ - self._security_groups[0], - exceptions.NotFound('foo'), - ] - arglist = [ - self._security_groups[0]['id'], - 'unexist_security_group', - ] - verifylist = [ - ('group', arglist), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - exc = self.assertRaises( - exceptions.CommandError, - self.cmd.take_action, - parsed_args, - ) - self.assertEqual('1 of 2 groups failed to delete.', str(exc)) - - sg_mock.assert_has_calls( - [ - mock.call(self.compute_client, self._security_groups[0]['id']), - ] - ) - - -@mock.patch.object(compute_v2, 'list_security_groups') -class TestListSecurityGroupCompute(compute_fakes.TestComputev2): - # The security group to be listed. - _security_groups = compute_fakes.create_security_groups(count=3) - - columns = ( - 'ID', - 'Name', - 'Description', - ) - columns_all_projects = ( - 'ID', - 'Name', - 'Description', - 'Project', - ) - - data = [] - for grp in _security_groups: - data.append( - ( - grp['id'], - grp['name'], - grp['description'], - ) - ) - data_all_projects = [] - for grp in _security_groups: - data_all_projects.append( - ( - grp['id'], - grp['name'], - grp['description'], - grp['tenant_id'], - ) - ) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - # Get the command object to test - self.cmd = security_group.ListSecurityGroup(self.app, None) - - def test_security_group_list_no_options(self, sg_mock): - sg_mock.return_value = self._security_groups - arglist = [] - verifylist = [ - ('all_projects', False), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - sg_mock.assert_called_once_with( - self.compute_client, all_projects=False - ) - self.assertEqual(self.columns, columns) - self.assertCountEqual(self.data, list(data)) - - def test_security_group_list_all_projects(self, sg_mock): - sg_mock.return_value = self._security_groups - arglist = [ - '--all-projects', - ] - verifylist = [ - ('all_projects', True), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - sg_mock.assert_called_once_with(self.compute_client, all_projects=True) - self.assertEqual(self.columns_all_projects, columns) - self.assertCountEqual(self.data_all_projects, list(data)) - - -@mock.patch.object(compute_v2, 'update_security_group') -class TestSetSecurityGroupCompute(compute_fakes.TestComputev2): - # The security group to be set. - _security_group = compute_fakes.create_one_security_group() - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - compute_v2.find_security_group = mock.Mock( - return_value=self._security_group - ) - - # Get the command object to test - self.cmd = security_group.SetSecurityGroup(self.app, None) - - def test_security_group_set_no_options(self, sg_mock): - self.assertRaises( - tests_utils.ParserException, self.check_parser, self.cmd, [], [] - ) - - def test_security_group_set_no_updates(self, sg_mock): - sg_mock.return_value = mock.Mock(return_value=None) - arglist = [ - self._security_group['name'], - ] - verifylist = [ - ('group', self._security_group['name']), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - sg_mock.assert_called_once_with( - self.compute_client, self._security_group['id'] - ) - self.assertIsNone(result) - - def test_security_group_set_all_options(self, sg_mock): - sg_mock.return_value = mock.Mock(return_value=None) - new_name = 'new-' + self._security_group['name'] - new_description = 'new-' + self._security_group['description'] - arglist = [ - '--name', - new_name, - '--description', - new_description, - self._security_group['name'], - ] - verifylist = [ - ('description', new_description), - ('group', self._security_group['name']), - ('name', new_name), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - sg_mock.assert_called_once_with( - self.compute_client, - self._security_group['id'], - name=new_name, - description=new_description, - ) - self.assertIsNone(result) - - -@mock.patch.object(compute_v2, 'find_security_group') -class TestShowSecurityGroupCompute(compute_fakes.TestComputev2): - # The security group rule to be shown with the group. - _security_group_rule = compute_fakes.create_one_security_group_rule() - - # The security group to be shown. - _security_group = compute_fakes.create_one_security_group( - attrs={'rules': [_security_group_rule]} - ) - - columns = ( - 'description', - 'id', - 'name', - 'project_id', - 'rules', - ) - - data = ( - _security_group['description'], - _security_group['id'], - _security_group['name'], - _security_group['tenant_id'], - security_group.ComputeSecurityGroupRulesColumn([_security_group_rule]), - ) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - # Get the command object to test - self.cmd = security_group.ShowSecurityGroup(self.app, None) - - def test_security_group_show_no_options(self, sg_mock): - self.assertRaises( - tests_utils.ParserException, self.check_parser, self.cmd, [], [] - ) - - def test_security_group_show_all_options(self, sg_mock): - sg_mock.return_value = self._security_group - arglist = [ - self._security_group['id'], - ] - verifylist = [ - ('group', self._security_group['id']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - sg_mock.assert_called_once_with( - self.compute_client, self._security_group['id'] - ) - self.assertEqual(self.columns, columns) - self.assertCountEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py b/openstackclient/tests/unit/network/v2/test_security_group_rule.py similarity index 93% rename from openstackclient/tests/unit/network/v2/test_security_group_rule_network.py rename to openstackclient/tests/unit/network/v2/test_security_group_rule.py index 34cf12e017..67f39958fc 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group_rule.py @@ -9,16 +9,16 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.network import utils as network_utils from openstackclient.network.v2 import security_group_rule -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -27,15 +27,10 @@ class TestSecurityGroupRuleNetwork(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - class TestCreateSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): - project = identity_fakes.FakeProject.create_one_project() - domain = identity_fakes.FakeDomain.create_one_domain() + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) # The security group rule to be created. _security_group_rule = None @@ -69,9 +64,10 @@ def _setup_security_group_rule(self, attrs=None): self._security_group_rule = ( network_fakes.create_one_security_group_rule(attrs) ) - self.network_client.create_security_group_rule = mock.Mock( - return_value=self._security_group_rule + self.network_client.create_security_group_rule.return_value = ( + self._security_group_rule ) + self.expected_data = ( self._security_group_rule.created_at, self._security_group_rule.description, @@ -93,16 +89,15 @@ def _setup_security_group_rule(self, attrs=None): def setUp(self): super().setUp() - self.network_client.find_security_group = mock.Mock( - return_value=self._security_group + self.network_client.find_security_group.return_value = ( + self._security_group ) - self.network_client.find_address_group = mock.Mock( - return_value=self._address_group + self.network_client.find_address_group.return_value = ( + self._address_group ) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project # Get the command object to test self.cmd = security_group_rule.CreateSecurityGroupRule(self.app, None) @@ -171,22 +166,6 @@ def test_proper_case_ethertype(self): parsed_args = self.check_parser(self.cmd, arglist, []) self.assertEqual('IPv6', parsed_args.ethertype) - def test_create_all_protocol_options(self): - arglist = [ - '--protocol', - 'tcp', - '--proto', - 'tcp', - self._security_group.id, - ] - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - [], - ) - def test_create_all_port_range_options(self): arglist = [ '--dst-port', @@ -251,7 +230,7 @@ def test_create_default_rule(self): self.assertEqual(self.expected_columns, columns) self.assertEqual(self.expected_data, data) - def test_create_proto_option(self): + def test_create_protocol_option(self): self._setup_security_group_rule( { 'protocol': 'icmp', @@ -259,15 +238,14 @@ def test_create_proto_option(self): } ) arglist = [ - '--proto', + '--protocol', self._security_group_rule.protocol, '--remote-ip', self._security_group_rule.remote_ip_prefix, self._security_group.id, ] verifylist = [ - ('proto', self._security_group_rule.protocol), - ('protocol', None), + ('protocol', self._security_group_rule.protocol), ('remote_ip', self._security_group_rule.remote_ip_prefix), ('group', self._security_group.id), ] @@ -295,15 +273,14 @@ def test_create_protocol_any(self): } ) arglist = [ - '--proto', + '--protocol', 'any', '--remote-ip', self._security_group_rule.remote_ip_prefix, self._security_group.id, ] verifylist = [ - ('proto', 'any'), - ('protocol', None), + ('protocol', 'any'), ('remote_ip', self._security_group_rule.remote_ip_prefix), ('group', self._security_group.id), ] @@ -970,9 +947,7 @@ class TestDeleteSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): def setUp(self): super().setUp() - self.network_client.delete_security_group_rule = mock.Mock( - return_value=None - ) + self.network_client.delete_security_group_rule.return_value = None self.network_client.find_security_group_rule = ( network_fakes.get_security_group_rules(self._security_group_rules) @@ -1030,8 +1005,8 @@ def test_multi_security_group_rules_delete_with_exception(self): self._security_group_rules[0], exceptions.CommandError, ] - self.network_client.find_security_group_rule = mock.Mock( - side_effect=find_mock_result + self.network_client.find_security_group_rule.side_effect = ( + find_mock_result ) try: @@ -1134,11 +1109,12 @@ class TestListSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): def setUp(self): super().setUp() - self.network_client.find_security_group = mock.Mock( - return_value=self._security_group + self.network_client.find_security_group.return_value = ( + self._security_group ) - self.network_client.security_group_rules = mock.Mock( - return_value=self._security_group_rules + + self.network_client.security_group_rules.return_value = ( + self._security_group_rules ) # Get the command object to test @@ -1154,13 +1130,17 @@ def test_list_default(self): self.assertEqual(self.expected_columns_no_group, columns) self.assertEqual(self.expected_data_no_group, list(data)) - def test_list_with_group(self): + def test_list_with_pagination(self): self._security_group_rule_tcp.port_range_min = 80 arglist = [ - self._security_group.id, + '--marker', + self._security_group_rules[0].id, + '--limit', + '1', ] verifylist = [ - ('group', self._security_group.id), + ('marker', self._security_group_rules[0].id), + ('limit', 1), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1168,27 +1148,32 @@ def test_list_with_group(self): self.network_client.security_group_rules.assert_called_once_with( **{ - 'security_group_id': self._security_group.id, + 'marker': self._security_group_rules[0].id, + 'limit': 1, } ) - self.assertEqual(self.expected_columns_with_group, columns) - self.assertEqual(self.expected_data_with_group, list(data)) + self.assertEqual(self.expected_columns_no_group, columns) + self.assertEqual(self.expected_data_no_group, list(data)) - def test_list_with_ignored_options(self): + def test_list_with_group(self): self._security_group_rule_tcp.port_range_min = 80 arglist = [ - '--all-projects', + self._security_group.id, ] verifylist = [ - ('all_projects', True), + ('group', self._security_group.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.network_client.security_group_rules.assert_called_once_with(**{}) - self.assertEqual(self.expected_columns_no_group, columns) - self.assertEqual(self.expected_data_no_group, list(data)) + self.network_client.security_group_rules.assert_called_once_with( + **{ + 'security_group_id': self._security_group.id, + } + ) + self.assertEqual(self.expected_columns_with_group, columns) + self.assertEqual(self.expected_data_with_group, list(data)) def test_list_with_protocol(self): self._security_group_rule_tcp.port_range_min = 80 @@ -1251,6 +1236,55 @@ def test_list_with_wrong_egress(self): self.assertEqual(self.expected_columns_no_group, columns) self.assertEqual(self.expected_data_no_group, list(data)) + def test_list_with_project(self): + project = sdk_fakes.generate_fake_resource(_project.Project) + self._security_group_rule_tcp.port_range_min = 80 + self.identity_sdk_client.find_project.return_value = project + + arglist = [ + '--project', + project.id, + ] + verifylist = [ + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'project_id': project.id} + + self.network_client.security_group_rules.assert_called_once_with( + **filters + ) + self.assertEqual(self.expected_columns_no_group, columns) + self.assertEqual(self.expected_data_no_group, list(data)) + + def test_list_with_project_domain(self): + project = sdk_fakes.generate_fake_resource(_project.Project) + self._security_group_rule_tcp.port_range_min = 80 + self.identity_sdk_client.find_project.return_value = project + + arglist = [ + '--project', + project.id, + '--project-domain', + project.domain_id, + ] + verifylist = [ + ('project', project.id), + ('project_domain', project.domain_id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + filters = {'project_id': project.id} + + self.network_client.security_group_rules.assert_called_once_with( + **filters + ) + self.assertEqual(self.expected_columns_no_group, columns) + self.assertEqual(self.expected_data_no_group, list(data)) + class TestShowSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): # The security group rule to be shown. @@ -1295,8 +1329,8 @@ class TestShowSecurityGroupRuleNetwork(TestSecurityGroupRuleNetwork): def setUp(self): super().setUp() - self.network_client.find_security_group_rule = mock.Mock( - return_value=self._security_group_rule + self.network_client.find_security_group_rule.return_value = ( + self._security_group_rule ) # Get the command object to test diff --git a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py b/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py deleted file mode 100644 index 9cab52e392..0000000000 --- a/openstackclient/tests/unit/network/v2/test_security_group_rule_compute.py +++ /dev/null @@ -1,555 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from unittest import mock - -from osc_lib import exceptions - -from openstackclient.api import compute_v2 -from openstackclient.network import utils as network_utils -from openstackclient.network.v2 import security_group_rule -from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -from openstackclient.tests.unit import utils as tests_utils - - -@mock.patch.object(compute_v2, 'create_security_group_rule') -class TestCreateSecurityGroupRuleCompute(compute_fakes.TestComputev2): - project = identity_fakes.FakeProject.create_one_project() - domain = identity_fakes.FakeDomain.create_one_domain() - - # The security group rule to be created. - _security_group_rule = None - - # The security group that will contain the rule created. - _security_group = compute_fakes.create_one_security_group() - - def _setup_security_group_rule(self, attrs=None): - self._security_group_rule = ( - compute_fakes.create_one_security_group_rule(attrs) - ) - ( - expected_columns, - expected_data, - ) = network_utils.format_security_group_rule_show( - self._security_group_rule - ) - return expected_columns, expected_data - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - compute_v2.find_security_group = mock.Mock( - return_value=self._security_group, - ) - - # Get the command object to test - self.cmd = security_group_rule.CreateSecurityGroupRule(self.app, None) - - def test_security_group_rule_create_no_options(self, sgr_mock): - self.assertRaises( - tests_utils.ParserException, self.check_parser, self.cmd, [], [] - ) - - def test_security_group_rule_create_all_remote_options(self, sgr_mock): - arglist = [ - '--remote-ip', - '10.10.0.0/24', - '--remote-group', - self._security_group['id'], - self._security_group['id'], - ] - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - [], - ) - - def test_security_group_rule_create_bad_protocol(self, sgr_mock): - arglist = [ - '--protocol', - 'foo', - self._security_group['id'], - ] - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - [], - ) - - def test_security_group_rule_create_all_protocol_options(self, sgr_mock): - arglist = [ - '--protocol', - 'tcp', - '--proto', - 'tcp', - self._security_group['id'], - ] - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - [], - ) - - def test_security_group_rule_create_network_options(self, sgr_mock): - arglist = [ - '--ingress', - '--ethertype', - 'IPv4', - '--icmp-type', - '3', - '--icmp-code', - '11', - '--project', - self.project.name, - '--project-domain', - self.domain.name, - self._security_group['id'], - ] - self.assertRaises( - tests_utils.ParserException, - self.check_parser, - self.cmd, - arglist, - [], - ) - - def test_security_group_rule_create_default_rule(self, sgr_mock): - expected_columns, expected_data = self._setup_security_group_rule() - sgr_mock.return_value = self._security_group_rule - dst_port = ( - str(self._security_group_rule['from_port']) - + ':' - + str(self._security_group_rule['to_port']) - ) - arglist = [ - '--dst-port', - dst_port, - self._security_group['id'], - ] - verifylist = [ - ( - 'dst_port', - ( - self._security_group_rule['from_port'], - self._security_group_rule['to_port'], - ), - ), - ('group', self._security_group['id']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - sgr_mock.assert_called_once_with( - self.compute_client, - security_group_id=self._security_group['id'], - ip_protocol=self._security_group_rule['ip_protocol'], - from_port=self._security_group_rule['from_port'], - to_port=self._security_group_rule['to_port'], - remote_ip=self._security_group_rule['ip_range']['cidr'], - remote_group=None, - ) - self.assertEqual(expected_columns, columns) - self.assertEqual(expected_data, data) - - def test_security_group_rule_create_remote_group(self, sgr_mock): - expected_columns, expected_data = self._setup_security_group_rule( - { - 'from_port': 22, - 'to_port': 22, - 'group': {'name': self._security_group['name']}, - } - ) - sgr_mock.return_value = self._security_group_rule - arglist = [ - '--dst-port', - str(self._security_group_rule['from_port']), - '--remote-group', - self._security_group['name'], - self._security_group['id'], - ] - verifylist = [ - ( - 'dst_port', - ( - self._security_group_rule['from_port'], - self._security_group_rule['to_port'], - ), - ), - ('remote_group', self._security_group['name']), - ('group', self._security_group['id']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - sgr_mock.assert_called_once_with( - self.compute_client, - security_group_id=self._security_group['id'], - ip_protocol=self._security_group_rule['ip_protocol'], - from_port=self._security_group_rule['from_port'], - to_port=self._security_group_rule['to_port'], - remote_ip=self._security_group_rule['ip_range']['cidr'], - remote_group=self._security_group['id'], - ) - self.assertEqual(expected_columns, columns) - self.assertEqual(expected_data, data) - - def test_security_group_rule_create_remote_ip(self, sgr_mock): - expected_columns, expected_data = self._setup_security_group_rule( - { - 'ip_protocol': 'icmp', - 'from_port': -1, - 'to_port': -1, - 'ip_range': {'cidr': '10.0.2.0/24'}, - } - ) - sgr_mock.return_value = self._security_group_rule - arglist = [ - '--protocol', - self._security_group_rule['ip_protocol'], - '--remote-ip', - self._security_group_rule['ip_range']['cidr'], - self._security_group['id'], - ] - verifylist = [ - ('protocol', self._security_group_rule['ip_protocol']), - ('remote_ip', self._security_group_rule['ip_range']['cidr']), - ('group', self._security_group['id']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - sgr_mock.assert_called_once_with( - self.compute_client, - security_group_id=self._security_group['id'], - ip_protocol=self._security_group_rule['ip_protocol'], - from_port=self._security_group_rule['from_port'], - to_port=self._security_group_rule['to_port'], - remote_ip=self._security_group_rule['ip_range']['cidr'], - remote_group=None, - ) - self.assertEqual(expected_columns, columns) - self.assertEqual(expected_data, data) - - def test_security_group_rule_create_proto_option(self, sgr_mock): - expected_columns, expected_data = self._setup_security_group_rule( - { - 'ip_protocol': 'icmp', - 'from_port': -1, - 'to_port': -1, - 'ip_range': {'cidr': '10.0.2.0/24'}, - } - ) - sgr_mock.return_value = self._security_group_rule - arglist = [ - '--proto', - self._security_group_rule['ip_protocol'], - '--remote-ip', - self._security_group_rule['ip_range']['cidr'], - self._security_group['id'], - ] - verifylist = [ - ('proto', self._security_group_rule['ip_protocol']), - ('protocol', None), - ('remote_ip', self._security_group_rule['ip_range']['cidr']), - ('group', self._security_group['id']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - sgr_mock.assert_called_once_with( - self.compute_client, - security_group_id=self._security_group['id'], - ip_protocol=self._security_group_rule['ip_protocol'], - from_port=self._security_group_rule['from_port'], - to_port=self._security_group_rule['to_port'], - remote_ip=self._security_group_rule['ip_range']['cidr'], - remote_group=None, - ) - self.assertEqual(expected_columns, columns) - self.assertEqual(expected_data, data) - - -@mock.patch.object(compute_v2, 'delete_security_group_rule') -class TestDeleteSecurityGroupRuleCompute(compute_fakes.TestComputev2): - # The security group rule to be deleted. - _security_group_rules = compute_fakes.create_security_group_rules(count=2) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - # Get the command object to test - self.cmd = security_group_rule.DeleteSecurityGroupRule(self.app, None) - - def test_security_group_rule_delete(self, sgr_mock): - arglist = [ - self._security_group_rules[0]['id'], - ] - verifylist = [ - ('rule', [self._security_group_rules[0]['id']]), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - result = self.cmd.take_action(parsed_args) - - sgr_mock.assert_called_once_with( - self.compute_client, self._security_group_rules[0]['id'] - ) - self.assertIsNone(result) - - def test_security_group_rule_delete_multi(self, sgr_mock): - arglist = [ - self._security_group_rules[0]['id'], - self._security_group_rules[1]['id'], - ] - verifylist = [ - ('rule', arglist), - ] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - - sgr_mock.assert_has_calls( - [ - mock.call( - self.compute_client, - self._security_group_rules[0]['id'], - ), - mock.call( - self.compute_client, - self._security_group_rules[1]['id'], - ), - ] - ) - self.assertIsNone(result) - - def test_security_group_rule_delete_multi_with_exception(self, sgr_mock): - arglist = [ - self._security_group_rules[0]['id'], - 'unexist_rule', - ] - verifylist = [ - ('rule', arglist), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - sgr_mock.side_effect = [None, exceptions.NotFound('foo')] - - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('1 of 2 rules failed to delete.', str(e)) - - sgr_mock.assert_has_calls( - [ - mock.call( - self.compute_client, - self._security_group_rules[0]['id'], - ), - mock.call(self.compute_client, 'unexist_rule'), - ] - ) - - -class TestListSecurityGroupRuleCompute(compute_fakes.TestComputev2): - # The security group to hold the rules. - _security_group = compute_fakes.create_one_security_group() - - # The security group rule to be listed. - _security_group_rule_tcp = compute_fakes.create_one_security_group_rule( - { - 'ip_protocol': 'tcp', - 'from_port': 80, - 'to_port': 80, - 'group': {'name': _security_group['name']}, - } - ) - _security_group_rule_icmp = compute_fakes.create_one_security_group_rule( - { - 'ip_protocol': 'icmp', - 'from_port': -1, - 'to_port': -1, - 'ip_range': {'cidr': '10.0.2.0/24'}, - 'group': {'name': _security_group['name']}, - } - ) - _security_group['rules'] = [ - _security_group_rule_tcp, - _security_group_rule_icmp, - ] - - expected_columns_with_group = ( - 'ID', - 'IP Protocol', - 'Ethertype', - 'IP Range', - 'Port Range', - 'Direction', - 'Remote Security Group', - ) - expected_columns_no_group = expected_columns_with_group + ( - 'Security Group', - ) - - expected_data_with_group = [] - expected_data_no_group = [] - for _security_group_rule in _security_group['rules']: - rule = network_utils.transform_compute_security_group_rule( - _security_group_rule - ) - expected_rule_with_group = ( - rule['id'], - rule['ip_protocol'], - '', # ethertype is a neutron-only thing - rule['ip_range'], - rule['port_range'], - rule['remote_security_group'], - ) - expected_rule_no_group = expected_rule_with_group + ( - _security_group_rule['parent_group_id'], - ) - expected_data_with_group.append(expected_rule_with_group) - expected_data_no_group.append(expected_rule_no_group) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - compute_v2.find_security_group = mock.Mock( - return_value=self._security_group, - ) - compute_v2.list_security_groups = mock.Mock( - return_value=[self._security_group], - ) - - # Get the command object to test - self.cmd = security_group_rule.ListSecurityGroupRule(self.app, None) - - def test_security_group_rule_list_default(self): - parsed_args = self.check_parser(self.cmd, [], []) - - columns, data = self.cmd.take_action(parsed_args) - compute_v2.list_security_groups.assert_called_once_with( - self.compute_client, all_projects=False - ) - self.assertEqual(self.expected_columns_no_group, columns) - self.assertEqual(self.expected_data_no_group, list(data)) - - def test_security_group_rule_list_with_group(self): - arglist = [ - self._security_group['id'], - ] - verifylist = [ - ('group', self._security_group['id']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - compute_v2.find_security_group.assert_called_once_with( - self.compute_client, self._security_group['id'] - ) - self.assertEqual(self.expected_columns_with_group, columns) - self.assertEqual(self.expected_data_with_group, list(data)) - - def test_security_group_rule_list_all_projects(self): - arglist = [ - '--all-projects', - ] - verifylist = [ - ('all_projects', True), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - compute_v2.list_security_groups.assert_called_once_with( - self.compute_client, all_projects=True - ) - self.assertEqual(self.expected_columns_no_group, columns) - self.assertEqual(self.expected_data_no_group, list(data)) - - def test_security_group_rule_list_with_ignored_options(self): - arglist = [ - '--long', - ] - verifylist = [ - ('long', False), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - compute_v2.list_security_groups.assert_called_once_with( - self.compute_client, all_projects=False - ) - self.assertEqual(self.expected_columns_no_group, columns) - self.assertEqual(self.expected_data_no_group, list(data)) - - -class TestShowSecurityGroupRuleCompute(compute_fakes.TestComputev2): - # The security group rule to be shown. - _security_group_rule = compute_fakes.create_one_security_group_rule() - - columns, data = network_utils.format_security_group_rule_show( - _security_group_rule - ) - - def setUp(self): - super().setUp() - - self.app.client_manager.network_endpoint_enabled = False - - # Build a security group fake customized for this test. - security_group_rules = [self._security_group_rule] - security_group = {'rules': security_group_rules} - compute_v2.list_security_groups = mock.Mock( - return_value=[security_group], - ) - - # Get the command object to test - self.cmd = security_group_rule.ShowSecurityGroupRule(self.app, None) - - def test_security_group_rule_show_no_options(self): - self.assertRaises( - tests_utils.ParserException, self.check_parser, self.cmd, [], [] - ) - - def test_security_group_rule_show_all_options(self): - arglist = [ - self._security_group_rule['id'], - ] - verifylist = [ - ('rule', self._security_group_rule['id']), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - compute_v2.list_security_groups.assert_called_once_with( - self.compute_client - ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/network/v2/test_security_groups_default_statefulness.py b/openstackclient/tests/unit/network/v2/test_security_groups_default_statefulness.py new file mode 100644 index 0000000000..d46693a7ce --- /dev/null +++ b/openstackclient/tests/unit/network/v2/test_security_groups_default_statefulness.py @@ -0,0 +1,338 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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.mock import call + +from openstack.identity.v3 import project as _project +from openstack.network.v2 import ( + security_groups_default_statefulness as _sg_default_statefulness, +) +from openstack.test import fakes as sdk_fakes +from osc_lib import exceptions + +from openstackclient.network.v2 import security_groups_default_statefulness +from openstackclient.tests.unit.network.v2 import fakes as network_fakes +from openstackclient.tests.unit import utils as tests_utils + + +def _generate_fake_setting(attrs=None): + setting_attrs = { + 'project_id': None, + 'stateful': False, + 'location': 'MUNCHMUNCHMUNCH', + } + if attrs: + setting_attrs.update(attrs) + return sdk_fakes.generate_fake_resource( + _sg_default_statefulness.SecurityGroupsDefaultStatefulness, + **setting_attrs, + ) + + +class TestCreateSecurityGroupDefaultStatefulness( + network_fakes.TestNetworkV2, +): + expected_columns = ( + 'id', + 'project_id', + 'stateful', + ) + + def setUp(self): + super().setUp() + self.cmd = security_groups_default_statefulness.CreateSecurityGroupDefaultStatefulness( + self.app, None + ) + + def test_create_stateless_system_wide(self): + setting = _generate_fake_setting({'stateful': False}) + self.network_client.create_security_groups_default_statefulness.return_value = setting + arglist = ['--stateless'] + verifylist = [('stateful', False)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.create_security_groups_default_statefulness.assert_called_once_with( + **{'stateful': False, 'project_id': None} + ) + self.assertEqual(self.expected_columns, columns) + self.assertEqual( + (setting.id, setting.project_id, setting.stateful), data + ) + + def test_create_stateful_system_wide(self): + setting = _generate_fake_setting({'stateful': True}) + self.network_client.create_security_groups_default_statefulness.return_value = setting + arglist = ['--stateful'] + verifylist = [('stateful', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.create_security_groups_default_statefulness.assert_called_once_with( + **{'stateful': True, 'project_id': None} + ) + self.assertEqual(self.expected_columns, columns) + self.assertEqual( + (setting.id, setting.project_id, setting.stateful), data + ) + + def test_create_per_project(self): + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project + setting = _generate_fake_setting( + {'stateful': False, 'project_id': project.id} + ) + self.network_client.create_security_groups_default_statefulness.return_value = setting + self.identity_client.projects.get.return_value = project + arglist = ['--stateless', '--project', project.id] + verifylist = [ + ('stateful', False), + ('project', project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.create_security_groups_default_statefulness.assert_called_once_with( + **{'stateful': False, 'project_id': project.id} + ) + self.assertEqual(self.expected_columns, columns) + self.assertEqual( + (setting.id, setting.project_id, setting.stateful), data + ) + + def test_create_no_statefulness_arg(self): + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + [], + [], + ) + + +class TestDeleteSecurityGroupDefaultStatefulness( + network_fakes.TestNetworkV2, +): + def setUp(self): + super().setUp() + self.network_client.delete_security_groups_default_statefulness.return_value = None + self.cmd = security_groups_default_statefulness.DeleteSecurityGroupDefaultStatefulness( + self.app, None + ) + self._settings = list( + sdk_fakes.generate_fake_resources( + _sg_default_statefulness.SecurityGroupsDefaultStatefulness, + count=2, + attrs={'stateful': False, 'location': 'MUNCHMUNCHMUNCH'}, + ) + ) + + def test_delete_one(self): + arglist = [self._settings[0].id] + verifylist = [('setting', [self._settings[0].id])] + self.network_client.find_security_groups_default_statefulness.return_value = self._settings[ + 0 + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_security_groups_default_statefulness.assert_called_once_with( + self._settings[0] + ) + self.assertIsNone(result) + + def test_delete_multi(self): + arglist = [s.id for s in self._settings] + verifylist = [('setting', arglist)] + self.network_client.find_security_groups_default_statefulness.side_effect = self._settings + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + calls = [call(s) for s in self._settings] + self.network_client.delete_security_groups_default_statefulness.assert_has_calls( + calls + ) + self.assertIsNone(result) + + def test_delete_with_exception(self): + arglist = [self._settings[0].id, 'nonexistent'] + verifylist = [ + ('setting', [self._settings[0].id, 'nonexistent']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + find_mock_result = [ + self._settings[0], + exceptions.CommandError, + ] + self.network_client.find_security_groups_default_statefulness.side_effect = find_mock_result + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual( + '1 of 2 default statefulness settings failed to delete.', + str(e), + ) + + self.network_client.find_security_groups_default_statefulness.assert_any_call( + self._settings[0].id, ignore_missing=False + ) + self.network_client.find_security_groups_default_statefulness.assert_any_call( + 'nonexistent', ignore_missing=False + ) + self.network_client.delete_security_groups_default_statefulness.assert_called_once_with( + self._settings[0] + ) + + +class TestListSecurityGroupDefaultStatefulness( + network_fakes.TestNetworkV2, +): + def setUp(self): + super().setUp() + self._setting_system = _generate_fake_setting( + {'stateful': False, 'project_id': None} + ) + self._setting_project = _generate_fake_setting( + {'stateful': True, 'project_id': 'project-id-1'} + ) + self.expected_columns = ( + 'ID', + 'Project ID', + 'Stateful', + ) + self.expected_data = [ + ( + self._setting_system.id, + self._setting_system.project_id, + self._setting_system.stateful, + ), + ( + self._setting_project.id, + self._setting_project.project_id, + self._setting_project.stateful, + ), + ] + self._settings = [self._setting_system, self._setting_project] + self.network_client.security_groups_default_statefulness.return_value = self._settings + self.cmd = security_groups_default_statefulness.ListSecurityGroupDefaultStatefulness( + self.app, None + ) + + def test_list_all(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.security_groups_default_statefulness.assert_called_once_with() + self.assertEqual(self.expected_columns, columns) + self.assertEqual(self.expected_data, list(data)) + + +class TestSetSecurityGroupDefaultStatefulness( + network_fakes.TestNetworkV2, +): + def setUp(self): + super().setUp() + self._setting = _generate_fake_setting({'stateful': False}) + self.network_client.find_security_groups_default_statefulness.return_value = self._setting + self.network_client.update_security_groups_default_statefulness.return_value = None + self.cmd = security_groups_default_statefulness.SetSecurityGroupDefaultStatefulness( + self.app, None + ) + + def test_set_stateful(self): + arglist = [self._setting.id, '--stateful'] + verifylist = [ + ('setting', self._setting.id), + ('stateful', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network_client.update_security_groups_default_statefulness.assert_called_once_with( + self._setting, **{'stateful': True} + ) + self.assertIsNone(result) + + def test_set_stateless(self): + arglist = [self._setting.id, '--stateless'] + verifylist = [ + ('setting', self._setting.id), + ('stateful', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.network_client.update_security_groups_default_statefulness.assert_called_once_with( + self._setting, **{'stateful': False} + ) + self.assertIsNone(result) + + def test_set_no_statefulness_arg(self): + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + [self._setting.id], + [], + ) + + +class TestShowSecurityGroupDefaultStatefulness( + network_fakes.TestNetworkV2, +): + expected_columns = ( + 'id', + 'project_id', + 'stateful', + ) + + def setUp(self): + super().setUp() + self._setting = _generate_fake_setting() + self.network_client.find_security_groups_default_statefulness.return_value = self._setting + self.cmd = security_groups_default_statefulness.ShowSecurityGroupDefaultStatefulness( + self.app, None + ) + + def test_show(self): + arglist = [self._setting.id] + verifylist = [('setting', self._setting.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.find_security_groups_default_statefulness.assert_called_once_with( + self._setting.id, ignore_missing=False + ) + self.assertEqual(self.expected_columns, columns) + self.assertEqual( + ( + self._setting.id, + self._setting.project_id, + self._setting.stateful, + ), + data, + ) diff --git a/openstackclient/tests/unit/network/v2/test_subnet.py b/openstackclient/tests/unit/network/v2/test_subnet.py index 8b82074116..dd3f9ad6be 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -11,14 +11,15 @@ # under the License. # -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from openstackclient.network.v2 import subnet as subnet_v2 -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as tests_utils @@ -27,16 +28,11 @@ class TestSubnet(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - class TestCreateSubnet(TestSubnet): def _init_subnet_variables(self): - self.project = identity_fakes_v3.FakeProject.create_one_project() - self.domain = identity_fakes_v3.FakeDomain.create_one_domain() + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.domain = sdk_fakes.generate_fake_resource(_domain.Domain) # An IPv4 subnet to be created with mostly default values self._subnet = network_fakes.FakeSubnet.create_one_subnet( attrs={ @@ -262,23 +258,17 @@ def setUp(self): # Get the command object to test self.cmd = subnet_v2.CreateSubnet(self.app, None) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project # Mock SDK calls for all tests. - self.network_client.create_subnet = mock.Mock( - return_value=self._subnet - ) - self.network_client.set_tags = mock.Mock(return_value=None) - self.network_client.find_network = mock.Mock( - return_value=self._network - ) - self.network_client.find_segment = mock.Mock( - return_value=self._network_segment - ) - self.network_client.find_subnet_pool = mock.Mock( - return_value=self._subnet_pool - ) + self.network_client.create_subnet.return_value = self._subnet + + self.network_client.set_tags.return_value = None + self.network_client.find_network.return_value = self._network + + self.network_client.find_segment.return_value = self._network_segment + + self.network_client.find_subnet_pool.return_value = self._subnet_pool def test_create_no_options(self): arglist = [] @@ -333,7 +323,7 @@ def test_create_default_options(self): def test_create_from_subnet_pool_options(self): # Mock SDK calls for this test. self.network_client.create_subnet.return_value = self._subnet_from_pool - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.set_tags.return_value = None self._network.id = self._subnet_from_pool.network_id arglist = [ @@ -721,7 +711,7 @@ class TestDeleteSubnet(TestSubnet): def setUp(self): super().setUp() - self.network_client.delete_subnet = mock.Mock(return_value=None) + self.network_client.delete_subnet.return_value = None self.network_client.find_subnet = network_fakes.FakeSubnet.get_subnets( self._subnets @@ -775,9 +765,7 @@ def test_multi_subnets_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self._subnets[0], exceptions.CommandError] - self.network_client.find_subnet = mock.Mock( - side_effect=find_mock_result - ) + self.network_client.find_subnet.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -798,7 +786,7 @@ def test_multi_subnets_delete_with_exception(self): class TestListSubnet(TestSubnet): # The subnets going to be listed up. - _subnet = network_fakes.FakeSubnet.create_subnets(count=3) + _subnets = network_fakes.FakeSubnet.create_subnets(count=3) columns = ( 'ID', @@ -806,7 +794,8 @@ class TestListSubnet(TestSubnet): 'Network', 'Subnet', ) - columns_long = columns + ( + columns_long = ( + *columns, 'Project', 'DHCP', 'Name Servers', @@ -819,7 +808,7 @@ class TestListSubnet(TestSubnet): ) data = [] - for subnet in _subnet: + for subnet in _subnets: data.append( ( subnet.id, @@ -830,7 +819,7 @@ class TestListSubnet(TestSubnet): ) data_long = [] - for subnet in _subnet: + for subnet in _subnets: data_long.append( ( subnet.id, @@ -855,7 +844,7 @@ def setUp(self): # Get the command object to test self.cmd = subnet_v2.ListSubnet(self.app, None) - self.network_client.subnets = mock.Mock(return_value=self._subnet) + self.network_client.subnets.return_value = self._subnets def test_subnet_list_no_options(self): arglist = [] @@ -885,6 +874,30 @@ def test_subnet_list_long(self): self.assertEqual(self.columns_long, columns) self.assertCountEqual(self.data_long, list(data)) + def test_subnet_list_pagination(self): + arglist = [ + '--marker', + self._subnets[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self._subnets[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.subnets.assert_called_once_with( + **{ + 'marker': self._subnets[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_subnet_list_ip_version(self): arglist = [ '--ip-version', @@ -951,8 +964,8 @@ def test_subnet_list_service_type(self): self.assertCountEqual(self.data, list(data)) def test_subnet_list_project(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -996,8 +1009,8 @@ def test_subnet_list_service_type_multiple(self): self.assertCountEqual(self.data, list(data)) def test_subnet_list_project_domain(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -1019,7 +1032,7 @@ def test_subnet_list_project_domain(self): def test_subnet_list_network(self): network = network_fakes.create_one_network() - self.network_client.find_network = mock.Mock(return_value=network) + self.network_client.find_network.return_value = network arglist = [ '--network', network.id, @@ -1038,7 +1051,7 @@ def test_subnet_list_network(self): def test_subnet_list_gateway(self): subnet = network_fakes.FakeSubnet.create_one_subnet() - self.network_client.find_network = mock.Mock(return_value=subnet) + self.network_client.find_network.return_value = subnet arglist = [ '--gateway', subnet.gateway_ip, @@ -1057,7 +1070,7 @@ def test_subnet_list_gateway(self): def test_subnet_list_name(self): subnet = network_fakes.FakeSubnet.create_one_subnet() - self.network_client.find_network = mock.Mock(return_value=subnet) + self.network_client.find_network.return_value = subnet arglist = [ '--name', subnet.name, @@ -1076,7 +1089,7 @@ def test_subnet_list_name(self): def test_subnet_list_subnet_range(self): subnet = network_fakes.FakeSubnet.create_one_subnet() - self.network_client.find_network = mock.Mock(return_value=subnet) + self.network_client.find_network.return_value = subnet arglist = [ '--subnet-range', subnet.cidr, @@ -1098,10 +1111,9 @@ def test_subnet_list_subnetpool_by_name(self): subnet = network_fakes.FakeSubnet.create_one_subnet( {'subnetpool_id': subnet_pool.id} ) - self.network_client.find_network = mock.Mock(return_value=subnet) - self.network_client.find_subnet_pool = mock.Mock( - return_value=subnet_pool - ) + self.network_client.find_network.return_value = subnet + self.network_client.find_subnet_pool.return_value = subnet_pool + arglist = [ '--subnet-pool', subnet_pool.name, @@ -1123,10 +1135,9 @@ def test_subnet_list_subnetpool_by_id(self): subnet = network_fakes.FakeSubnet.create_one_subnet( {'subnetpool_id': subnet_pool.id} ) - self.network_client.find_network = mock.Mock(return_value=subnet) - self.network_client.find_subnet_pool = mock.Mock( - return_value=subnet_pool - ) + self.network_client.find_network.return_value = subnet + self.network_client.find_subnet_pool.return_value = subnet_pool + arglist = [ '--subnet-pool', subnet_pool.id, @@ -1182,9 +1193,9 @@ class TestSetSubnet(TestSubnet): def setUp(self): super().setUp() - self.network_client.update_subnet = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) - self.network_client.find_subnet = mock.Mock(return_value=self._subnet) + self.network_client.update_subnet.return_value = None + self.network_client.set_tags.return_value = None + self.network_client.find_subnet.return_value = self._subnet self.cmd = subnet_v2.SetSubnet(self.app, None) def test_set_this(self): @@ -1263,7 +1274,7 @@ def test_append_options(self): 'service_types': ["network:router_gateway"], } ) - self.network_client.find_subnet = mock.Mock(return_value=_testsubnet) + self.network_client.find_subnet.return_value = _testsubnet arglist = [ '--dns-nameserver', '10.0.0.2', @@ -1329,7 +1340,7 @@ def test_overwrite_options(self): 'dns_nameservers': ["10.0.0.1"], } ) - self.network_client.find_subnet = mock.Mock(return_value=_testsubnet) + self.network_client.find_subnet.return_value = _testsubnet arglist = [ '--host-route', 'destination=10.30.30.30/24,gateway=10.30.30.1', @@ -1379,7 +1390,7 @@ def test_clear_options(self): 'dns_nameservers': ['10.0.0.1'], } ) - self.network_client.find_subnet = mock.Mock(return_value=_testsubnet) + self.network_client.find_subnet.return_value = _testsubnet arglist = [ '--no-host-route', '--no-allocation-pool', @@ -1448,8 +1459,8 @@ def test_set_segment(self): 'segment_id': None, } ) - self.network_client.find_subnet = mock.Mock(return_value=_subnet) - self.network_client.find_segment = mock.Mock(return_value=_segment) + self.network_client.find_subnet.return_value = _subnet + self.network_client.find_segment.return_value = _segment arglist = ['--network-segment', _segment.id, _subnet.name] verifylist = [('network_segment', _segment.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1514,7 +1525,7 @@ def setUp(self): # Get the command object to test self.cmd = subnet_v2.ShowSubnet(self.app, None) - self.network_client.find_subnet = mock.Mock(return_value=self._subnet) + self.network_client.find_subnet.return_value = self._subnet def test_show_no_options(self): arglist = [] @@ -1572,11 +1583,10 @@ def setUp(self): 'tags': ['green', 'red'], } ) - self.network_client.find_subnet = mock.Mock( - return_value=self._testsubnet - ) - self.network_client.update_subnet = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.find_subnet.return_value = self._testsubnet + + self.network_client.update_subnet.return_value = None + self.network_client.set_tags.return_value = None # Get the command object to test self.cmd = subnet_v2.UnsetSubnet(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/test_subnet_pool.py b/openstackclient/tests/unit/network/v2/test_subnet_pool.py index 40aa1e990b..0d0043d285 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/unit/network/v2/test_subnet_pool.py @@ -10,31 +10,22 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest import mock from unittest.mock import call +from openstack.identity.v3 import domain as _domain +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from openstackclient.network.v2 import subnet_pool -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes_v3 from openstackclient.tests.unit.network.v2 import fakes as network_fakes from openstackclient.tests.unit import utils as test_utils -class TestSubnetPool(network_fakes.TestNetworkV2): - def setUp(self): - super().setUp() - - # Get a shortcut to the ProjectManager Mock - self.projects_mock = self.identity_client.projects - # Get a shortcut to the DomainManager Mock - self.domains_mock = self.identity_client.domains - - -class TestCreateSubnetPool(TestSubnetPool): - project = identity_fakes_v3.FakeProject.create_one_project() - domain = identity_fakes_v3.FakeDomain.create_one_domain() +class TestCreateSubnetPool(network_fakes.TestNetworkV2): + project = sdk_fakes.generate_fake_resource(_project.Project) + domain = sdk_fakes.generate_fake_resource(_domain.Domain) # The new subnet pool to create. _subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() @@ -76,20 +67,18 @@ class TestCreateSubnetPool(TestSubnetPool): def setUp(self): super().setUp() - self.network_client.create_subnet_pool = mock.Mock( - return_value=self._subnet_pool - ) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.create_subnet_pool.return_value = self._subnet_pool + + self.network_client.set_tags.return_value = None # Get the command object to test self.cmd = subnet_pool.CreateSubnetPool(self.app, None) - self.network_client.find_address_scope = mock.Mock( - return_value=self._address_scope + self.network_client.find_address_scope.return_value = ( + self._address_scope ) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain + self.identity_sdk_client.find_project.return_value = self.project def test_create_no_options(self): arglist = [] @@ -380,14 +369,14 @@ def test_create_with_no_tag(self): self._test_create_with_tag(add_tags=False) -class TestDeleteSubnetPool(TestSubnetPool): +class TestDeleteSubnetPool(network_fakes.TestNetworkV2): # The subnet pools to delete. _subnet_pools = network_fakes.FakeSubnetPool.create_subnet_pools(count=2) def setUp(self): super().setUp() - self.network_client.delete_subnet_pool = mock.Mock(return_value=None) + self.network_client.delete_subnet_pool.return_value = None self.network_client.find_subnet_pool = ( network_fakes.FakeSubnetPool.get_subnet_pools(self._subnet_pools) @@ -445,9 +434,7 @@ def test_multi_subnet_pools_delete_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self._subnet_pools[0], exceptions.CommandError] - self.network_client.find_subnet_pool = mock.Mock( - side_effect=find_mock_result - ) + self.network_client.find_subnet_pool.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -466,7 +453,7 @@ def test_multi_subnet_pools_delete_with_exception(self): ) -class TestListSubnetPool(TestSubnetPool): +class TestListSubnetPool(network_fakes.TestNetworkV2): # The subnet pools going to be listed up. _subnet_pools = network_fakes.FakeSubnetPool.create_subnet_pools(count=3) @@ -475,7 +462,8 @@ class TestListSubnetPool(TestSubnetPool): 'Name', 'Prefixes', ) - columns_long = columns + ( + columns_long = ( + *columns, 'Default Prefix Length', 'Address Scope', 'Default Subnet Pool', @@ -514,9 +502,7 @@ def setUp(self): # Get the command object to test self.cmd = subnet_pool.ListSubnetPool(self.app, None) - self.network_client.subnet_pools = mock.Mock( - return_value=self._subnet_pools - ) + self.network_client.subnet_pools.return_value = self._subnet_pools def test_subnet_pool_list_no_option(self): arglist = [] @@ -546,6 +532,30 @@ def test_subnet_pool_list_long(self): self.assertEqual(self.columns_long, columns) self.assertCountEqual(self.data_long, list(data)) + def test_subnet_pool_list_pagination(self): + arglist = [ + '--marker', + self._subnet_pools[0].id, + '--limit', + '1', + ] + verifylist = [ + ('marker', self._subnet_pools[0].id), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.network_client.subnet_pools.assert_called_once_with( + **{ + 'marker': self._subnet_pools[0].id, + 'limit': 1, + } + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + def test_subnet_pool_list_no_share(self): arglist = [ '--no-share', @@ -611,8 +621,8 @@ def test_subnet_pool_list_default(self): self.assertCountEqual(self.data, list(data)) def test_subnet_pool_list_project(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -630,8 +640,8 @@ def test_subnet_pool_list_project(self): self.assertCountEqual(self.data, list(data)) def test_subnet_pool_list_project_domain(self): - project = identity_fakes_v3.FakeProject.create_one_project() - self.projects_mock.get.return_value = project + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', project.id, @@ -653,7 +663,7 @@ def test_subnet_pool_list_project_domain(self): def test_subnet_pool_list_name(self): subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() - self.network_client.find_network = mock.Mock(return_value=subnet_pool) + self.network_client.find_network.return_value = subnet_pool arglist = [ '--name', subnet_pool.name, @@ -672,9 +682,8 @@ def test_subnet_pool_list_name(self): def test_subnet_pool_list_address_scope(self): addr_scope = network_fakes.create_one_address_scope() - self.network_client.find_address_scope = mock.Mock( - return_value=addr_scope - ) + self.network_client.find_address_scope.return_value = addr_scope + arglist = [ '--address-scope', addr_scope.id, @@ -723,7 +732,7 @@ def test_list_with_tag_options(self): self.assertCountEqual(self.data, list(data)) -class TestSetSubnetPool(TestSubnetPool): +class TestSetSubnetPool(network_fakes.TestNetworkV2): # The subnet_pool to set. _subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool( {'default_quota': 10, 'tags': ['green', 'red']} @@ -734,15 +743,13 @@ class TestSetSubnetPool(TestSubnetPool): def setUp(self): super().setUp() - self.network_client.update_subnet_pool = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.update_subnet_pool.return_value = None + self.network_client.set_tags.return_value = None - self.network_client.find_subnet_pool = mock.Mock( - return_value=self._subnet_pool - ) + self.network_client.find_subnet_pool.return_value = self._subnet_pool - self.network_client.find_address_scope = mock.Mock( - return_value=self._address_scope + self.network_client.find_address_scope.return_value = ( + self._address_scope ) # Get the command object to test @@ -1038,7 +1045,7 @@ def test_set_with_no_tag(self): self._test_set_tags(with_tags=False) -class TestShowSubnetPool(TestSubnetPool): +class TestShowSubnetPool(network_fakes.TestNetworkV2): # The subnet_pool to set. _subnet_pool = network_fakes.FakeSubnetPool.create_one_subnet_pool() @@ -1079,9 +1086,7 @@ class TestShowSubnetPool(TestSubnetPool): def setUp(self): super().setUp() - self.network_client.find_subnet_pool = mock.Mock( - return_value=self._subnet_pool - ) + self.network_client.find_subnet_pool.return_value = self._subnet_pool # Get the command object to test self.cmd = subnet_pool.ShowSubnetPool(self.app, None) @@ -1116,17 +1121,16 @@ def test_show_all_options(self): self.assertCountEqual(self.data, data) -class TestUnsetSubnetPool(TestSubnetPool): +class TestUnsetSubnetPool(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self._subnetpool = network_fakes.FakeSubnetPool.create_one_subnet_pool( {'tags': ['green', 'red']} ) - self.network_client.find_subnet_pool = mock.Mock( - return_value=self._subnetpool - ) - self.network_client.update_subnet_pool = mock.Mock(return_value=None) - self.network_client.set_tags = mock.Mock(return_value=None) + self.network_client.find_subnet_pool.return_value = self._subnetpool + + self.network_client.update_subnet_pool.return_value = None + self.network_client.set_tags.return_value = None # Get the command object to test self.cmd = subnet_pool.UnsetSubnetPool(self.app, None) diff --git a/openstackclient/tests/unit/network/v2/vpnaas/__init__.py b/openstackclient/tests/unit/network/v2/vpnaas/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/tests/unit/network/v2/vpnaas/fakes.py b/openstackclient/tests/unit/network/v2/vpnaas/fakes.py new file mode 100644 index 0000000000..2cbfbe8c6a --- /dev/null +++ b/openstackclient/tests/unit/network/v2/vpnaas/fakes.py @@ -0,0 +1,199 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import collections +from unittest import mock +import uuid + +from openstack.network.v2 import vpn_endpoint_group as vpn_epg +from openstack.network.v2 import vpn_ike_policy as vpn_ikep +from openstack.network.v2 import vpn_ipsec_policy as vpn_ipsecp +from openstack.network.v2 import vpn_ipsec_site_connection as vpn_sitec +from openstack.network.v2 import vpn_service + + +class FakeVPNaaS: + def create(self, attrs={}): + """Create a fake vpnaas resources + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A OrderedDict faking the vpnaas resource + """ + self.ordered.update(attrs) + if 'IKEPolicy' == self.__class__.__name__: + return vpn_ikep.VpnIkePolicy(**self.ordered) + if 'IPSecPolicy' == self.__class__.__name__: + return vpn_ipsecp.VpnIpsecPolicy(**self.ordered) + if 'VPNService' == self.__class__.__name__: + return vpn_service.VpnService(**self.ordered) + if 'EndpointGroup' == self.__class__.__name__: + return vpn_epg.VpnEndpointGroup(**self.ordered) + + def bulk_create(self, attrs=None, count=2): + """Create multiple fake vpnaas resources + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of vpnaas resources to fake + :return: + A list of dictionaries faking the vpnaas resources + """ + return [self.create(attrs=attrs) for i in range(0, count)] + + def get(self, attrs=None, count=2): + """Get multiple fake vpnaas resources + + :param Dictionary attrs: + A dictionary with all attributes + :param int count: + The number of vpnaas resources to fake + :return: + A list of dictionaries faking the vpnaas resource + """ + if attrs is None: + self.attrs = self.bulk_create(count=count) + return mock.Mock(side_effect=attrs) + + +class IKEPolicy(FakeVPNaaS): + """Fake one or more IKE policies""" + + def __init__(self): + super().__init__() + self.ordered = collections.OrderedDict( + ( + ('id', 'ikepolicy-id-' + uuid.uuid4().hex), + ('name', 'my-ikepolicy-' + uuid.uuid4().hex), + ('auth_algorithm', 'sha1'), + ('encryption_algorithm', 'aes-128'), + ('ike_version', 'v1'), + ('pfs', 'group5'), + ('description', 'my-desc-' + uuid.uuid4().hex), + ('phase1_negotiation_mode', 'main'), + ('project_id', 'project-id-' + uuid.uuid4().hex), + ('lifetime', {'units': 'seconds', 'value': 3600}), + ) + ) + + +class IPSecPolicy(FakeVPNaaS): + """Fake one or more IPsec policies""" + + def __init__(self): + super().__init__() + self.ordered = collections.OrderedDict( + ( + ('id', 'ikepolicy-id-' + uuid.uuid4().hex), + ('name', 'my-ikepolicy-' + uuid.uuid4().hex), + ('auth_algorithm', 'sha1'), + ('encapsulation_mode', 'tunnel'), + ('transform_protocol', 'esp'), + ('encryption_algorithm', 'aes-128'), + ('pfs', 'group5'), + ('description', 'my-desc-' + uuid.uuid4().hex), + ('project_id', 'project-id-' + uuid.uuid4().hex), + ('lifetime', {'units': 'seconds', 'value': 3600}), + ) + ) + + +class VPNService(FakeVPNaaS): + """Fake one or more VPN services""" + + def __init__(self): + super().__init__() + self.ordered = collections.OrderedDict( + ( + ('id', 'vpnservice-id-' + uuid.uuid4().hex), + ('name', 'my-vpnservice-' + uuid.uuid4().hex), + ('router_id', 'router-id-' + uuid.uuid4().hex), + ('subnet_id', 'subnet-id-' + uuid.uuid4().hex), + ('flavor_id', 'flavor-id-' + uuid.uuid4().hex), + ('admin_state_up', True), + ('status', 'ACTIVE'), + ('description', 'my-desc-' + uuid.uuid4().hex), + ('project_id', 'project-id-' + uuid.uuid4().hex), + ('external_v4_ip', '192.0.2.42'), + ('external_v6_ip', '2001:0db8:207a:4a3a:053b:6fab:7df9:1afd'), + ) + ) + + +class EndpointGroup(FakeVPNaaS): + """Fake one or more Endpoint Groups""" + + def __init__(self): + super().__init__() + self.ordered = collections.OrderedDict( + ( + ('id', 'ep-group-id-' + uuid.uuid4().hex), + ('name', 'my-ep-group-' + uuid.uuid4().hex), + ('type', 'cidr'), + ('endpoints', ['10.0.0.0/24', '20.0.0.0/24']), + ('description', 'my-desc-' + uuid.uuid4().hex), + ('project_id', 'project-id-' + uuid.uuid4().hex), + ) + ) + + +class IPsecSiteConnection: + """Fake one or more IPsec site connections""" + + @staticmethod + def create_conn(attrs=None): + """Create a fake IPsec conn. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A Dictionary with id, name, peer_address, auth_mode, status, + project_id, peer_cidrs, vpnservice_id, ipsecpolicy_id, + ikepolicy_id, mtu, initiator, admin_state_up, description, + psk, route_mode, local_id, peer_id, local_ep_group_id, + peer_ep_group_id + """ + attrs = attrs or {} + + # Set default attributes. + conn_attrs = { + 'id': 'ipsec-site-conn-id-' + uuid.uuid4().hex, + 'name': 'my-ipsec-site-conn-' + uuid.uuid4().hex, + 'peer_address': '192.168.2.10', + 'auth_mode': '', + 'status': '', + 'project_id': 'project-id-' + uuid.uuid4().hex, + 'peer_cidrs': [], + 'vpnservice_id': 'vpnservice-id-' + uuid.uuid4().hex, + 'ipsecpolicy_id': 'ipsecpolicy-id-' + uuid.uuid4().hex, + 'ikepolicy_id': 'ikepolicy-id-' + uuid.uuid4().hex, + 'mtu': 1500, + 'initiator': 'bi-directional', + 'admin_state_up': True, + 'description': 'my-vpn-connection', + 'psk': 'abcd', + 'route_mode': '', + 'local_id': '', + 'peer_id': '192.168.2.10', + 'local_ep_group_id': 'local-ep-group-id-' + uuid.uuid4().hex, + 'peer_ep_group_id': 'peer-ep-group-id-' + uuid.uuid4().hex, + } + + # Overwrite default attributes. + conn_attrs.update(attrs) + return vpn_sitec.VpnIPSecSiteConnection(**conn_attrs) diff --git a/openstackclient/tests/unit/network/v2/vpnaas/test_endpoint_group.py b/openstackclient/tests/unit/network/v2/vpnaas/test_endpoint_group.py new file mode 100644 index 0000000000..55def7efbf --- /dev/null +++ b/openstackclient/tests/unit/network/v2/vpnaas/test_endpoint_group.py @@ -0,0 +1,309 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from openstack.identity.v3 import project as _project +from openstack.network.v2 import vpn_endpoint_group as _vpn_endpoint_group +from openstack.test import fakes as sdk_fakes +from openstackclient.tests.unit import utils as tests_utils +from osc_lib import exceptions + +from openstackclient.network.v2.vpnaas import endpoint_group +from openstackclient.tests.unit.network.v2 import fakes as test_fakes +from openstackclient.tests.unit.network.v2.vpnaas import fakes + + +class TestEndpointGroup(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + + self._endpoint_group = fakes.EndpointGroup().create() + + def _mock_endpoint_group(*args, **kwargs): + self.network_client.find_vpn_endpoint_group.assert_called_once_with( + self._endpoint_group['id'], ignore_missing=False + ) + return {'id': args[0]} + + self.network_client.find_vpn_endpoint_group.side_effect = mock.Mock( + side_effect=_mock_endpoint_group + ) + self.headers = ( + 'ID', + 'Name', + 'Type', + 'Endpoints', + 'Description', + 'Project', + ) + self.ordered_headers = ( + 'Description', + 'Endpoints', + 'ID', + 'Name', + 'Project', + 'Type', + ) + self.ordered_data = ( + self._endpoint_group['description'], + self._endpoint_group['endpoints'], + self._endpoint_group['id'], + self._endpoint_group['name'], + self._endpoint_group['project_id'], + self._endpoint_group['type'], + ) + self.ordered_columns = ( + 'description', + 'endpoints', + 'id', + 'name', + 'project_id', + 'type', + ) + + +class TestCreateEndpointGroup(TestEndpointGroup): + def setUp(self): + super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + + self.network_client.create_vpn_endpoint_group.return_value = ( + self._endpoint_group + ) + + self.cmd = endpoint_group.CreateEndpointGroup(self.app, None) + + def test_create_with_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_all_params_cidr(self): + self.network_client.create_vpn_endpoint_group.return_value = ( + self._endpoint_group + ) + + arglist = [ + '--description', + 'my-desc', + '--type', + 'cidr', + '--value', + '10.0.0.0/24', + '--value', + '20.0.0.0/24', + '--project', + self.project.id, + self._endpoint_group.name, + ] + verifylist = [ + ('description', 'my-desc'), + ('type', 'cidr'), + ('endpoints', ['10.0.0.0/24', '20.0.0.0/24']), + ('project', self.project.id), + ('name', self._endpoint_group.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.ordered_data = tuple( + self._endpoint_group[column] for column in self.ordered_columns + ) + self.network_client.create_vpn_endpoint_group.assert_called_once_with( + description='my-desc', + type='cidr', + endpoints=['10.0.0.0/24', '20.0.0.0/24'], + project_id=self.project.id, + name=self._endpoint_group.name, + ) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + self.assertEqual(self.ordered_data, data) + + +class TestDeleteEndpointGroup(TestEndpointGroup): + def setUp(self): + super().setUp() + self.cmd = endpoint_group.DeleteEndpointGroup(self.app, None) + + def test_delete_with_one_resource(self): + self.network_client.find_vpn_endpoint_group.side_effect = [ + self._endpoint_group + ] + + arglist = [self._endpoint_group.id] + verifylist = [('endpoint_group', [self._endpoint_group.id])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_vpn_endpoint_group.assert_called_once_with( + self._endpoint_group.id + ) + self.assertIsNone(result) + + def test_delete_with_multiple_resources(self): + + def _mock_e_g(*args, **kwargs): + return _vpn_endpoint_group.VpnEndpointGroup(**{'id': args[0]}) + + self.network_client.find_vpn_endpoint_group.side_effect = _mock_e_g + + arglist = ['target1', 'target2'] + verifylist = [('endpoint_group', ['target1', 'target2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertEqual( + 2, self.network_client.delete_vpn_endpoint_group.call_count + ) + for idx, reference in enumerate(['target1', 'target2']): + actual = ''.join( + self.network_client.delete_vpn_endpoint_group.call_args_list[ + idx + ][0] + ) + self.assertEqual(reference, actual) + + def test_delete_multiple_with_exception(self): + arglist = ['target1'] + verifylist = [('endpoint_group', ['target1'])] + + self.network_client.find_vpn_ipsec_policy.side_effect = [ + 'target1', + exceptions.CommandError, + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestListEndpointGroup(TestEndpointGroup): + def setUp(self): + super().setUp() + self.cmd = endpoint_group.ListEndpointGroup(self.app, None) + + self.short_header = ( + 'ID', + 'Name', + 'Type', + 'Endpoints', + ) + + self.short_data = ( + self._endpoint_group['id'], + self._endpoint_group['name'], + self._endpoint_group['type'], + self._endpoint_group['endpoints'], + ) + + self.network_client.vpn_endpoint_groups.return_value = [ + self._endpoint_group + ] + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + + self.network_client.vpn_endpoint_groups.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.vpn_endpoint_groups.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + +class TestSetEndpointGroup(TestEndpointGroup): + def setUp(self): + super().setUp() + + self.network_client.update_vpn_endpoint_group.return_value = ( + self._endpoint_group + ) + self.cmd = endpoint_group.SetEndpointGroup(self.app, None) + + def test_set_name(self): + arglist = [self._endpoint_group.id, '--name', 'foo'] + verifylist = [ + ('endpoint_group', self._endpoint_group['id']), + ('name', 'foo'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_endpoint_group.assert_called_once_with( + self._endpoint_group.id, name='foo' + ) + self.assertIsNone(result) + + def test_set_description(self): + arglist = [self._endpoint_group.id, '--description', 'foo'] + verifylist = [ + ('endpoint_group', self._endpoint_group.id), + ('description', 'foo'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_endpoint_group.assert_called_once_with( + self._endpoint_group.id, description='foo' + ) + self.assertIsNone(result) + + +class TestShowEndpointGroup(TestEndpointGroup): + def setUp(self): + super().setUp() + + self.network_client.find_vpn_endpoint_group.return_value = ( + self._endpoint_group + ) + self.network_client.find_vpn_endpoint_group.side_effect = None + self.cmd = endpoint_group.ShowEndpointGroup(self.app, None) + + def test_show_filtered_by_id_or_name(self): + arglist = [self._endpoint_group.id] + verifylist = [('endpoint_group', self._endpoint_group.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.find_vpn_endpoint_group.assert_called_once_with( + self._endpoint_group.id, ignore_missing=False + ) + self.assertEqual(self.ordered_headers, headers) + self.assertCountEqual(self.ordered_data, data) diff --git a/openstackclient/tests/unit/network/v2/vpnaas/test_ikepolicy.py b/openstackclient/tests/unit/network/v2/vpnaas/test_ikepolicy.py new file mode 100644 index 0000000000..46fa1430fc --- /dev/null +++ b/openstackclient/tests/unit/network/v2/vpnaas/test_ikepolicy.py @@ -0,0 +1,390 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from openstack.identity.v3 import project as _project +from openstack.network.v2 import vpn_ike_policy as _vpn_ike_policy +from openstack.test import fakes as sdk_fakes +from osc_lib import exceptions + +from openstackclient.network.v2.vpnaas import ikepolicy +from openstackclient.tests.unit.network.v2 import fakes as test_fakes +from openstackclient.tests.unit.network.v2.vpnaas import fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestIKEPolicy(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + + self._ikepolicy = fakes.IKEPolicy().create() + + def _mock_ikepolicy(*args, **kwargs): + self.network_client.find_vpn_ike_policy.assert_called_once_with( + self._ikepolicy.id, ignore_missing=False + ) + return {'id': args[0]} + + self.network_client.find_vpn_ike_policy.side_effect = mock.Mock( + side_effect=_mock_ikepolicy + ) + self.headers = ( + 'ID', + 'Name', + 'Authentication Algorithm', + 'Encryption Algorithm', + 'IKE Version', + 'Perfect Forward Secrecy (PFS)', + 'Description', + 'Phase1 Negotiation Mode', + 'Project', + 'Lifetime', + ) + self.ordered_headers = ( + 'Authentication Algorithm', + 'Description', + 'Encryption Algorithm', + 'ID', + 'IKE Version', + 'Lifetime', + 'Name', + 'Perfect Forward Secrecy (PFS)', + 'Phase1 Negotiation Mode', + 'Project', + ) + self.ordered_data = ( + self._ikepolicy['auth_algorithm'], + self._ikepolicy['description'], + self._ikepolicy['encryption_algorithm'], + self._ikepolicy.id, + self._ikepolicy['ike_version'], + self._ikepolicy['lifetime'], + self._ikepolicy['name'], + self._ikepolicy['pfs'], + self._ikepolicy['phase1_negotiation_mode'], + self._ikepolicy['project_id'], + ) + self.ordered_columns = ( + 'auth_algorithm', + 'description', + 'encryption_algorithm', + 'id', + 'ike_version', + 'lifetime', + 'name', + 'pfs', + 'phase1_negotiation_mode', + 'project_id', + ) + + +class TestCreateIKEPolicy(TestIKEPolicy): + def setUp(self): + super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + self.network_client.create_vpn_ike_policy.return_value = ( + self._ikepolicy + ) + + self.cmd = ikepolicy.CreateIKEPolicy(self.app, None) + + def test_create_with_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_all_params(self): + self.network_client.create_vpn_ike_policy.return_value = ( + self._ikepolicy + ) + + arglist = [ + '--description', + 'my-desc', + '--auth-algorithm', + 'sha1', + '--encryption-algorithm', + 'aes-128', + '--phase1-negotiation-mode', + 'main', + '--ike-version', + 'v1', + '--pfs', + 'group5', + '--project', + self.project.id, + self._ikepolicy.name, + ] + verifylist = [ + ('description', 'my-desc'), + ('auth_algorithm', 'sha1'), + ('encryption_algorithm', 'aes-128'), + ('phase1_negotiation_mode', 'main'), + ('ike_version', 'v1'), + ('pfs', 'group5'), + ('project', self.project.id), + ('name', self._ikepolicy.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.ordered_data = tuple( + self._ikepolicy[column] for column in self.ordered_columns + ) + self.network_client.create_vpn_ike_policy.assert_called_once_with( + description='my-desc', + auth_algorithm='sha1', + encryption_algorithm='aes-128', + phase1_negotiation_mode='main', + ike_version='v1', + pfs='group5', + project_id=self.project.id, + name=self._ikepolicy.name, + ) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + self.assertEqual(self.ordered_data, data) + + +class TestDeleteIKEPolicy(TestIKEPolicy): + def setUp(self): + super().setUp() + + self.cmd = ikepolicy.DeleteIKEPolicy(self.app, None) + + def test_delete_with_one_resource(self): + def _mock_ike_p(*args, **kwargs): + return _vpn_ike_policy.VpnIkePolicy(**{'id': args[0]}) + + self.network_client.find_vpn_ike_policy.side_effect = _mock_ike_p + + arglist = [self._ikepolicy.id] + verifylist = [('ikepolicy', [self._ikepolicy.id])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_vpn_ike_policy.assert_called_once_with( + self._ikepolicy.id + ) + self.assertIsNone(result) + + def test_delete_with_multiple_resources(self): + + def _mock_ike_p(*args, **kwargs): + return _vpn_ike_policy.VpnIkePolicy(**{'id': args[0]}) + + self.network_client.find_vpn_ike_policy.side_effect = _mock_ike_p + + arglist = ['target1', 'target2'] + verifylist = [('ikepolicy', ['target1', 'target2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertEqual( + 2, self.network_client.delete_vpn_ike_policy.call_count + ) + for idx, reference in enumerate(['target1', 'target2']): + actual = ''.join( + self.network_client.delete_vpn_ike_policy.call_args_list[idx][ + 0 + ] + ) + self.assertEqual(reference, actual) + + def test_delete_multiple_with_exception(self): + arglist = ['target1'] + verifylist = [('ikepolicy', ['target1'])] + + self.network_client.find_vpn_ike_policy.side_effect = [ + 'target1', + exceptions.CommandError, + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + +class TestListIKEPolicy(TestIKEPolicy): + def setUp(self): + super().setUp() + + self.cmd = ikepolicy.ListIKEPolicy(self.app, None) + + self.short_header = ( + 'ID', + 'Name', + 'Authentication Algorithm', + 'Encryption Algorithm', + 'IKE Version', + 'Perfect Forward Secrecy (PFS)', + ) + + self.short_data = ( + self._ikepolicy.id, + self._ikepolicy['name'], + self._ikepolicy['auth_algorithm'], + self._ikepolicy['encryption_algorithm'], + self._ikepolicy['ike_version'], + self._ikepolicy['pfs'], + ) + + self.network_client.vpn_ike_policies.return_value = [self._ikepolicy] + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + + self.network_client.vpn_ike_policies.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.vpn_ike_policies.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + +class TestSetIKEPolicy(TestIKEPolicy): + def setUp(self): + super().setUp() + + self.network_client.update_vpn_ike_policy.return_value = ( + self._ikepolicy + ) + + self.cmd = ikepolicy.SetIKEPolicy(self.app, None) + + def test_set_auth_algorithm_with_sha256(self): + arglist = [ + self._ikepolicy.id, + '--auth-algorithm', + 'sha256', + ] + verifylist = [ + ('ikepolicy', self._ikepolicy.id), + ('auth_algorithm', 'sha256'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_ike_policy.assert_called_once_with( + self._ikepolicy.id, **{'auth_algorithm': 'sha256'} + ) + self.assertIsNone(result) + + def test_set_phase1_negotiation_mode_with_aggressive(self): + arglist = [ + self._ikepolicy.id, + '--phase1-negotiation-mode', + 'aggressive', + ] + verifylist = [ + ('ikepolicy', self._ikepolicy.id), + ('phase1_negotiation_mode', 'aggressive'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_ike_policy.assert_called_once_with( + self._ikepolicy.id, **{'phase1_negotiation_mode': 'aggressive'} + ) + self.assertIsNone(result) + + def test_set_name(self): + arglist = [ + self._ikepolicy.id, + '--name', + 'foo', + ] + verifylist = [ + ('ikepolicy', self._ikepolicy.id), + ('name', 'foo'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_ike_policy.assert_called_once_with( + self._ikepolicy.id, name='foo' + ) + self.assertIsNone(result) + + def test_set_description(self): + arglist = [ + self._ikepolicy.id, + '--description', + 'foo', + ] + verifylist = [ + ('ikepolicy', self._ikepolicy.id), + ('description', 'foo'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_ike_policy.assert_called_once_with( + self._ikepolicy.id, description='foo' + ) + self.assertIsNone(result) + + +class TestShowIKEPolicy(TestIKEPolicy): + def setUp(self): + super().setUp() + + self.network_client.find_vpn_ike_policy.side_effect = None + self.network_client.find_vpn_ike_policy.return_value = self._ikepolicy + + self.cmd = ikepolicy.ShowIKEPolicy(self.app, None) + + def test_show_filtered_by_id_or_name(self): + arglist = [self._ikepolicy.id] + verifylist = [('ikepolicy', self._ikepolicy.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.find_vpn_ike_policy.assert_called_once_with( + self._ikepolicy.id, ignore_missing=False + ) + self.assertEqual(self.ordered_headers, headers) + self.assertCountEqual(self.ordered_data, data) diff --git a/openstackclient/tests/unit/network/v2/vpnaas/test_ipsec_site_connection.py b/openstackclient/tests/unit/network/v2/vpnaas/test_ipsec_site_connection.py new file mode 100644 index 0000000000..4cc4754e61 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/vpnaas/test_ipsec_site_connection.py @@ -0,0 +1,468 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from osc_lib.cli import format_columns +from osc_lib import exceptions + +from openstack.identity.v3 import project as _project +from openstack.network.v2 import vpn_endpoint_group as _vpn_endpoint_group +from openstack.network.v2 import vpn_ike_policy as _vpn_ike_policy +from openstack.network.v2 import vpn_ipsec_policy as sdk_vpn_ipsecp_p +from openstack.network.v2 import vpn_ipsec_site_connection as sdk_vpn_ipsec_sc +from openstack.network.v2 import vpn_service as sdk_vpn_service +from openstack.test import fakes as sdk_fakes + +from openstackclient.network.v2.vpnaas import ipsec_site_connection +from openstackclient.tests.unit.network.v2 import fakes as test_fakes +from openstackclient.tests.unit.network.v2.vpnaas import fakes +from openstackclient.tests.unit import utils as tests_utils + + +class TestIPsecSiteConn(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + + self._ipsec_site_conn = fakes.IPsecSiteConnection().create_conn() + + def _mock_ipsec_site_conn(*args, **kwargs): + return {'id': args[0]} + + self.network_client.find_vpn_ipsec_site_connection.side_effect = ( + mock.Mock(side_effect=_mock_ipsec_site_conn) + ) + self.headers = ( + 'ID', + 'Name', + 'Peer Address', + 'Authentication Algorithm', + 'Status', + 'Project', + 'Peer CIDRs', + 'VPN Service', + 'IPSec Policy', + 'IKE Policy', + 'MTU', + 'Initiator', + 'State', + 'Description', + 'Pre-shared Key', + 'Route Mode', + 'Local ID', + 'Peer ID', + 'Local Endpoint Group ID', + 'Peer Endpoint Group ID', + 'DPD', + ) + self.ordered_headers = ( + 'Authentication Algorithm', + 'DPD', + 'Description', + 'ID', + 'IKE Policy', + 'IPSec Policy', + 'Initiator', + 'Local Endpoint Group ID', + 'Local ID', + 'MTU', + 'Name', + 'Peer Address', + 'Peer CIDRs', + 'Peer Endpoint Group ID', + 'Peer ID', + 'Pre-shared Key', + 'Project', + 'Route Mode', + 'State', + 'Status', + 'VPN Service', + ) + self.ordered_data = ( + self._ipsec_site_conn['auth_mode'], + self._ipsec_site_conn['dpd'], + self._ipsec_site_conn['description'], + self._ipsec_site_conn['id'], + self._ipsec_site_conn['ikepolicy_id'], + self._ipsec_site_conn['ipsecpolicy_id'], + self._ipsec_site_conn['initiator'], + self._ipsec_site_conn['local_ep_group_id'], + self._ipsec_site_conn['local_id'], + self._ipsec_site_conn['mtu'], + self._ipsec_site_conn['name'], + self._ipsec_site_conn['peer_address'], + format_columns.ListColumn(self._ipsec_site_conn['peer_cidrs']), + self._ipsec_site_conn['peer_ep_group_id'], + self._ipsec_site_conn['peer_id'], + self._ipsec_site_conn['psk'], + self._ipsec_site_conn['project_id'], + self._ipsec_site_conn['route_mode'], + self._ipsec_site_conn['admin_state_up'], + self._ipsec_site_conn['status'], + self._ipsec_site_conn['vpnservice_id'], + ) + + +class TestCreateIPsecSiteConn(TestIPsecSiteConn): + def setUp(self): + super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + + self.network_client.create_vpn_ipsec_site_connection.return_value = ( + self._ipsec_site_conn + ) + self.mocked = self.network_client.create_vpn_ipsec_site_connection + self.cmd = ipsec_site_connection.CreateIPsecSiteConnection( + self.app, None + ) + + def test_create_with_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_all_params(self): + arglist = [ + '--project', + self.project.id, + '--peer-address', + '192.168.2.10', + '--peer-id', + '192.168.2.10', + '--psk', + 'abcd', + '--initiator', + 'bi-directional', + '--vpnservice', + 'vpnservice_id', + '--ikepolicy', + 'ikepolicy_id', + '--ipsecpolicy', + 'ipsecpolicy_id', + '--mtu', + '1500', + '--description', + 'my-vpn-connection', + '--local-endpoint-group', + 'local-epg', + '--peer-endpoint-group', + 'peer-epg', + self._ipsec_site_conn.name, + ] + verifylist = [ + ('project', self.project.id), + ('peer_address', '192.168.2.10'), + ('peer_id', '192.168.2.10'), + ('psk', 'abcd'), + ('initiator', 'bi-directional'), + ('vpnservice', 'vpnservice_id'), + ('ikepolicy', 'ikepolicy_id'), + ('ipsecpolicy', 'ipsecpolicy_id'), + ('mtu', '1500'), + ('description', 'my-vpn-connection'), + ('local_endpoint_group', 'local-epg'), + ('peer_endpoint_group', 'peer-epg'), + ('name', self._ipsec_site_conn.name), + ] + + def _mock_endpoint_group(*args, **kwargs): + return _vpn_endpoint_group.VpnEndpointGroup(**{'id': args[0]}) + + def _mock_vpn_service(*args, **kwargs): + return sdk_vpn_service.VpnService(**{'id': args[0]}) + + def _mock_vpn_ike_policy(*args, **kwargs): + return _vpn_ike_policy.VpnIkePolicy(**{'id': args[0]}) + + def _mock_vpn_ipsec_policy(*args, **kwargs): + return sdk_vpn_ipsecp_p.VpnIpsecPolicy(**{'id': args[0]}) + + self.network_client.find_vpn_endpoint_group.side_effect = mock.Mock( + side_effect=_mock_endpoint_group + ) + self.network_client.find_vpn_service.side_effect = mock.Mock( + side_effect=_mock_vpn_service + ) + self.network_client.find_vpn_ike_policy.side_effect = mock.Mock( + side_effect=_mock_vpn_ike_policy + ) + self.network_client.find_vpn_ipsec_policy.side_effect = mock.Mock( + side_effect=_mock_vpn_ipsec_policy + ) + self.network_client.create_vpn_ipsec_site_connection.return_value = ( + self._ipsec_site_conn + ) + self.ordered_data = ( + self._ipsec_site_conn['auth_mode'], + self._ipsec_site_conn['dpd'], + self._ipsec_site_conn['description'], + self._ipsec_site_conn['id'], + self._ipsec_site_conn['ikepolicy_id'], + self._ipsec_site_conn['ipsecpolicy_id'], + self._ipsec_site_conn['initiator'], + self._ipsec_site_conn['local_ep_group_id'], + self._ipsec_site_conn['local_id'], + self._ipsec_site_conn['mtu'], + self._ipsec_site_conn['name'], + self._ipsec_site_conn['peer_address'], + format_columns.ListColumn(self._ipsec_site_conn['peer_cidrs']), + self._ipsec_site_conn['peer_ep_group_id'], + self._ipsec_site_conn['peer_id'], + self._ipsec_site_conn['psk'], + self._ipsec_site_conn['project_id'], + self._ipsec_site_conn['route_mode'], + self._ipsec_site_conn['admin_state_up'], + self._ipsec_site_conn['status'], + self._ipsec_site_conn['vpnservice_id'], + ) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.mocked.assert_called_once_with( + project_id=self.project.id, + peer_address='192.168.2.10', + peer_id='192.168.2.10', + psk='abcd', + initiator='bi-directional', + vpnservice_id='vpnservice_id', + ikepolicy_id='ikepolicy_id', + ipsecpolicy_id='ipsecpolicy_id', + mtu='1500', + description='my-vpn-connection', + local_ep_group_id='local-epg', + peer_ep_group_id='peer-epg', + name=self._ipsec_site_conn.name, + ) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + self.assertCountEqual(self.ordered_data, data) + + +class TestDeleteIPsecSiteConn(TestIPsecSiteConn): + def setUp(self): + super().setUp() + self.cmd = ipsec_site_connection.DeleteIPsecSiteConnection( + self.app, None + ) + + def test_delete_with_one_resource(self): + def _mock_ips_sc(*args, **kwargs): + return sdk_vpn_ipsec_sc.VpnIPSecSiteConnection(**{'id': args[0]}) + + self.network_client.find_vpn_endpoint_group.side_effect = _mock_ips_sc + + arglist = [self._ipsec_site_conn['id']] + verifylist = [('ipsec_site_connection', [self._ipsec_site_conn['id']])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_vpn_ipsec_site_connection.assert_called_once_with( + self._ipsec_site_conn['id'] + ) + self.assertIsNone(result) + + def test_delete_with_multiple_resources(self): + + def _mock_ips_c(*args, **kwargs): + return sdk_vpn_ipsec_sc.VpnIPSecSiteConnection(**{'id': args[0]}) + + self.network_client.find_vpn_ipsec_policy.side_effect = _mock_ips_c + + arglist = ['target1', 'target2'] + verifylist = [('ipsec_site_connection', ['target1', 'target2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertEqual( + 2, + self.network_client.delete_vpn_ipsec_site_connection.call_count, + ) + for idx, reference in enumerate(['target1', 'target2']): + actual = ''.join( + self.network_client.delete_vpn_ipsec_site_connection.call_args_list[ + idx + ][0] + ) + self.assertEqual(reference, actual) + + def test_delete_multiple_with_exception(self): + arglist = ['target'] + verifylist = [('ipsec_site_connection', ['target'])] + + self.network_client.find_vpn_ipsec_site_connection.side_effect = [ + 'target', + exceptions.CommandError, + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestListIPsecSiteConn(TestIPsecSiteConn): + def setUp(self): + super().setUp() + self.cmd = ipsec_site_connection.ListIPsecSiteConnection( + self.app, None + ) + + self.short_header = ( + 'ID', + 'Name', + 'Peer Address', + 'Authentication Algorithm', + 'Status', + ) + + self.short_data = ( + self._ipsec_site_conn['id'], + self._ipsec_site_conn['name'], + self._ipsec_site_conn['peer_address'], + self._ipsec_site_conn['auth_mode'], + self._ipsec_site_conn['status'], + ) + + self.network_client.vpn_ipsec_site_connections.return_value = [ + self._ipsec_site_conn + ] + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + + self.network_client.vpn_ipsec_site_connections.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.vpn_ipsec_site_connections.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + +class TestSetIPsecSiteConn(TestIPsecSiteConn): + def setUp(self): + super().setUp() + + self.network_client.update_vpn_ipsec_site_connection.return_value = ( + self._ipsec_site_conn + ) + + self.cmd = ipsec_site_connection.SetIPsecSiteConnection(self.app, None) + + def test_set_ipsec_site_conn_with_peer_id(self): + arglist = [ + self._ipsec_site_conn['id'], + '--peer-id', + '192.168.3.10', + ] + verifylist = [ + ('ipsec_site_connection', self._ipsec_site_conn['id']), + ('peer_id', '192.168.3.10'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_ipsec_site_connection.assert_called_once_with( + self._ipsec_site_conn['id'], peer_id='192.168.3.10' + ) + self.assertIsNone(result) + + def test_set_name(self): + arglist = [ + self._ipsec_site_conn['id'], + '--name', + 'change', + ] + verifylist = [ + ('ipsec_site_connection', self._ipsec_site_conn['id']), + ('name', 'change'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_ipsec_site_connection.assert_called_once_with( + self._ipsec_site_conn['id'], name='change' + ) + self.assertIsNone(result) + + def test_set_description(self): + arglist = [ + self._ipsec_site_conn['id'], + '--description', + 'change-desc', + ] + verifylist = [ + ('ipsec_site_connection', self._ipsec_site_conn['id']), + ('description', 'change-desc'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_ipsec_site_connection.assert_called_once_with( + self._ipsec_site_conn['id'], description='change-desc' + ) + self.assertIsNone(result) + + +class TestShowIPsecSiteConn(TestIPsecSiteConn): + def setUp(self): + super().setUp() + + self.network_client.find_vpn_ipsec_site_connection.side_effect = None + self.network_client.find_vpn_ipsec_site_connection.return_value = ( + self._ipsec_site_conn + ) + + self.cmd = ipsec_site_connection.ShowIPsecSiteConnection( + self.app, None + ) + + def test_show_filtered_by_id_or_name(self): + arglist = [ + self._ipsec_site_conn['id'], + ] + verifylist = [ + ('ipsec_site_connection', self._ipsec_site_conn['id']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.find_vpn_ipsec_site_connection.assert_called_once_with( + self._ipsec_site_conn['id'], ignore_missing=False + ) + self.assertEqual(self.ordered_headers, headers) + self.assertCountEqual(self.ordered_data, data) diff --git a/openstackclient/tests/unit/network/v2/vpnaas/test_ipsecpolicy.py b/openstackclient/tests/unit/network/v2/vpnaas/test_ipsecpolicy.py new file mode 100644 index 0000000000..50c7ef0d66 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/vpnaas/test_ipsecpolicy.py @@ -0,0 +1,389 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from openstack.identity.v3 import project as _project +from openstack.network.v2 import vpn_ipsec_policy as sdk_vpn_ipsecp_p +from openstack.test import fakes as sdk_fakes +from openstackclient.tests.unit import utils as tests_utils +from osc_lib import exceptions + +from openstackclient.network.v2.vpnaas import ipsecpolicy +from openstackclient.tests.unit.network.v2 import fakes as test_fakes +from openstackclient.tests.unit.network.v2.vpnaas import fakes + + +class TestIPSecPolicy(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + + self._ipsecpolicy = fakes.IPSecPolicy().create() + + def _mock_ipsecpolicy(*args, **kwargs): + self.network_client.find_vpn_ipsec_policy.assert_called_once_with( + self._ipsecpolicy['id'], ignore_missing=False + ) + return {'id': args[0]} + + self.network_client.find_vpn_ipsec_policy.side_effect = mock.Mock( + side_effect=_mock_ipsecpolicy + ) + self.headers = ( + 'ID', + 'Name', + 'Authentication Algorithm', + 'Encapsulation Mode', + 'Transform Protocol', + 'Encryption Algorithm', + 'Perfect Forward Secrecy (PFS)', + 'Description', + 'Project', + 'Lifetime', + ) + self.ordered_headers = ( + 'Authentication Algorithm', + 'Description', + 'Encapsulation Mode', + 'Encryption Algorithm', + 'ID', + 'Lifetime', + 'Name', + 'Perfect Forward Secrecy (PFS)', + 'Project', + 'Transform Protocol', + ) + self.ordered_data = ( + self._ipsecpolicy['auth_algorithm'], + self._ipsecpolicy['description'], + self._ipsecpolicy['encapsulation_mode'], + self._ipsecpolicy['encryption_algorithm'], + self._ipsecpolicy['id'], + self._ipsecpolicy['lifetime'], + self._ipsecpolicy['name'], + self._ipsecpolicy['pfs'], + self._ipsecpolicy['project_id'], + self._ipsecpolicy['transform_protocol'], + ) + self.ordered_columns = ( + 'auth_algorithm', + 'description', + 'encapsulation_mode', + 'encryption_algorithm', + 'id', + 'lifetime', + 'name', + 'pfs', + 'project_id', + 'transform_protocol', + ) + + +class TestCreateIPSecPolicy(TestIPSecPolicy): + def setUp(self): + super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + + self.network_client.create_vpn_ipsec_policy.return_value = ( + self._ipsecpolicy + ) + self.mocked = self.network_client.create_vpn_ipsec_policy + self.cmd = ipsecpolicy.CreateIPsecPolicy(self.app, None) + + def test_create_with_no_options(self): + arglist = [] + verifylist = [] + + self.assertRaises( + tests_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_create_with_all_params(self): + arglist = [ + 'my-name', + '--auth-algorithm', + 'sha1', + '--encapsulation-mode', + 'tunnel', + '--transform-protocol', + 'esp', + '--encryption-algorithm', + 'aes-128', + '--pfs', + 'group5', + '--description', + 'my-desc', + '--project', + self.project.id, + ] + verifylist = [ + ('name', 'my-name'), + ('auth_algorithm', 'sha1'), + ('encapsulation_mode', 'tunnel'), + ('transform_protocol', 'esp'), + ('encryption_algorithm', 'aes-128'), + ('pfs', 'group5'), + ('description', 'my-desc'), + ('project', self.project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + self.network_client.create_vpn_ipsec_policy.assert_called_once_with( + name='my-name', + auth_algorithm='sha1', + encapsulation_mode='tunnel', + transform_protocol='esp', + encryption_algorithm='aes-128', + pfs='group5', + description='my-desc', + project_id=self.project.id, + ) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + self.assertCountEqual(self.ordered_data, data) + + def test_create_with_all_params_name(self): + arglist = [ + 'new_ipsecpolicy', + '--auth-algorithm', + 'sha1', + '--encapsulation-mode', + 'tunnel', + '--transform-protocol', + 'esp', + '--encryption-algorithm', + 'aes-128', + '--pfs', + 'group5', + '--description', + 'my-desc', + '--project', + self.project.id, + ] + verifylist = [ + ('name', 'new_ipsecpolicy'), + ('auth_algorithm', 'sha1'), + ('encapsulation_mode', 'tunnel'), + ('transform_protocol', 'esp'), + ('encryption_algorithm', 'aes-128'), + ('pfs', 'group5'), + ('description', 'my-desc'), + ('project', self.project.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + self.network_client.create_vpn_ipsec_policy.assert_called_once_with( + name='new_ipsecpolicy', + auth_algorithm='sha1', + encapsulation_mode='tunnel', + transform_protocol='esp', + encryption_algorithm='aes-128', + pfs='group5', + description='my-desc', + project_id=self.project.id, + ) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + self.assertCountEqual(self.ordered_data, data) + + +class TestDeleteIPSecPolicy(TestIPSecPolicy): + def setUp(self): + super().setUp() + self.cmd = ipsecpolicy.DeleteIPsecPolicy(self.app, None) + + def test_delete_with_one_resource(self): + def _mock_ipsec_p(*args, **kwargs): + return sdk_vpn_ipsecp_p.VpnIpsecPolicy(**{'id': args[0]}) + + self.network_client.find_vpn_ipsec_policy.side_effect = _mock_ipsec_p + + arglist = [self._ipsecpolicy['id']] + verifylist = [('ipsecpolicy', [self._ipsecpolicy['id']])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_vpn_ipsec_policy.assert_called_once_with( + self._ipsecpolicy['id'] + ) + self.assertIsNone(result) + + def test_delete_with_multiple_resources(self): + + def _mock_ipsec_p(*args, **kwargs): + return sdk_vpn_ipsecp_p.VpnIpsecPolicy(**{'id': args[0]}) + + self.network_client.find_vpn_ipsec_policy.side_effect = _mock_ipsec_p + + arglist = ['target1', 'target2'] + verifylist = [('ipsecpolicy', ['target1', 'target2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertEqual( + 2, self.network_client.delete_vpn_ipsec_policy.call_count + ) + for idx, reference in enumerate(['target1', 'target2']): + actual = ''.join( + self.network_client.delete_vpn_ipsec_policy.call_args_list[ + idx + ][0] + ) + self.assertEqual(reference, actual) + + def test_delete_multiple_with_exception(self): + arglist = ['target'] + verifylist = [('ipsecpolicy', ['target'])] + + self.network_client.find_vpn_ipsec_policy.side_effect = [ + 'target', + exceptions.CommandError, + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + +class TestListIPSecPolicy(TestIPSecPolicy): + def setUp(self): + super().setUp() + self.cmd = ipsecpolicy.ListIPsecPolicy(self.app, None) + + self.short_header = ( + 'ID', + 'Name', + 'Authentication Algorithm', + 'Encapsulation Mode', + 'Transform Protocol', + 'Encryption Algorithm', + ) + + self.short_data = ( + self._ipsecpolicy['id'], + self._ipsecpolicy['name'], + self._ipsecpolicy['auth_algorithm'], + self._ipsecpolicy['encapsulation_mode'], + self._ipsecpolicy['transform_protocol'], + self._ipsecpolicy['encryption_algorithm'], + ) + + self.network_client.vpn_ipsec_policies.return_value = [ + self._ipsecpolicy + ] + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + + self.network_client.vpn_ipsec_policies.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.vpn_ipsec_policies.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + +class TestSetIPSecPolicy(TestIPSecPolicy): + def setUp(self): + super().setUp() + self.network_client.update_vpn_ipsec_policy.return_value = ( + self._ipsecpolicy + ) + self.cmd = ipsecpolicy.SetIPsecPolicy(self.app, None) + + def test_set_auth_algorithm_with_sha256(self): + arglist = [self._ipsecpolicy['id'], '--auth-algorithm', 'sha256'] + verifylist = [ + ('ipsecpolicy', self._ipsecpolicy['id']), + ('auth_algorithm', 'sha256'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_ipsec_policy.assert_called_once_with( + self._ipsecpolicy['id'], auth_algorithm='sha256' + ) + self.assertIsNone(result) + + def test_set_name(self): + arglist = [self._ipsecpolicy['id'], '--name', 'change'] + verifylist = [ + ('ipsecpolicy', self._ipsecpolicy['id']), + ('name', 'change'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_ipsec_policy.assert_called_once_with( + self._ipsecpolicy['id'], name='change' + ) + self.assertIsNone(result) + + def test_set_description(self): + arglist = [self._ipsecpolicy['id'], '--description', 'change-desc'] + verifylist = [ + ('ipsecpolicy', self._ipsecpolicy['id']), + ('description', 'change-desc'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_ipsec_policy.assert_called_once_with( + self._ipsecpolicy['id'], description='change-desc' + ) + self.assertIsNone(result) + + +class TestShowIPSecPolicy(TestIPSecPolicy): + def setUp(self): + super().setUp() + + self.network_client.find_vpn_ipsec_policy.side_effect = None + self.network_client.find_vpn_ipsec_policy.return_value = ( + self._ipsecpolicy + ) + self.cmd = ipsecpolicy.ShowIPsecPolicy(self.app, None) + + def test_show_filtered_by_id_or_name(self): + arglist = [self._ipsecpolicy['id']] + verifylist = [('ipsecpolicy', self._ipsecpolicy['id'])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.find_vpn_ipsec_policy.assert_called_once_with( + self._ipsecpolicy['id'], ignore_missing=False + ) + self.assertEqual(self.ordered_headers, headers) + self.assertCountEqual(self.ordered_data, data) diff --git a/openstackclient/tests/unit/network/v2/vpnaas/test_vpnservice.py b/openstackclient/tests/unit/network/v2/vpnaas/test_vpnservice.py new file mode 100644 index 0000000000..89e7ca5447 --- /dev/null +++ b/openstackclient/tests/unit/network/v2/vpnaas/test_vpnservice.py @@ -0,0 +1,309 @@ +# Copyright 2017 FUJITSU LIMITED +# All Rights Reserved +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock +import uuid + +from openstack.identity.v3 import project as _project +from openstack.network.v2 import vpn_service as sdk_vpn_service +from openstack.test import fakes as sdk_fakes +from osc_lib import exceptions + +from openstackclient.network.v2.vpnaas import vpnservice +from openstackclient.tests.unit.network.v2 import fakes as test_fakes +from openstackclient.tests.unit.network.v2.vpnaas import fakes + + +class TestVPNService(test_fakes.TestNetworkV2): + def setUp(self): + super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + + self._vpnservice = fakes.VPNService().create() + + def _mock_vpnservice(*args, **kwargs): + self.network_client.find_vpn_service.assert_called_once_with( + self._vpnservice['id'], ignore_missing=False + ) + return {'id': args[0]} + + self.fake_router = mock.Mock() + self.fake_subnet = mock.Mock() + self.network_client.find_router.return_value = self.fake_router + self.network_client.find_subnet.return_value = self.fake_subnet + self.fake_router.id = 'router-id-' + uuid.uuid4().hex + self.fake_subnet.id = 'subnet-id-' + uuid.uuid4().hex + + self.network_client.find_vpn_service.side_effect = mock.Mock( + side_effect=_mock_vpnservice + ) + + self.headers = ( + 'ID', + 'Name', + 'Router', + 'Subnet', + 'Flavor', + 'State', + 'Status', + 'Description', + 'Project', + 'Ext v4 IP', + 'Ext v6 IP', + ) + self.ordered_headers = ( + 'Description', + 'Ext v4 IP', + 'Ext v6 IP', + 'Flavor', + 'ID', + 'Name', + 'Project', + 'Router', + 'State', + 'Status', + 'Subnet', + ) + self.ordered_data = ( + self._vpnservice['description'], + self._vpnservice['external_v4_ip'], + self._vpnservice['external_v6_ip'], + self._vpnservice['flavor_id'], + self._vpnservice['id'], + self._vpnservice['name'], + self._vpnservice['project_id'], + self._vpnservice['router_id'], + self._vpnservice['admin_state_up'], + self._vpnservice['status'], + self._vpnservice['subnet_id'], + ) + self.ordered_columns = ( + 'description', + 'external_v4_ip', + 'external_v6_ip', + 'flavor_id', + 'id', + 'name', + 'project_id', + 'router_id', + 'admin_state_up', + 'status', + 'subnet_id', + ) + + +class TestCreateVPNService(TestVPNService): + def setUp(self): + super().setUp() + + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + + self.network_client.create_vpn_service.return_value = self._vpnservice + self.mocked = self.network_client.create_vpn_service + self.cmd = vpnservice.CreateVPNService(self.app, None) + + def test_create_with_all_params(self): + arglist = [ + '--description', + 'my-desc', + '--project', + self.project.id, + '--subnet', + self.fake_subnet.id, + '--router', + self.fake_router.id, + 'my-name', + ] + verifylist = [ + ('description', 'my-desc'), + ('project', self.project.id), + ('subnet', self.fake_subnet.id), + ('router', self.fake_router.id), + ('name', 'my-name'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + self.network_client.create_vpn_service.assert_called_once_with( + description='my-desc', + project_id=self.project.id, + subnet_id=self.fake_subnet.id, + router_id=self.fake_router.id, + name='my-name', + ) + self.assertEqual(self.ordered_headers, tuple(sorted(headers))) + self.assertCountEqual(self.ordered_data, data) + + +class TestDeleteVPNService(TestVPNService): + def setUp(self): + super().setUp() + self.cmd = vpnservice.DeleteVPNService(self.app, None) + + def test_delete_with_one_resource(self): + def _mock_vpn_s(*args, **kwargs): + return sdk_vpn_service.VpnService(**{'id': args[0]}) + + self.network_client.find_vpn_service.side_effect = _mock_vpn_s + + arglist = [self._vpnservice['id']] + verifylist = [('vpnservice', [self._vpnservice['id']])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.delete_vpn_service.assert_called_once_with( + self._vpnservice['id'] + ) + self.assertIsNone(result) + + def test_delete_with_multiple_resources(self): + + def _mock_vpn_s(*args, **kwargs): + return sdk_vpn_service.VpnService(**{'id': args[0]}) + + self.network_client.find_vpn_service.side_effect = _mock_vpn_s + + arglist = ['target1', 'target2'] + verifylist = [('vpnservice', ['target1', 'target2'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.assertEqual(2, self.network_client.delete_vpn_service.call_count) + for idx, reference in enumerate(['target1', 'target2']): + actual = ''.join( + self.network_client.delete_vpn_service.call_args_list[idx][0] + ) + self.assertEqual(reference, actual) + + def test_delete_multiple_with_exception(self): + arglist = ['target'] + verifylist = [('vpnservice', ['target'])] + + self.network_client.find_vpn_ipsec_policy.side_effect = [ + 'target', + exceptions.CommandError, + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + +class TestListVPNService(TestVPNService): + def setUp(self): + super().setUp() + self.cmd = vpnservice.ListVPNService(self.app, None) + + self.short_header = ( + 'ID', + 'Name', + 'Router', + 'Subnet', + 'Flavor', + 'State', + 'Status', + ) + + self.short_data = ( + self._vpnservice['id'], + self._vpnservice['name'], + self._vpnservice['router_id'], + self._vpnservice['subnet_id'], + self._vpnservice['flavor_id'], + self._vpnservice['admin_state_up'], + self._vpnservice['status'], + ) + + self.network_client.vpn_services.return_value = [self._vpnservice] + + def test_list_with_long_option(self): + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, _data = self.cmd.take_action(parsed_args) + + self.network_client.vpn_services.assert_called_once_with() + self.assertEqual(list(self.headers), headers) + + def test_list_with_no_option(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.vpn_services.assert_called_once_with() + self.assertEqual(list(self.short_header), headers) + self.assertEqual([self.short_data], list(data)) + + +class TestSetVPNService(TestVPNService): + def setUp(self): + super().setUp() + self.network_client.update_vpn_service.return_value = self._vpnservice + self.cmd = vpnservice.SetVPNSercice(self.app, None) + + def test_set_name(self): + arglist = [self._vpnservice['id'], '--name', 'change'] + verifylist = [ + ('vpnservice', self._vpnservice['id']), + ('name', 'change'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_service.assert_called_once_with( + self._vpnservice['id'], name='change' + ) + self.assertIsNone(result) + + def test_set_description(self): + arglist = [self._vpnservice['id'], '--description', 'change-desc'] + verifylist = [ + ('vpnservice', self._vpnservice['id']), + ('description', 'change-desc'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.network_client.update_vpn_service.assert_called_once_with( + self._vpnservice['id'], description='change-desc' + ) + self.assertIsNone(result) + + +class TestShowVPNService(TestVPNService): + def setUp(self): + super().setUp() + self.network_client.find_vpn_service.side_effect = None + self.network_client.find_vpn_service.return_value = self._vpnservice + self.cmd = vpnservice.ShowVPNService(self.app, None) + + def test_show_filtered_by_id_or_name(self): + arglist = [self._vpnservice['id']] + verifylist = [('vpnservice', self._vpnservice['id'])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + headers, data = self.cmd.take_action(parsed_args) + + self.network_client.find_vpn_service.assert_called_once_with( + self._vpnservice['id'], ignore_missing=False + ) + self.assertEqual(self.ordered_headers, headers) + self.assertCountEqual(self.ordered_data, data) diff --git a/openstackclient/tests/unit/object/v1/fakes.py b/openstackclient/tests/unit/object/v1/fakes.py index eedccdc0e8..d43e5065f6 100644 --- a/openstackclient/tests/unit/object/v1/fakes.py +++ b/openstackclient/tests/unit/object/v1/fakes.py @@ -11,9 +11,8 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -from keystoneauth1 import session +from unittest import mock from openstackclient.api import object_store_v1 as object_store from openstackclient.tests.unit import utils @@ -80,12 +79,14 @@ object_upload_name = 'test-object-name' -class TestObjectv1(utils.TestCommand): +class FakeClientMixin: def setUp(self): super().setUp() - self.app.client_manager.session = session.Session() - self.app.client_manager.object_store = object_store.APIv1( - session=self.app.client_manager.session, - endpoint=ENDPOINT, + self.app.client_manager.object_store = mock.Mock( + spec=object_store.APIv1 ) + self.object_store_client = self.app.client_manager.object_store + + +class TestObjectV1(FakeClientMixin, utils.TestCommand): ... diff --git a/openstackclient/tests/unit/object/v1/test_container.py b/openstackclient/tests/unit/object/v1/test_container.py index 9143df9c9a..0ba9118bf6 100644 --- a/openstackclient/tests/unit/object/v1/test_container.py +++ b/openstackclient/tests/unit/object/v1/test_container.py @@ -14,48 +14,21 @@ # import copy -from unittest import mock -from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import container from openstackclient.tests.unit.object.v1 import fakes as object_fakes -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" - - -class FakeClient: - def __init__(self, endpoint=None, **kwargs): - self.endpoint = AUTH_URL - self.token = AUTH_TOKEN - - -class TestContainer(object_fakes.TestObjectv1): - columns = ('Name',) - +class TestContainerDelete(object_fakes.TestObjectV1): def setUp(self): super().setUp() - self.app.client_manager.object_store = object_store.APIv1( - session=mock.Mock(), - service_type="object-store", - ) - self.api = self.app.client_manager.object_store - -@mock.patch('openstackclient.api.object_store_v1.APIv1.object_delete') -@mock.patch('openstackclient.api.object_store_v1.APIv1.object_list') -@mock.patch('openstackclient.api.object_store_v1.APIv1.container_delete') -class TestContainerDelete(TestContainer): - def setUp(self): - super().setUp() + self.object_store_client.container_delete.return_value = None # Get the command object to test self.cmd = container.DeleteContainer(self.app, None) - def test_container_delete(self, c_mock, o_list_mock, o_delete_mock): - c_mock.return_value = None - + def test_container_delete(self): arglist = [ object_fakes.container_name, ] @@ -68,16 +41,17 @@ def test_container_delete(self, c_mock, o_list_mock, o_delete_mock): self.assertIsNone(self.cmd.take_action(parsed_args)) kwargs = {} - c_mock.assert_called_with( + self.object_store_client.container_delete.assert_called_with( container=object_fakes.container_name, **kwargs ) - self.assertFalse(o_list_mock.called) - self.assertFalse(o_delete_mock.called) + self.object_store_client.object_list.assert_not_called() + self.object_store_client.object_delete.assert_not_called() - def test_recursive_delete(self, c_mock, o_list_mock, o_delete_mock): - c_mock.return_value = None - o_list_mock.return_value = [object_fakes.OBJECT] - o_delete_mock.return_value = None + def test_recursive_delete(self): + self.object_store_client.object_delete.return_value = None + self.object_store_client.object_list.return_value = [ + object_fakes.OBJECT + ] arglist = [ '--recursive', @@ -91,20 +65,22 @@ def test_recursive_delete(self, c_mock, o_list_mock, o_delete_mock): self.assertIsNone(self.cmd.take_action(parsed_args)) - kwargs = {} - c_mock.assert_called_with( - container=object_fakes.container_name, **kwargs + self.object_store_client.container_delete.assert_called_with( + container=object_fakes.container_name ) - o_list_mock.assert_called_with(container=object_fakes.container_name) - o_delete_mock.assert_called_with( + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name + ) + self.object_store_client.object_delete.assert_called_with( container=object_fakes.container_name, object=object_fakes.OBJECT['name'], ) - def test_r_delete(self, c_mock, o_list_mock, o_delete_mock): - c_mock.return_value = None - o_list_mock.return_value = [object_fakes.OBJECT] - o_delete_mock.return_value = None + def test_r_delete(self): + self.object_store_client.object_delete.return_value = None + self.object_store_client.object_list.return_value = [ + object_fakes.OBJECT + ] arglist = [ '-r', @@ -118,27 +94,29 @@ def test_r_delete(self, c_mock, o_list_mock, o_delete_mock): self.assertIsNone(self.cmd.take_action(parsed_args)) - kwargs = {} - c_mock.assert_called_with( - container=object_fakes.container_name, **kwargs + self.object_store_client.container_delete.assert_called_with( + container=object_fakes.container_name ) - o_list_mock.assert_called_with(container=object_fakes.container_name) - o_delete_mock.assert_called_with( + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name + ) + self.object_store_client.object_delete.assert_called_with( container=object_fakes.container_name, object=object_fakes.OBJECT['name'], ) -@mock.patch('openstackclient.api.object_store_v1.APIv1.container_list') -class TestContainerList(TestContainer): +class TestContainerList(object_fakes.TestObjectV1): + columns = ('Name',) + def setUp(self): super().setUp() # Get the command object to test self.cmd = container.ListContainer(self.app, None) - def test_object_list_containers_no_options(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_no_options(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_3), copy.deepcopy(object_fakes.CONTAINER_2), @@ -153,9 +131,7 @@ def test_object_list_containers_no_options(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = {} - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with() self.assertEqual(self.columns, columns) datalist = ( @@ -165,8 +141,8 @@ def test_object_list_containers_no_options(self, c_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_containers_prefix(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_prefix(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_3), ] @@ -185,11 +161,9 @@ def test_object_list_containers_prefix(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'prefix': 'bit', - } - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with( + prefix='bit', + ) self.assertEqual(self.columns, columns) datalist = ( @@ -198,8 +172,8 @@ def test_object_list_containers_prefix(self, c_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_containers_marker(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_marker(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_3), ] @@ -221,12 +195,10 @@ def test_object_list_containers_marker(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'marker': object_fakes.container_name, - 'end_marker': object_fakes.container_name_3, - } - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with( + marker=object_fakes.container_name, + end_marker=object_fakes.container_name_3, + ) self.assertEqual(self.columns, columns) datalist = ( @@ -235,8 +207,8 @@ def test_object_list_containers_marker(self, c_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_containers_limit(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_limit(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_3), ] @@ -255,11 +227,9 @@ def test_object_list_containers_limit(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'limit': 2, - } - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with( + limit=2, + ) self.assertEqual(self.columns, columns) datalist = ( @@ -268,8 +238,8 @@ def test_object_list_containers_limit(self, c_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_containers_long(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_long(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_3), ] @@ -287,9 +257,7 @@ def test_object_list_containers_long(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = {} - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with() collist = ('Name', 'Bytes', 'Count') self.assertEqual(collist, columns) @@ -307,8 +275,8 @@ def test_object_list_containers_long(self, c_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_containers_all(self, c_mock): - c_mock.return_value = [ + def test_object_list_containers_all(self): + self.object_store_client.container_list.return_value = [ copy.deepcopy(object_fakes.CONTAINER), copy.deepcopy(object_fakes.CONTAINER_2), copy.deepcopy(object_fakes.CONTAINER_3), @@ -327,11 +295,9 @@ def test_object_list_containers_all(self, c_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'full_listing': True, - } - c_mock.assert_called_with(**kwargs) + self.object_store_client.container_list.assert_called_with( + full_listing=True, + ) self.assertEqual(self.columns, columns) datalist = ( @@ -342,16 +308,17 @@ def test_object_list_containers_all(self, c_mock): self.assertEqual(datalist, tuple(data)) -@mock.patch('openstackclient.api.object_store_v1.APIv1.container_show') -class TestContainerShow(TestContainer): +class TestContainerShow(object_fakes.TestObjectV1): def setUp(self): super().setUp() # Get the command object to test self.cmd = container.ShowContainer(self.app, None) - def test_container_show(self, c_mock): - c_mock.return_value = copy.deepcopy(object_fakes.CONTAINER) + def test_container_show(self): + self.object_store_client.container_show.return_value = copy.deepcopy( + object_fakes.CONTAINER + ) arglist = [ object_fakes.container_name, @@ -366,11 +333,8 @@ def test_container_show(self, c_mock): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = {} - # lib.container.show_container(api, url, container) - c_mock.assert_called_with( - container=object_fakes.container_name, **kwargs + self.object_store_client.container_show.assert_called_with( + container=object_fakes.container_name, ) collist = ('bytes', 'count', 'name') diff --git a/openstackclient/tests/unit/object/v1/test_container_all.py b/openstackclient/tests/unit/object/v1/test_container_all.py index 0a795dd860..7de0137884 100644 --- a/openstackclient/tests/unit/object/v1/test_container_all.py +++ b/openstackclient/tests/unit/object/v1/test_container_all.py @@ -13,17 +13,24 @@ import copy +from keystoneauth1 import session from requests_mock.contrib import fixture +from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import container as container_cmds from openstackclient.tests.unit.object.v1 import fakes as object_fakes -class TestContainerAll(object_fakes.TestObjectv1): +class TestContainerAll(object_fakes.TestObjectV1): def setUp(self): super().setUp() + # these tests require a "real" client since we mock requests self.requests_mock = self.useFixture(fixture.Fixture()) + self.app.client_manager.object_store = object_store.APIv1( + session=session.Session(), + endpoint=object_fakes.ENDPOINT, + ) class TestContainerCreate(TestContainerAll): diff --git a/openstackclient/tests/unit/object/v1/test_object.py b/openstackclient/tests/unit/object/v1/test_object.py index f1777f963c..d544cd00ef 100644 --- a/openstackclient/tests/unit/object/v1/test_object.py +++ b/openstackclient/tests/unit/object/v1/test_object.py @@ -14,29 +14,12 @@ # import copy -from unittest import mock -from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import object as obj from openstackclient.tests.unit.object.v1 import fakes as object_fakes -AUTH_TOKEN = "foobar" -AUTH_URL = "http://0.0.0.0" - - -class TestObject(object_fakes.TestObjectv1): - def setUp(self): - super().setUp() - self.app.client_manager.object_store = object_store.APIv1( - session=mock.Mock(), - service_type="object-store", - ) - self.api = self.app.client_manager.object_store - - -@mock.patch('openstackclient.api.object_store_v1.APIv1.object_list') -class TestObjectList(TestObject): +class TestObjectList(object_fakes.TestObjectV1): columns = ('Name',) datalist = ((object_fakes.object_name_2,),) @@ -46,8 +29,8 @@ def setUp(self): # Get the command object to test self.cmd = obj.ListObject(self.app, None) - def test_object_list_objects_no_options(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_no_options(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT), copy.deepcopy(object_fakes.OBJECT_2), ] @@ -65,7 +48,7 @@ def test_object_list_objects_no_options(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - o_mock.assert_called_with( + self.object_store_client.object_list.assert_called_with( container=object_fakes.container_name, ) @@ -76,8 +59,8 @@ def test_object_list_objects_no_options(self, o_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_objects_prefix(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_prefix(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT_2), ] @@ -97,19 +80,16 @@ def test_object_list_objects_prefix(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'prefix': 'floppy', - } - o_mock.assert_called_with( - container=object_fakes.container_name_2, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name_2, + prefix='floppy', ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) - def test_object_list_objects_delimiter(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_delimiter(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT_2), ] @@ -129,19 +109,16 @@ def test_object_list_objects_delimiter(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'delimiter': '=', - } - o_mock.assert_called_with( - container=object_fakes.container_name_2, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name_2, + delimiter='=', ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) - def test_object_list_objects_marker(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_marker(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT_2), ] @@ -161,19 +138,16 @@ def test_object_list_objects_marker(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'marker': object_fakes.object_name_2, - } - o_mock.assert_called_with( - container=object_fakes.container_name_2, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name_2, + marker=object_fakes.object_name_2, ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) - def test_object_list_objects_end_marker(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_end_marker(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT_2), ] @@ -193,19 +167,16 @@ def test_object_list_objects_end_marker(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'end_marker': object_fakes.object_name_2, - } - o_mock.assert_called_with( - container=object_fakes.container_name_2, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name_2, + end_marker=object_fakes.object_name_2, ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) - def test_object_list_objects_limit(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_limit(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT_2), ] @@ -225,19 +196,16 @@ def test_object_list_objects_limit(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'limit': 2, - } - o_mock.assert_called_with( - container=object_fakes.container_name_2, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name_2, + limit=2, ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) - def test_object_list_objects_long(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_long(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT), copy.deepcopy(object_fakes.OBJECT_2), ] @@ -257,10 +225,8 @@ def test_object_list_objects_long(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = {} - o_mock.assert_called_with( - container=object_fakes.container_name, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name, ) collist = ('Name', 'Bytes', 'Hash', 'Content Type', 'Last Modified') @@ -283,8 +249,8 @@ def test_object_list_objects_long(self, o_mock): ) self.assertEqual(datalist, tuple(data)) - def test_object_list_objects_all(self, o_mock): - o_mock.return_value = [ + def test_object_list_objects_all(self): + self.object_store_client.object_list.return_value = [ copy.deepcopy(object_fakes.OBJECT), copy.deepcopy(object_fakes.OBJECT_2), ] @@ -304,12 +270,9 @@ def test_object_list_objects_all(self, o_mock): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'full_listing': True, - } - o_mock.assert_called_with( - container=object_fakes.container_name, **kwargs + self.object_store_client.object_list.assert_called_with( + container=object_fakes.container_name, + full_listing=True, ) self.assertEqual(self.columns, columns) @@ -320,16 +283,17 @@ def test_object_list_objects_all(self, o_mock): self.assertEqual(datalist, tuple(data)) -@mock.patch('openstackclient.api.object_store_v1.APIv1.object_show') -class TestObjectShow(TestObject): +class TestObjectShow(object_fakes.TestObjectV1): def setUp(self): super().setUp() # Get the command object to test self.cmd = obj.ShowObject(self.app, None) - def test_object_show(self, c_mock): - c_mock.return_value = copy.deepcopy(object_fakes.OBJECT) + def test_object_show(self): + self.object_store_client.object_show.return_value = copy.deepcopy( + object_fakes.OBJECT + ) arglist = [ object_fakes.container_name, @@ -346,13 +310,9 @@ def test_object_show(self, c_mock): # data to be shown. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = {} - # lib.container.show_container(api, url, container) - c_mock.assert_called_with( + self.object_store_client.object_show.assert_called_with( container=object_fakes.container_name, object=object_fakes.object_name_1, - **kwargs, ) collist = ('bytes', 'content_type', 'hash', 'last_modified', 'name') diff --git a/openstackclient/tests/unit/object/v1/test_object_all.py b/openstackclient/tests/unit/object/v1/test_object_all.py index 968667b68e..66dfabf48b 100644 --- a/openstackclient/tests/unit/object/v1/test_object_all.py +++ b/openstackclient/tests/unit/object/v1/test_object_all.py @@ -15,18 +15,25 @@ import io from unittest import mock +from keystoneauth1 import session from osc_lib import exceptions from requests_mock.contrib import fixture +from openstackclient.api import object_store_v1 as object_store from openstackclient.object.v1 import object as object_cmds from openstackclient.tests.unit.object.v1 import fakes as object_fakes -class TestObjectAll(object_fakes.TestObjectv1): +class TestObjectAll(object_fakes.TestObjectV1): def setUp(self): super().setUp() + # these tests require a "real" client since we mock requests self.requests_mock = self.useFixture(fixture.Fixture()) + self.app.client_manager.object_store = object_store.APIv1( + session=session.Session(), + endpoint=object_fakes.ENDPOINT, + ) class TestObjectCreate(TestObjectAll): diff --git a/openstackclient/tests/unit/share/__init__.py b/openstackclient/tests/unit/share/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/tests/unit/share/v2/__init__.py b/openstackclient/tests/unit/share/v2/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/openstackclient/tests/unit/share/v2/fakes.py b/openstackclient/tests/unit/share/v2/fakes.py new file mode 100644 index 0000000000..c6a46c820c --- /dev/null +++ b/openstackclient/tests/unit/share/v2/fakes.py @@ -0,0 +1,1617 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +import copy +import datetime +import random +import re +from unittest import mock +import uuid + +from keystoneauth1 import discover +from manilaclient import api_versions +from openstack.shared_file_system.v2 import _proxy +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from osc_lib.cli import format_columns + +from openstackclient.tests.unit import fakes +from openstackclient.tests.unit import utils + + +class FakeShareClient: + def __init__(self, **kwargs): + super().__init__() + self.auth_token = kwargs['token'] + self.management_url = kwargs['endpoint'] + self.api_version = api_versions.APIVersion('2.0') + + self.shares = mock.Mock() + self.transfers = mock.Mock() + self.share_access_rules = mock.Mock() + self.share_groups = mock.Mock() + self.share_types = mock.Mock() + self.share_type_access = mock.Mock() + self.quotas = mock.Mock() + self.quota_classes = mock.Mock() + self.share_backups = mock.Mock() + self.share_snapshots = mock.Mock() + self.share_group_snapshots = mock.Mock() + self.share_snapshot_export_locations = mock.Mock() + self.share_snapshot_instances = mock.Mock() + self.share_replicas = mock.Mock() + self.share_replica_export_locations = mock.Mock() + self.share_networks = mock.Mock() + self.share_network_subnets = mock.Mock() + self.security_services = mock.Mock() + self.shares.resource_class = fakes.FakeResource(None, {}) + self.share_instance_export_locations = mock.Mock() + self.share_export_locations = mock.Mock() + self.share_snapshot_instance_export_locations = mock.Mock() + self.share_export_locations.resource_class = fakes.FakeResource( + None, {} + ) + self.messages = mock.Mock() + self.availability_zones = mock.Mock() + self.services = mock.Mock() + self.share_instances = mock.Mock() + self.pools = mock.Mock() + self.limits = mock.Mock() + self.share_group_types = mock.Mock() + self.share_group_type_access = mock.Mock() + self.share_servers = mock.Mock() + self.resource_locks = mock.Mock() + + +class FakeClientMixin: + def setUp(self): + super().setUp() + + self.app.client_manager.share = FakeShareClient( + endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN + ) + self.share_client = self.app.client_manager.share + + # TODO(stephenfin): Rename to 'share_client' once all commands are + # migrated to SDK + self.app.client_manager.sdk_connection.share = mock.Mock( + spec=_proxy.Proxy, + ) + self.share_sdk_client = self.app.client_manager.sdk_connection.share + self.set_share_api_version() # default to the lowest + + def set_share_api_version(self, version: str = '2.0'): + """Set a fake shared file system API version. + + :param version: The fake microversion to "support". This should be a + string of format '2.xx'. + :returns: None + """ + assert re.match(r'2.\d+', version) + + self.share_client.api_version = api_versions.APIVersion(version) + + self.share_sdk_client.default_microversion = version + self.share_sdk_client.get_endpoint_data.return_value = ( + discover.EndpointData( + min_microversion='2.0', # manila has not bumped this yet + max_microversion=version, + ) + ) + + +class TestShare( + identity_fakes.FakeClientMixin, + FakeClientMixin, + utils.TestCommand, +): + def setUp(self): + super().setUp() + + +class FakeShare: + """Fake one or more shares.""" + + @staticmethod + def create_one_share(attrs=None, methods=None): + """Create a fake share. + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with flavor_id, image_id, and so on + """ + + attrs = attrs or {} + methods = methods or {} + + # set default attributes. + share_info = { + "status": None, + "share_server_id": None, + "project_id": 'project-id-' + uuid.uuid4().hex, + "name": 'share-name-' + uuid.uuid4().hex, + "share_type": 'share-type-' + uuid.uuid4().hex, + "share_type_name": "default", + "availability_zone": None, + "created_at": 'time-' + uuid.uuid4().hex, + "share_network_id": None, + "share_group_id": None, + "share_proto": "NFS", + "host": None, + "access_rules_status": "active", + "has_replicas": False, + "replication_type": None, + "task_state": None, + "snapshot_support": True, + "snapshot_id": None, + "is_public": True, + "metadata": {}, + "id": 'share-id-' + uuid.uuid4().hex, + "size": random.randint(1, 20), + "description": 'share-description-' + uuid.uuid4().hex, + "user_id": 'share-user-id-' + uuid.uuid4().hex, + "create_share_from_snapshot_support": False, + "mount_snapshot_support": False, + "revert_to_snapshot_support": False, + "source_share_group_snapshot_member_id": None, + "scheduler_hints": {}, + "mount_point_name": None, + "encryption_key_ref": None, + } + + # Overwrite default attributes. + share_info.update(attrs) + + share = fakes.FakeResource( + info=copy.deepcopy(share_info), methods=methods, loaded=True + ) + return share + + @staticmethod + def create_shares(attrs=None, count=2): + """Create multiple fake shares. + + :param Dictionary attrs: + A dictionary with all share attributes + :param Integer count: + The number of shares to be faked + :return: + A list of FakeResource objects + """ + shares = [] + for n in range(0, count): + shares.append(FakeShare.create_one_share(attrs)) + + return shares + + @staticmethod + def get_shares(shares=None, count=2): + """Get an iterable MagicMock object with a list of faked shares. + + If a shares list is provided, then initialize the Mock object with the + list. Otherwise create one. + :param List shares: + A list of FakeResource objects faking shares + :param Integer count: + The number of shares to be faked + :return + An iterable Mock object with side_effect set to a list of faked + shares + """ + if shares is None: + shares = FakeShare.create_shares(count) + + return mock.Mock(side_effect=shares) + + @staticmethod + def get_share_columns(share=None): + """Get the shares columns from a faked shares object. + + :param shares: + A FakeResource objects faking shares + :return + A tuple which may include the following keys: + ('id', 'name', 'description', 'status', 'size', 'share_type', + 'metadata', 'snapshot', 'availability_zone') + """ + if share is not None: + return tuple(k for k in sorted(share.keys())) + return tuple([]) + + @staticmethod + def get_share_data(share=None): + """Get the shares data from a faked shares object. + + :param shares: + A FakeResource objects faking shares + :return + A tuple which may include the following values: + ('ce26708d', 'fake name', 'fake description', 'available', + 20, 'fake share type', "Manila='zorilla', Zorilla='manila', + Zorilla='zorilla'", 1, 'nova') + """ + data_list = [] + if share is not None: + for x in sorted(share.keys()): + if x == 'tags': + # The 'tags' should be format_list + data_list.append( + format_columns.ListColumn(share.info.get(x)) + ) + else: + data_list.append(share.info.get(x)) + return tuple(data_list) + + +class FakeShareType: + """Fake one or more share types""" + + @staticmethod + def create_one_sharetype(attrs=None, methods=None): + """Create a fake share type + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + + share_type_info = { + "required_extra_specs": {"driver_handles_share_servers": True}, + "share_type_access:is_public": True, + "extra_specs": { + "replication_type": "readable", + "driver_handles_share_servers": True, + "mount_snapshot_support": False, + "revert_to_snapshot_support": False, + "create_share_from_snapshot_support": True, + "snapshot_support": True, + }, + "id": 'share-type-id-' + uuid.uuid4().hex, + "name": 'share-type-name-' + uuid.uuid4().hex, + "is_default": False, + "description": 'share-type-description-' + uuid.uuid4().hex, + } + + share_type_info.update(attrs) + share_type = fakes.FakeResource( + info=copy.deepcopy(share_type_info), methods=methods, loaded=True + ) + return share_type + + @staticmethod + def create_share_types(attrs=None, count=2): + """Create multiple fake share types. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share types to be faked + :return: + A list of FakeResource objects + """ + + share_types = [] + for n in range(0, count): + share_types.append(FakeShareType.create_one_sharetype(attrs)) + + return share_types + + @staticmethod + def get_share_types(share_types=None, count=2): + """Get an iterable MagicMock object with a list of faked types. + + If types list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List types: + A list of FakeResource objects faking types + :param Integer count: + The number of types to be faked + :return + An iterable Mock object with side_effect set to a list of faked + types + """ + + if share_types is None: + share_types = FakeShareType.create_share_types(count) + + return mock.Mock(side_effect=share_types) + + +class FakeShareExportLocation: + """Fake one or more export locations""" + + @staticmethod + def create_one_export_location(attrs=None): + """Create a fake share export location + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + + share_export_location_info = { + "created_at": 'time-' + uuid.uuid4().hex, + "fake_path": "/foo/el/path", + "fake_share_instance_id": 'share-instance-id' + uuid.uuid4().hex, + "fake_uuid": "foo_el_uuid", + "id": "id-" + uuid.uuid4().hex, + "is_admin_only": False, + "preferred": False, + "properties": {}, + "updated_at": 'time-' + uuid.uuid4().hex, + } + + share_export_location_info.update(attrs) + share_export_location = fakes.FakeResource( + info=copy.deepcopy(share_export_location_info), loaded=True + ) + return share_export_location + + @staticmethod + def create_share_export_locations(attrs=None, count=2): + """Create multiple fake export locations. + + :param Dictionary attrs: + A dictionary with all attributes + + :param Integer count: + The number of share export locations to be faked + + :return: + A list of FakeResource objects + """ + + share_export_locations = [] + for n in range(0, count): + share_export_locations.append( + FakeShareExportLocation.create_one_export_location(attrs) + ) + return share_export_locations + + +class FakeShareAccessRule: + """Fake one or more share access rules""" + + @staticmethod + def create_one_access_rule(attrs=None): + """Create a fake share access rule + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + share_access_rule = { + 'id': 'access_rule-id-' + uuid.uuid4().hex, + 'share_id': 'share-id-' + uuid.uuid4().hex, + 'access_level': 'rw', + 'access_to': 'demo', + 'access_type': 'user', + 'state': 'active', + 'access_key': None, + 'created_at': datetime.datetime.now().isoformat(), + 'updated_at': None, + 'properties': {}, + } + + share_access_rule.update(attrs) + share_access_rule = fakes.FakeResource( + info=copy.deepcopy(share_access_rule), loaded=True + ) + return share_access_rule + + +class FakeQuotaSet: + """Fake quota set""" + + @staticmethod + def create_fake_quotas(attrs=None): + """Create a fake quota set + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + + quotas_info = { + 'gigabytes': 1000, + 'id': 'tenant-id-c96a43119a40ec7d01794cb8', + 'share_group_snapshots': 50, + 'share_groups': 50, + 'share_networks': 10, + 'shares': 50, + 'shapshot_gigabytes': 1000, + 'snapshots': 50, + 'per_share_gigabytes': -1, + 'encryption_keys': 100, + } + + quotas_info.update(attrs) + quotas = fakes.FakeResource( + info=copy.deepcopy(quotas_info), loaded=True + ) + return quotas + + +class FakeShareSnapshotIntances: + """Fake a share snapshot instance""" + + @staticmethod + def create_one_snapshot_instance(attrs=None, methods=None): + """Create a fake share snapshot instance + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + share_snapshot_instance = { + 'id': 'snapshot-instance-id-' + uuid.uuid4().hex, + 'snapshot_id': 'snapshot-id-' + uuid.uuid4().hex, + 'status': None, + 'created_at': datetime.datetime.now().isoformat(), + 'updated_at': datetime.datetime.now().isoformat(), + 'share_id': 'share-id-' + uuid.uuid4().hex, + 'share_instance_id': 'share-instance-id-' + uuid.uuid4().hex, + 'progress': None, + 'provider_location': None, + } + + share_snapshot_instance.update(attrs) + share_snapshot_instance = fakes.FakeResource( + info=copy.deepcopy(share_snapshot_instance), + methods=methods, + loaded=True, + ) + return share_snapshot_instance + + @staticmethod + def create_share_snapshot_instances(attrs=None, count=2): + """Create multiple fake snapshot instances. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share snapshot instances to be faked + :return: + A list of FakeResource objects + """ + + share_snapshot_instances = [] + for n in range(0, count): + share_snapshot_instances.append( + FakeShareSnapshot.create_one_snapshot(attrs) + ) + + return share_snapshot_instances + + +class FakeShareSnapshotInstancesExportLocations: + """Fake a share snapshot instance Export Locations""" + + @staticmethod + def create_one_snapshot_instance(attrs=None, methods=None): + """Create a fake share snapshot instance export locations + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + share_snapshot_instance_export_location = { + 'id': 'snapshot-instance-export-location-id-' + uuid.uuid4().hex, + 'is_admin_only': False, + 'path': '0.0.0.0/:fake-share-instance-export-location-id', + } + + share_snapshot_instance_export_location.update(attrs) + share_snapshot_instance_export_location = fakes.FakeResource( + info=copy.deepcopy(share_snapshot_instance_export_location), + methods=methods, + loaded=True, + ) + return share_snapshot_instance_export_location + + @staticmethod + def create_share_snapshot_instances(attrs=None, count=2): + """Create multiple fake snapshot instances. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share snapshot instance locations to be faked + :return: + A list of FakeResource objects + """ + + share_snapshot_instances = [] + for n in range(0, count): + share_snapshot_instances.append( + FakeShareSnapshot.create_one_snapshot(attrs) + ) + + return share_snapshot_instances + + +class FakeShareSnapshot: + """Fake a share snapshot""" + + @staticmethod + def create_one_snapshot(attrs=None, methods=None): + """Create a fake share snapshot + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + + share_snapshot = { + 'created_at': datetime.datetime.now().isoformat(), + 'description': 'description-' + uuid.uuid4().hex, + 'id': 'snapshot-id-' + uuid.uuid4().hex, + 'name': 'name-' + uuid.uuid4().hex, + 'project_id': 'project-id-' + uuid.uuid4().hex, + 'provider_location': None, + 'share_id': 'share-id-' + uuid.uuid4().hex, + 'share_proto': 'NFS', + 'share_size': 1, + 'size': 1, + 'status': None, + 'user_id': 'user-id-' + uuid.uuid4().hex, + } + + share_snapshot.update(attrs) + share_snapshot = fakes.FakeResource( + info=copy.deepcopy(share_snapshot), methods=methods, loaded=True + ) + return share_snapshot + + @staticmethod + def create_share_snapshots(attrs=None, count=2): + """Create multiple fake snapshots. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share types to be faked + :return: + A list of FakeResource objects + """ + + share_snapshots = [] + for n in range(0, count): + share_snapshots.append( + FakeShareSnapshot.create_one_snapshot(attrs) + ) + + return share_snapshots + + +class FakeShareTransfer: + """Fake a share transfer""" + + @staticmethod + def create_one_transfer(attrs=None, methods=None): + """Create a fake share transfer + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + now_time = datetime.datetime.now() + delta_time = now_time + datetime.timedelta(minutes=5) + + share_transfer = { + 'accepted': 'False', + 'auth_key': 'auth-key-' + uuid.uuid4().hex, + 'created_at': now_time.isoformat(), + 'destination_project_id': None, + 'expires_at': delta_time.isoformat(), + 'id': 'transfer-id-' + uuid.uuid4().hex, + 'name': 'name-' + uuid.uuid4().hex, + 'resource_id': 'resource-id-' + uuid.uuid4().hex, + 'resource_type': 'share', + 'source_project_id': 'source-project-id-' + uuid.uuid4().hex, + } + + share_transfer.update(attrs) + share_transfer = fakes.FakeResource( + info=copy.deepcopy(share_transfer), methods=methods, loaded=True + ) + return share_transfer + + @staticmethod + def create_share_transfers(attrs=None, count=2): + """Create multiple fake transfers. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share transfers to be faked + :return: + A list of FakeResource objects + """ + + share_transfers = [] + for n in range(0, count): + share_transfers.append( + FakeShareSnapshot.create_one_snapshot(attrs) + ) + + return share_transfers + + +class FakeSnapshotAccessRule: + """Fake one or more snapshot access rules""" + + @staticmethod + def create_one_access_rule(attrs={}): + """Create a fake snapshot access rule + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + snapshot_access_rule = { + 'access_to': 'demo', + 'access_type': 'user', + 'id': 'access_rule-id-' + uuid.uuid4().hex, + 'state': 'queued_to_apply', + } + + snapshot_access_rule.update(attrs) + snapshot_access_rule = fakes.FakeResource( + info=copy.deepcopy(snapshot_access_rule), loaded=True + ) + return snapshot_access_rule + + @staticmethod + def create_access_rules(attrs={}, count=2): + """Create multiple fake snapshots. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share types to be faked + :return: + A list of FakeResource objects + """ + + access_rules = [] + for n in range(0, count): + access_rules.append( + FakeSnapshotAccessRule.create_one_access_rule(attrs) + ) + + return access_rules + + +class FakeSnapshotExportLocation: + """Fake one or more export locations""" + + @staticmethod + def create_one_export_location(attrs=None): + """Create a fake snapshot export location + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + + snapshot_export_location_info = { + "created_at": 'time-' + uuid.uuid4().hex, + "id": "id-" + uuid.uuid4().hex, + "is_admin_only": False, + "links": [], + "path": "/path/to/fake/snapshot/snapshot", + "share_snapshot_instance_id": 'instance-id' + uuid.uuid4().hex, + "updated_at": 'time-' + uuid.uuid4().hex, + } + + snapshot_export_location_info.update(attrs) + snapshot_export_location = fakes.FakeResource( + info=copy.deepcopy(snapshot_export_location_info), loaded=True + ) + return snapshot_export_location + + @staticmethod + def create_export_locations(attrs={}, count=2): + """Create multiple fake export locations. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share types to be faked + :return: + A list of FakeResource objects + """ + + export_locations = [] + for n in range(0, count): + export_locations.append( + FakeSnapshotExportLocation.create_one_export_location(attrs) + ) + + return export_locations + + +class FakeMessage: + """Fake message""" + + @staticmethod + def create_one_message(attrs=None): + """Create a fake message + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + + message = { + 'id': 'message-id-' + uuid.uuid4().hex, + 'action_id': '001', + 'detail_id': '002', + 'user_message': 'user message', + 'message_level': 'ERROR', + 'resource_type': 'SHARE', + 'resource_id': 'resource-id-' + uuid.uuid4().hex, + 'created_at': datetime.datetime.now().isoformat(), + 'expires_at': ( + datetime.datetime.now() + datetime.timedelta(days=30) + ).isoformat(), + 'request_id': 'req-' + uuid.uuid4().hex, + } + + message.update(attrs) + message = fakes.FakeResource(info=copy.deepcopy(message), loaded=True) + return message + + @staticmethod + def create_messages(attrs={}, count=2): + """Create multiple fake messages. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share types to be faked + :return: + A list of FakeResource objects + """ + + messages = [] + for n in range(0, count): + messages.append(FakeMessage.create_one_message(attrs)) + + return messages + + +class FakeShareReplica: + """Fake a share replica""" + + @staticmethod + def create_one_replica(attrs=None, methods=None): + """Create a fake share replica + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + + share_replica = { + 'availability_zone': None, + 'cast_rules_to_readonly': True, + 'created_at': datetime.datetime.now().isoformat(), + 'host': None, + 'id': 'replica-id-' + uuid.uuid4().hex, + 'replica_state': None, + 'share_id': 'share-id-' + uuid.uuid4().hex, + 'share_network_id': None, + 'share_server_id': None, + 'status': None, + 'updated_at': None, + } + + share_replica.update(attrs) + share_replica = fakes.FakeResource( + info=copy.deepcopy(share_replica), methods=methods, loaded=True + ) + return share_replica + + @staticmethod + def create_share_replicas(attrs=None, count=2): + """Create multiple fake replicas. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share types to be faked + :return: + A list of FakeResource objects + """ + + share_replicas = [] + for n in range(0, count): + share_replicas.append(FakeShareReplica.create_one_replica(attrs)) + return share_replicas + + +class FakeShareService: + """Fake one or more share service""" + + @staticmethod + def create_fake_service(attrs=None): + """Create a fake share service + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + + share_service_info = { + "binary": "manila-share", + "host": "fake_host@fake_backend", + "id": uuid.uuid4().hex, + "status": "enabled", + "state": "up", + "updated_at": 'time-' + uuid.uuid4().hex, + "zone": "fake_zone", + } + + share_service_info.update(attrs) + share_service = fakes.FakeResource( + info=copy.deepcopy(share_service_info), loaded=True + ) + return share_service + + @staticmethod + def create_fake_services(attrs=None, count=2): + """Create multiple fake services. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share services to be faked + :return: + A list of FakeResource objects + """ + + services = [] + for n in range(count): + services.append(FakeShareService.create_fake_service(attrs)) + return services + + +class FakeShareSecurityService: + """Fake one or more share security service""" + + @staticmethod + def create_fake_security_service(attrs=None, methods=None): + """Create a fake share security service + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + + share_security_service_info = { + "created_at": datetime.datetime.now().isoformat(), + "description": 'description', + "dns_ip": '0.0.0.0', + "domain": 'fake.domain', + "id": uuid.uuid4().hex, + "name": 'name-' + uuid.uuid4().hex, + "ou": 'fake_OU', + "password": 'password', + "project_id": uuid.uuid4().hex, + "server": 'fake_hostname', + "default_ad_site": 'fake_default_ad_site', + "status": 'new', + "type": 'ldap', + "updated_at": datetime.datetime.now().isoformat(), + "user": 'fake_user', + } + + share_security_service_info.update(attrs) + share_security_service = fakes.FakeResource( + info=copy.deepcopy(share_security_service_info), + methods=methods, + loaded=True, + ) + return share_security_service + + @staticmethod + def create_fake_security_services(attrs=None, count=2): + """Create multiple fake security services. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share security services to be faked + :return: + A list of FakeResource objects + """ + + security_services = [] + for n in range(count): + security_services.append( + FakeShareSecurityService.create_fake_security_service(attrs) + ) + return security_services + + +class FakeSharePools: + """Fake one or more share pool""" + + @staticmethod + def create_one_share_pool(attrs=None): + """Create a fake share pool + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object + """ + + attrs = attrs or {} + + share_pool = { + "name": 'fake_pool@gamma#fake_pool', + "host": 'fake_host_' + uuid.uuid4().hex, + "backend": 'fake_backend_' + uuid.uuid4().hex, + "pool": 'fake_pool_' + uuid.uuid4().hex, + "capabilities": {'fake_capability': uuid.uuid4().hex}, + } + + share_pool.update(attrs) + share_pool = fakes.FakeResource( + info=copy.deepcopy(share_pool), loaded=True + ) + return share_pool + + @staticmethod + def create_share_pools(attrs=None, count=2): + """Create multiple fake share pools. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share pools to be faked + :return: + A list of FakeResource objects + """ + + share_pools = [] + for n in range(count): + share_pools.append(FakeSharePools.create_one_share_pool(attrs)) + return share_pools + + +class FakeShareInstance: + """Fake a share instance""" + + @staticmethod + def create_one_share_instance(attrs=None, methods=None): + """Create a fake share instance + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + attrs = attrs or {} + methods = methods or {} + + share_instance = { + 'status': None, + 'progress': None, + 'share_id': 'share-id-' + uuid.uuid4().hex, + 'availability_zone': None, + 'replica_state': None, + 'created_at': datetime.datetime.now().isoformat(), + 'cast_rules_to_readonly': False, + 'share_network_id': 'sn-id-' + uuid.uuid4().hex, + 'share_server_id': 'ss-id-' + uuid.uuid4().hex, + 'host': None, + 'access_rules_status': None, + 'id': 'instance-id-' + uuid.uuid4().hex, + } + + share_instance.update(attrs) + share_instance = fakes.FakeResource( + info=copy.deepcopy(share_instance), methods=methods, loaded=True + ) + return share_instance + + @staticmethod + def create_share_instances(attrs=None, count=2): + """Create multiple fake instances. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share instances to be faked + :return: + A list of FakeResource objects + """ + share_instances = [] + for n in range(count): + share_instances.append( + FakeShareInstance.create_one_share_instance(attrs) + ) + return share_instances + + +class FakeShareLimits: + """Fake one or more share limits""" + + @staticmethod + def create_one_share_limit(attrs=None): + """Create a fake share limit dict + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeLimitsResource object, with share limits. + """ + + attrs = attrs or {} + + share_limits = { + 'absolute_limit': { + "totalShareNetworksUsed": 4, + }, + 'rate_limit': { + "regex": "^/shares", + "uri": "/shares", + "verb": "GET", + "next-available": "2021-09-01T00:00:00Z", + "unit": "MINUTE", + "value": "3", + "remaining": "1", + }, + } + + share_limits.update(attrs) + share_limits = fakes.FakeLimitsResource( + info=copy.deepcopy(share_limits), loaded=True + ) + return share_limits + + +class FakeShareNetwork: + """Fake a share network""" + + @staticmethod + def create_one_share_network(attrs=None, methods=None): + """Create a fake share network + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + + share_network = { + 'id': str(uuid.uuid4()), + 'project_id': uuid.uuid4().hex, + 'created_at': datetime.datetime.now().isoformat(), + 'description': 'description-' + uuid.uuid4().hex, + 'name': 'name-' + uuid.uuid4().hex, + "status": "active", + "security_service_update_support": True, + 'share_network_subnets': [ + { + 'id': str(uuid.uuid4()), + "availability_zone": None, + "created_at": datetime.datetime.now().isoformat(), + "updated_at": datetime.datetime.now().isoformat(), + "segmentation_id": 1010, + "neutron_net_id": str(uuid.uuid4()), + "neutron_subnet_id": str(uuid.uuid4()), + "ip_version": 4, + "cidr": "10.0.0.0/24", + "network_type": "vlan", + "mtu": "1500", + "gateway": "10.0.0.1", + }, + ], + } + + share_network.update(attrs) + share_network = fakes.FakeResource( + info=copy.deepcopy(share_network), methods=methods, loaded=True + ) + return share_network + + @staticmethod + def create_share_networks(attrs=None, count=2): + """Create multiple fake share networks. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share networks to be faked + + :return: + A list of FakeResource objects + """ + + share_networks = [] + for n in range(count): + share_networks.append( + FakeShareNetwork.create_one_share_network(attrs) + ) + + return share_networks + + +class FakeShareNetworkSubnet: + """Fake a share network subnet""" + + @staticmethod + def create_one_share_subnet(attrs=None): + """Create a fake share network subnet + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + + share_network_subnet = { + "availability_zone": None, + "cidr": "10.0.0.0/24", + "created_at": datetime.datetime.now().isoformat(), + "gateway": "10.0.0.1", + 'id': str(uuid.uuid4()), + "ip_version": 4, + "mtu": "1500", + "network_type": "vlan", + "neutron_net_id": str(uuid.uuid4()), + "neutron_subnet_id": str(uuid.uuid4()), + "segmentation_id": 1010, + "share_network_id": str(uuid.uuid4()), + "share_network_name": str(uuid.uuid4()), + "updated_at": datetime.datetime.now().isoformat(), + "properties": {}, + } + + share_network_subnet.update(attrs) + share_network_subnet = fakes.FakeResource( + info=copy.deepcopy(share_network_subnet), loaded=True + ) + return share_network_subnet + + @staticmethod + def create_share_network_subnets(attrs=None, count=2): + """Create multiple fake share network subnets. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share network subnets to be faked + :return: + A list of FakeResource objects + """ + + share_network_subnets = [] + for n in range(count): + share_network_subnets.append( + FakeShareNetworkSubnet.create_one_share_subnet(attrs) + ) + + return share_network_subnets + + +class FakeShareGroup: + """Fake a share group""" + + @staticmethod + def create_one_share_group(attrs=None, methods=None): + """Create a fake share group + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + + share_group = { + "id": 'share-group-id-' + uuid.uuid4().hex, + 'name': None, + 'created_at': datetime.datetime.now().isoformat(), + 'status': 'available', + 'description': None, + 'availability_zone': None, + "project_id": 'project-id-' + uuid.uuid4().hex, + 'host': None, + 'share_group_type_id': 'share-group-type-id-' + uuid.uuid4().hex, + 'source_share_group_snapshot_id': None, + 'share_network_id': None, + 'share_server_id': None, + 'share_types': ['share-types-id-' + uuid.uuid4().hex], + 'consistent_snapshot_support': None, + } + + share_group.update(attrs) + share_group = fakes.FakeResource( + info=copy.deepcopy(share_group), methods=methods, loaded=True + ) + return share_group + + @staticmethod + def create_share_groups(attrs=None, count=2): + """Create multiple fake groups. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share groups to be faked + :return: + A list of FakeResource objects + """ + + share_groups = [] + for n in range(0, count): + share_groups.append(FakeShareGroup.create_one_share_group(attrs)) + return share_groups + + +class FakeShareGroupType: + """Fake one or more share group types""" + + @staticmethod + def create_one_share_group_type(attrs=None, methods=None): + """Create a fake share group type + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + + share_group_type_info = { + "is_public": True, + "group_specs": {"snapshot_support": True}, + "share_types": ['share-types-id-' + uuid.uuid4().hex], + "id": 'share-group-type-id-' + uuid.uuid4().hex, + "name": 'share-group-type-name-' + uuid.uuid4().hex, + "is_default": False, + } + + share_group_type_info.update(attrs) + share_group_type = fakes.FakeResource( + info=copy.deepcopy(share_group_type_info), + methods=methods, + loaded=True, + ) + return share_group_type + + @staticmethod + def create_share_group_types(attrs=None, count=2): + """Create multiple fake share group types. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share group types to be faked + :return: + A list of FakeResource objects + """ + + share_group_types = [] + for n in range(0, count): + share_group_types.append( + FakeShareGroupType.create_one_share_group_type(attrs) + ) + + return share_group_types + + @staticmethod + def get_share_group_types(share_group_types=None, count=2): + """Get an iterable MagicMock object with a list of faked group types. + + If types list is provided, then initialize the Mock object with the + list. Otherwise create one. + + :param List types: + A list of FakeResource objects faking types + :param Integer count: + The number of group types to be faked + :return + An iterable Mock object with side_effect set to a list of faked + group types + """ + + if share_group_types is None: + share_group_types = FakeShareGroupType.share_group_types(count) + + return mock.Mock(side_effect=share_group_types) + + +class FakeShareGroupSnapshot: + """Fake a share group snapshot""" + + @staticmethod + def create_one_share_group_snapshot(attrs=None, methods=None): + """Create a fake share group snapshot + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + + share_group_snapshot = { + 'status': 'available', + 'share_group_id': 'share-group-id-' + uuid.uuid4().hex, + 'name': None, + 'created_at': datetime.datetime.now().isoformat(), + "project_id": 'project-id-' + uuid.uuid4().hex, + 'id': 'share-group-snapshot-id-' + uuid.uuid4().hex, + 'description': None, + } + + share_group_snapshot.update(attrs) + share_group_snapshot = fakes.FakeResource( + info=copy.deepcopy(share_group_snapshot), + methods=methods, + loaded=True, + ) + return share_group_snapshot + + @staticmethod + def create_share_group_snapshots(attrs=None, count=2): + """Create multiple fake share group snapshot. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share group snapshot to be faked + :return: + A list of FakeResource objects + """ + + share_group_snapshots = [] + for n in range(0, count): + share_group_snapshots.append( + FakeShareGroupSnapshot.create_one_share_group_snapshot(attrs) + ) + return share_group_snapshots + + +class FakeShareServer: + """Fake a share server""" + + @staticmethod + def create_one_server(attrs=None, methods=None): + """Create a fake share server + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + + share_server = { + 'id': str(uuid.uuid4()), + 'project_id': uuid.uuid4().hex, + "updated_at": datetime.datetime.now().isoformat(), + 'status': None, + 'host': None, + 'check_only': False, + 'share_network_name': None, + 'share_network_id': str(uuid.uuid4()), + 'share_network_subnet_id': str(uuid.uuid4()), + 'source_share_server_id': str(uuid.uuid4()), + 'created_at': datetime.datetime.now().isoformat(), + 'is_auto_deletable': False, + 'identifier': str(uuid.uuid4()), + } + + share_server.update(attrs) + share_server = fakes.FakeResource( + info=copy.deepcopy(share_server), methods=methods, loaded=True + ) + return share_server + + @staticmethod + def create_share_servers(attrs=None, count=2): + """Create multiple fake servers. + + :param dict attrs: + A dictionary with all attributes + :param int count: + The number of share server to be faked + :return: + A list of FakeResource objects + """ + attrs = attrs or {} + share_servers = [] + for n in range(count): + share_servers.append(FakeShareServer.create_one_server(attrs)) + return share_servers + + +class FakeResourceLock: + """Fake a resource lock""" + + @staticmethod + def create_one_lock(attrs=None, methods=None): + """Create a fake resource lock + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + now_time = datetime.datetime.now() + delta_time = now_time + datetime.timedelta(minutes=5) + + lock = { + 'id': str(uuid.uuid4()), + 'resource_id': str(uuid.uuid4()), + 'resource_type': 'share', + 'resource_action': 'delete', + 'created_at': now_time.isoformat(), + 'updated_at': delta_time.isoformat(), + 'project_id': uuid.uuid4().hex, + 'user_id': uuid.uuid4().hex, + 'lock_context': 'user', + 'lock_reason': 'created by func tests', + } + + lock.update(attrs) + lock = fakes.FakeResource( + info=copy.deepcopy(lock), methods=methods, loaded=True + ) + return lock + + @staticmethod + def create_locks(attrs=None, count=2): + """Create multiple fake locks. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share transfers to be faked + :return: + A list of FakeResource objects + """ + + resource_locks = [] + for n in range(0, count): + resource_locks.append(FakeResourceLock.create_one_lock(attrs)) + + return resource_locks + + +class FakeShareBackup: + """Fake a share Backup""" + + @staticmethod + def create_one_backup(attrs=None, methods=None): + """Create a fake share backup + + :param Dictionary attrs: + A dictionary with all attributes + :return: + A FakeResource object, with project_id, resource and so on + """ + + attrs = attrs or {} + methods = methods or {} + + share_backup = { + 'id': 'backup-id-' + uuid.uuid4().hex, + 'share_id': 'share-id-' + uuid.uuid4().hex, + 'status': None, + 'name': None, + 'description': None, + 'size': '0', + 'created_at': datetime.datetime.now().isoformat(), + 'updated_at': datetime.datetime.now().isoformat(), + 'availability_zone': None, + 'progress': None, + 'restore_progress': None, + 'host': None, + 'topic': None, + } + + share_backup.update(attrs) + share_backup = fakes.FakeResource( + info=copy.deepcopy(share_backup), methods=methods, loaded=True + ) + return share_backup + + @staticmethod + def create_share_backups(attrs=None, count=2): + """Create multiple fake backups. + + :param Dictionary attrs: + A dictionary with all attributes + :param Integer count: + The number of share backups to be faked + :return: + A list of FakeResource objects + """ + + share_backups = [] + for n in range(0, count): + share_backups.append(FakeShareBackup.create_one_backup(attrs)) + return share_backups diff --git a/openstackclient/tests/unit/share/v2/test_messages.py b/openstackclient/tests/unit/share/v2/test_messages.py new file mode 100644 index 0000000000..5d3cae2a47 --- /dev/null +++ b/openstackclient/tests/unit/share/v2/test_messages.py @@ -0,0 +1,191 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from manilaclient import api_versions +from osc_lib import exceptions +from osc_lib import utils as oscutils + +from openstackclient.share.v2 import messages +from openstackclient.tests.unit.share.v2 import fakes as share_fakes +from openstackclient.tests.unit import utils as test_utils + +COLUMNS = [ + 'ID', + 'Resource Type', + 'Resource ID', + 'Action ID', + 'User Message', + 'Detail ID', + 'Created At', +] + + +class TestMessage(share_fakes.TestShare): + def setUp(self): + super().setUp() + + self.messages_mock = self.share_client.messages + self.messages_mock.reset_mock() + + self.set_share_api_version(api_versions.MAX_VERSION) + + +class TestMessageDelete(TestMessage): + def setUp(self): + super().setUp() + + self.message = share_fakes.FakeMessage.create_one_message() + + self.messages_mock.get.return_value = self.message + + self.cmd = messages.DeleteMessage(self.app, None) + + def test_message_delete_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_message_delete(self): + arglist = [self.message.id] + verifylist = [('message', [self.message.id])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.messages_mock.delete.assert_called_with(self.message) + self.assertIsNone(result) + + def test_message_delete_multiple(self): + messages = share_fakes.FakeMessage.create_messages(count=2) + arglist = [messages[0].id, messages[1].id] + verifylist = [('message', [messages[0].id, messages[1].id])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertEqual(self.messages_mock.delete.call_count, len(messages)) + self.assertIsNone(result) + + def test_message_delete_exception(self): + arglist = [self.message.id] + verifylist = [('message', [self.message.id])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.messages_mock.delete.side_effect = exceptions.CommandError() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestMessageShow(TestMessage): + def setUp(self): + super().setUp() + + self.message = share_fakes.FakeMessage.create_one_message() + self.messages_mock.get.return_value = self.message + + self.cmd = messages.ShowMessage(self.app, None) + + self.data = self.message._info.values() + self.columns = self.message._info.keys() + + def test_message_show_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_message_show(self): + arglist = [self.message.id] + verifylist = [('message', self.message.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.messages_mock.get.assert_called_with(self.message.id) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + +class TestMessageList(TestMessage): + def setUp(self): + super().setUp() + + self.messages = share_fakes.FakeMessage.create_messages(count=2) + + self.messages_mock.list.return_value = self.messages + + self.values = ( + oscutils.get_dict_properties(m._info, COLUMNS) + for m in self.messages + ) + + self.cmd = messages.ListMessage(self.app, None) + + def test_list_messages(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.messages_mock.list.assert_called_with( + search_opts={ + 'limit': None, + 'request_id': None, + 'resource_type': None, + 'resource_id': None, + 'action_id': None, + 'detail_id': None, + 'message_level': None, + 'created_since': None, + 'created_before': None, + } + ) + + self.assertEqual(COLUMNS, columns) + self.assertEqual(list(self.values), list(data)) + + def test_list_messages_api_version_exception(self): + self.set_share_api_version('2.50') + + arglist = [ + '--before', + '2021-02-06T09:49:58-05:00', + '--since', + '2021-02-05T09:49:58-05:00', + ] + verifylist = [ + ('before', '2021-02-06T09:49:58-05:00'), + ('since', '2021-02-05T09:49:58-05:00'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) diff --git a/openstackclient/tests/unit/share/v2/test_resource_locks.py b/openstackclient/tests/unit/share/v2/test_resource_locks.py new file mode 100644 index 0000000000..c1c4078527 --- /dev/null +++ b/openstackclient/tests/unit/share/v2/test_resource_locks.py @@ -0,0 +1,338 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from manilaclient import api_versions +from osc_lib import exceptions +from osc_lib import utils as oscutils + +from openstackclient.share.v2 import resource_locks +from openstackclient.tests.unit.share.v2 import fakes as share_fakes +from openstackclient.tests.unit import utils as test_utils + +DETAIL_COLUMNS = [ + 'ID', + 'Resource Id', + 'Resource Type', + 'Resource Action', + 'Created At', + 'Updated At', + 'User Id', + 'Project Id', + 'Lock Reason', + 'Lock Context', +] + +SUMMARY_COLUMNS = [ + 'ID', + 'Resource Id', + 'Resource Type', + 'Resource Action', +] + + +class TestResourceLock(share_fakes.TestShare): + def setUp(self): + super().setUp() + + self.shares_mock = self.share_client.shares + self.shares_mock.reset_mock() + + self.locks_mock = self.share_client.resource_locks + self.locks_mock.reset_mock() + + self.set_share_api_version(api_versions.MAX_VERSION) + + +class TestResourceLockCreate(TestResourceLock): + def setUp(self): + super().setUp() + + self.share = share_fakes.FakeShare.create_one_share() + self.shares_mock.create.return_value = self.share + + self.shares_mock.get.return_value = self.share + + self.lock = share_fakes.FakeResourceLock.create_one_lock( + attrs={'resource_id': self.share.id} + ) + self.locks_mock.get.return_value = self.lock + self.locks_mock.create.return_value = self.lock + + self.cmd = resource_locks.CreateResourceLock(self.app, None) + + self.data = tuple(self.lock._info.values()) + self.columns = tuple(self.lock._info.keys()) + + def test_share_lock_create_missing_required_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_lock_create(self): + arglist = [ + '--resource-action', + 'revert_to_snapshot', + '--lock-reason', + "you cannot go back in time", + self.share.id, + 'share', + ] + verifylist = [ + ('resource', self.share.id), + ('resource_type', 'share'), + ('resource_action', 'revert_to_snapshot'), + ('lock_reason', 'you cannot go back in time'), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.locks_mock.create.assert_called_with( + self.share.id, + 'share', + 'revert_to_snapshot', + 'you cannot go back in time', + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + +class TestResourceLockDelete(TestResourceLock): + def setUp(self): + super().setUp() + + self.lock = share_fakes.FakeResourceLock.create_one_lock() + + self.locks_mock.get.return_value = self.lock + self.lock.delete = mock.Mock() + + self.cmd = resource_locks.DeleteResourceLock(self.app, None) + + def test_share_lock_delete_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_lock_delete(self): + arglist = [self.lock.id] + verifylist = [('lock', [self.lock.id])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.lock.delete.assert_called_once_with() + self.assertIsNone(result) + + def test_share_lock_delete_multiple(self): + locks = share_fakes.FakeResourceLock.create_locks(count=2) + arglist = [locks[0].id, locks[1].id] + verifylist = [('lock', [locks[0].id, locks[1].id])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertEqual(self.lock.delete.call_count, len(locks)) + self.assertIsNone(result) + + def test_share_lock_delete_exception(self): + arglist = [self.lock.id] + verifylist = [('lock', [self.lock.id])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.lock.delete.side_effect = exceptions.CommandError() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestResourceLockShow(TestResourceLock): + def setUp(self): + super().setUp() + + self.lock = share_fakes.FakeResourceLock.create_one_lock() + self.locks_mock.get.return_value = self.lock + + self.cmd = resource_locks.ShowResourceLock(self.app, None) + + self.data = self.lock._info.values() + self.columns = list(self.lock._info.keys()) + + def test_share_lock_show_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_lock_show(self): + arglist = [ + self.lock.id, + ] + verifylist = [('lock', self.lock.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.locks_mock.get.assert_called_with(self.lock.id) + self.assertEqual(len(self.columns), len(columns)) + self.assertCountEqual(sorted(self.data), sorted(data)) + + +class TestResourceLockList(TestResourceLock): + def setUp(self): + super().setUp() + + self.locks = share_fakes.FakeResourceLock.create_locks(count=2) + + self.locks_mock.list.return_value = self.locks + + self.values = ( + oscutils.get_dict_properties(m._info, DETAIL_COLUMNS) + for m in self.locks + ) + + self.cmd = resource_locks.ListResourceLock(self.app, None) + + def test_share_lock_list(self): + arglist = ['--detailed'] + verifylist = [('detailed', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.locks_mock.list.assert_called_with( + search_opts={ + 'all_projects': False, + 'project_id': None, + 'user_id': None, + 'id': None, + 'resource_id': None, + 'resource_type': None, + 'resource_action': None, + 'lock_context': None, + 'created_before': None, + 'created_since': None, + 'limit': None, + 'offset': None, + }, + sort_key=None, + sort_dir=None, + ) + + self.assertEqual(sorted(DETAIL_COLUMNS), sorted(columns)) + actual_data = [sorted(d) for d in data] + expected_data = [sorted(v) for v in self.values] + self.assertEqual(actual_data, expected_data) + + +class TestResourceLockSet(TestResourceLock): + def setUp(self): + super().setUp() + + self.lock = share_fakes.FakeResourceLock.create_one_lock() + self.lock.update = mock.Mock() + + self.locks_mock.get.return_value = self.lock + + self.cmd = resource_locks.SetResourceLock(self.app, None) + + def test_share_lock_set_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_lock_set(self): + arglist = [ + self.lock.id, + '--resource-action', + 'unmanage', + ] + verifylist = [('lock', self.lock.id), ('resource_action', 'unmanage')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.locks_mock.update.assert_called_with( + self.lock.id, resource_action='unmanage' + ) + + +class TestResourceLockUnSet(TestResourceLock): + def setUp(self): + super().setUp() + + self.lock = share_fakes.FakeResourceLock.create_one_lock() + self.lock.update = mock.Mock() + + self.locks_mock.get.return_value = self.lock + + self.cmd = resource_locks.UnsetResourceLock(self.app, None) + + def test_share_lock_unset_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_lock_unset(self): + arglist = [self.lock.id, '--lock-reason'] + verifylist = [('lock', self.lock.id), ('lock_reason', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.locks_mock.update.assert_called_with( + self.lock.id, lock_reason=None + ) diff --git a/openstackclient/tests/unit/share/v2/test_security_services.py b/openstackclient/tests/unit/share/v2/test_security_services.py new file mode 100644 index 0000000000..354a8484c0 --- /dev/null +++ b/openstackclient/tests/unit/share/v2/test_security_services.py @@ -0,0 +1,664 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# +import ddt +from manilaclient import api_versions +from openstackclient.share.v2 import security_services +from osc_lib import exceptions +from osc_lib import utils as oscutils + +from openstackclient.tests.unit.share.v2 import fakes as share_fakes +from openstackclient.tests.unit import utils as test_utils + + +class TestShareSecurityService(share_fakes.TestShare): + def setUp(self): + super().setUp() + + self.security_services_mock = self.share_client.security_services + self.security_services_mock.reset_mock() + + self.share_networks_mock = self.share_client.share_networks + self.share_networks_mock.reset_mock() + + self.set_share_api_version(api_versions.MAX_VERSION) + + +@ddt.ddt +class TestShareSecurityServiceCreate(TestShareSecurityService): + def setUp(self): + super().setUp() + + self.security_service = ( + share_fakes.FakeShareSecurityService.create_fake_security_service() + ) + self.security_services_mock.create.return_value = self.security_service + self.cmd = security_services.CreateShareSecurityService(self.app, None) + + self.data = self.security_service._info.values() + self.columns = self.security_service._info.keys() + + def test_share_security_service_create_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_security_service_create(self): + arglist = [ + self.security_service.type, + '--dns-ip', + self.security_service.dns_ip, + '--ou', + self.security_service.ou, + '--server', + self.security_service.server, + '--domain', + self.security_service.domain, + '--user', + self.security_service.user, + '--password', + self.security_service.password, + '--name', + self.security_service.name, + '--description', + self.security_service.description, + '--default-ad-site', + self.security_service.default_ad_site, + ] + verifylist = [ + ('type', self.security_service.type), + ('dns_ip', self.security_service.dns_ip), + ('ou', self.security_service.ou), + ('server', self.security_service.server), + ('domain', self.security_service.domain), + ('user', self.security_service.user), + ('password', self.security_service.password), + ('name', self.security_service.name), + ('description', self.security_service.description), + ('default_ad_site', self.security_service.default_ad_site), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.security_services_mock.create.assert_called_with( + self.security_service.type, + dns_ip=self.security_service.dns_ip, + server=self.security_service.server, + domain=self.security_service.domain, + user=self.security_service.user, + password=self.security_service.password, + name=self.security_service.name, + description=self.security_service.description, + ou=self.security_service.ou, + default_ad_site=self.security_service.default_ad_site, + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + @ddt.data('2.43', '2.75') + def test_share_security_service_create_api_version_exception( + self, version + ): + self.set_share_api_version(version) + + arglist = [ + self.security_service.type, + ] + verifylist = [ + ('type', self.security_service.type), + ] + + if api_versions.APIVersion(version) <= api_versions.APIVersion("2.43"): + arglist.extend(['--ou', self.security_service.ou]) + verifylist.append(('ou', self.security_service.ou)) + + if api_versions.APIVersion(version) <= api_versions.APIVersion("2.75"): + arglist.extend( + ['--default-ad-site', self.security_service.default_ad_site] + ) + verifylist.append( + ('default_ad_site', self.security_service.default_ad_site) + ) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestShareSecurityServiceDelete(TestShareSecurityService): + def setUp(self): + super().setUp() + + self.security_service = ( + share_fakes.FakeShareSecurityService.create_fake_security_service() + ) + self.security_services_mock.get.return_value = self.security_service + + self.security_services = share_fakes.FakeShareSecurityService.create_fake_security_services() + + self.cmd = security_services.DeleteShareSecurityService(self.app, None) + + def test_share_security_service_delete_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_security_service_delete(self): + arglist = [ + self.security_services[0].id, + self.security_services[1].id, + ] + verifylist = [ + ( + 'security_service', + [self.security_services[0].id, self.security_services[1].id], + ), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertEqual( + self.security_services_mock.delete.call_count, + len(self.security_services), + ) + self.assertIsNone(result) + + def test_share_security_service_delete_exception(self): + arglist = [ + self.security_services[0].id, + ] + verifylist = [ + ('security_service', [self.security_services[0].id]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.security_services_mock.delete.side_effect = ( + exceptions.CommandError() + ) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestShareSecurityServiceShow(TestShareSecurityService): + def setUp(self): + super().setUp() + + self.security_service = ( + share_fakes.FakeShareSecurityService.create_fake_security_service() + ) + self.security_services_mock.get.return_value = self.security_service + + self.cmd = security_services.ShowShareSecurityService(self.app, None) + + self.data = self.security_service._info.values() + self.columns = self.security_service._info.keys() + + def test_share_security_service_show_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_security_service_show(self): + arglist = [self.security_service.id] + verifylist = [('security_service', self.security_service.id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.security_services_mock.get.assert_called_with( + self.security_service.id + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + +@ddt.ddt +class TestShareSecurityServiceSet(TestShareSecurityService): + def setUp(self): + super().setUp() + + self.security_service = ( + share_fakes.FakeShareSecurityService.create_fake_security_service( + methods={'update': None} + ) + ) + self.security_services_mock.get.return_value = self.security_service + self.cmd = security_services.SetShareSecurityService(self.app, None) + + def test_share_security_service_set_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_security_service_set(self): + arglist = [ + self.security_service.id, + '--dns-ip', + self.security_service.dns_ip, + '--ou', + self.security_service.ou, + '--server', + self.security_service.server, + '--domain', + self.security_service.domain, + '--user', + self.security_service.user, + '--password', + self.security_service.password, + '--name', + self.security_service.name, + '--description', + self.security_service.description, + '--default-ad-site', + self.security_service.default_ad_site, + ] + verifylist = [ + ('security_service', self.security_service.id), + ('dns_ip', self.security_service.dns_ip), + ('ou', self.security_service.ou), + ('server', self.security_service.server), + ('domain', self.security_service.domain), + ('user', self.security_service.user), + ('password', self.security_service.password), + ('name', self.security_service.name), + ('description', self.security_service.description), + ('default_ad_site', self.security_service.default_ad_site), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.security_service.update.assert_called_with( + dns_ip=self.security_service.dns_ip, + server=self.security_service.server, + domain=self.security_service.domain, + user=self.security_service.user, + password=self.security_service.password, + name=self.security_service.name, + description=self.security_service.description, + ou=self.security_service.ou, + default_ad_site=self.security_service.default_ad_site, + ) + self.assertIsNone(result) + + def test_share_security_service_set_exception(self): + arglist = [ + self.security_service.id, + '--name', + self.security_service.name, + ] + verifylist = [ + ('security_service', self.security_service.id), + ('name', self.security_service.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.security_service.update.side_effect = exceptions.CommandError() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + @ddt.data('2.43', '2.75') + def test_share_security_service_set_api_version_exception(self, version): + self.set_share_api_version(version) + + arglist = [ + self.security_service.id, + ] + verifylist = [ + ('security_service', self.security_service.id), + ] + + if api_versions.APIVersion(version) <= api_versions.APIVersion("2.43"): + arglist.extend(['--ou', self.security_service.ou]) + verifylist.append(('ou', self.security_service.ou)) + + if api_versions.APIVersion(version) <= api_versions.APIVersion("2.75"): + arglist.extend( + ['--default-ad-site', self.security_service.default_ad_site] + ) + verifylist.append( + ('default_ad_site', self.security_service.default_ad_site) + ) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +@ddt.ddt +class TestShareSecurityServiceUnset(TestShareSecurityService): + def setUp(self): + super().setUp() + + self.security_service = ( + share_fakes.FakeShareSecurityService.create_fake_security_service( + methods={'update': None} + ) + ) + self.security_services_mock.get.return_value = self.security_service + self.cmd = security_services.UnsetShareSecurityService(self.app, None) + + def test_share_security_service_unset_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_security_service_unset(self): + arglist = [ + self.security_service.id, + '--dns-ip', + '--ou', + '--server', + '--domain', + '--user', + '--password', + '--name', + '--description', + '--default-ad-site', + ] + verifylist = [ + ('security_service', self.security_service.id), + ('dns_ip', True), + ('ou', True), + ('server', True), + ('domain', True), + ('user', True), + ('password', True), + ('name', True), + ('description', True), + ('default_ad_site', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.security_service.update.assert_called_with( + dns_ip='', + server='', + domain='', + user='', + password='', + name='', + description='', + ou='', + default_ad_site='', + ) + self.assertIsNone(result) + + def test_share_security_service_unset_exception(self): + arglist = [ + self.security_service.id, + '--name', + ] + verifylist = [ + ('security_service', self.security_service.id), + ('name', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.security_service.update.side_effect = exceptions.CommandError() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + @ddt.data('2.43', '2.75') + def test_share_security_service_unset_api_version_exception(self, version): + self.set_share_api_version(version) + + arglist = [ + self.security_service.id, + ] + verifylist = [ + ('security_service', self.security_service.id), + ] + + if api_versions.APIVersion(version) <= api_versions.APIVersion("2.43"): + arglist.extend(['--ou']) + verifylist.append(('ou', True)) + + if api_versions.APIVersion(version) <= api_versions.APIVersion("2.75"): + (arglist.extend(['--default-ad-site']),) + verifylist.append(('default_ad_site', True)) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestShareSecurityServiceList(TestShareSecurityService): + columns = [ + 'ID', + 'Name', + 'Status', + 'Type', + ] + + def setUp(self): + super().setUp() + + self.share_network = ( + share_fakes.FakeShareNetwork.create_one_share_network() + ) + self.share_networks_mock.get.return_value = self.share_network + self.services_list = share_fakes.FakeShareSecurityService.create_fake_security_services() + self.security_services_mock.list.return_value = self.services_list + self.values = ( + oscutils.get_dict_properties(i._info, self.columns) + for i in self.services_list + ) + + self.cmd = security_services.ListShareSecurityService(self.app, None) + + def test_share_security_service_list_no_args(self): + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.security_services_mock.list.assert_called_with( + search_opts={ + 'all_tenants': False, + 'status': None, + 'name': None, + 'type': None, + 'user': None, + 'dns_ip': None, + 'server': None, + 'domain': None, + 'offset': None, + 'limit': None, + }, + detailed=False, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.values), list(data)) + + def test_share_security_service_list(self): + arglist = [ + '--share-network', + self.share_network.id, + '--status', + self.services_list[0].status, + '--name', + self.services_list[0].name, + '--type', + self.services_list[0].type, + '--user', + self.services_list[0].user, + '--dns-ip', + self.services_list[0].dns_ip, + '--ou', + self.services_list[0].ou, + '--server', + self.services_list[0].server, + '--domain', + self.services_list[0].domain, + '--default-ad-site', + self.services_list[0].default_ad_site, + '--limit', + '1', + ] + verifylist = [ + ('share_network', self.share_network.id), + ('status', self.services_list[0].status), + ('name', self.services_list[0].name), + ('type', self.services_list[0].type), + ('user', self.services_list[0].user), + ('dns_ip', self.services_list[0].dns_ip), + ('ou', self.services_list[0].ou), + ('server', self.services_list[0].server), + ('domain', self.services_list[0].domain), + ('default_ad_site', self.services_list[0].default_ad_site), + ('limit', 1), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.security_services_mock.list.assert_called_with( + search_opts={ + 'all_tenants': False, + 'status': self.services_list[0].status, + 'name': self.services_list[0].name, + 'type': self.services_list[0].type, + 'user': self.services_list[0].user, + 'dns_ip': self.services_list[0].dns_ip, + 'server': self.services_list[0].server, + 'domain': self.services_list[0].domain, + 'default_ad_site': self.services_list[0].default_ad_site, + 'offset': None, + 'limit': 1, + 'ou': self.services_list[0].ou, + 'share_network_id': self.share_network.id, + }, + detailed=False, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.values), list(data)) + + def test_share_security_service_list_ou_api_version_exception(self): + self.set_share_api_version('2.43') + + arglist = [ + '--ou', + self.services_list[0].ou, + ] + verifylist = [ + ('ou', self.services_list[0].ou), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + def test_share_security_service_list_ad_site_api_version_exception(self): + self.set_share_api_version('2.75') + + arglist = [ + '--default-ad-site', + self.services_list[0].default_ad_site, + ] + verifylist = [ + ('default_ad_site', self.services_list[0].default_ad_site), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + def test_share_security_service_list_detail_all_projects(self): + arglist = ['--all-projects', '--detail'] + verifylist = [ + ('all_projects', True), + ('detail', True), + ] + columns_detail = self.columns.copy() + columns_detail.append('Project ID') + columns_detail.append('Share Networks') + + values_detail = ( + oscutils.get_dict_properties(i._info, columns_detail) + for i in self.services_list + ) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.security_services_mock.list.assert_called_with( + search_opts={ + 'all_tenants': True, + 'status': None, + 'name': None, + 'type': None, + 'user': None, + 'dns_ip': None, + 'server': None, + 'domain': None, + 'offset': None, + 'limit': None, + }, + detailed=True, + ) + self.assertEqual(columns_detail, columns) + self.assertEqual(list(values_detail), list(data)) diff --git a/openstackclient/tests/unit/share/v2/test_services.py b/openstackclient/tests/unit/share/v2/test_services.py new file mode 100644 index 0000000000..c2742b7a83 --- /dev/null +++ b/openstackclient/tests/unit/share/v2/test_services.py @@ -0,0 +1,316 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import ddt +from manilaclient import api_versions +from manilaclient.osc import utils +from osc_lib import exceptions +from osc_lib import utils as oscutils + +from openstackclient.share.v2 import services as services +from openstackclient.tests.unit.share.v2 import fakes as share_fakes + + +class TestShareService(share_fakes.TestShare): + def setUp(self): + super().setUp() + + self.services_mock = self.share_client.services + self.services_mock.reset_mock() + + +class TestShareServiceSet(TestShareService): + def setUp(self): + super().setUp() + + self.share_service = share_fakes.FakeShareService.create_fake_service() + self.cmd = services.SetShareService(self.app, None) + + def test_share_service_set_enable(self): + arglist = [ + self.share_service.host, + self.share_service.binary, + '--enable', + ] + verifylist = [ + ('host', self.share_service.host), + ('binary', self.share_service.binary), + ('enable', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.services_mock.enable.assert_called_with( + self.share_service.host, self.share_service.binary + ) + self.assertIsNone(result) + + def test_share_service_set_enable_exception(self): + arglist = [ + self.share_service.host, + self.share_service.binary, + '--enable', + ] + verifylist = [ + ('host', self.share_service.host), + ('binary', self.share_service.binary), + ('enable', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.services_mock.enable.side_effect = Exception() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + def test_share_service_set_disable(self): + arglist = [ + self.share_service.host, + self.share_service.binary, + '--disable', + ] + verifylist = [ + ('host', self.share_service.host), + ('binary', self.share_service.binary), + ('disable', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.services_mock.disable.assert_called_with( + self.share_service.host, self.share_service.binary + ) + self.assertIsNone(result) + + def test_service_set_disable_with_reason(self): + self.set_share_api_version('2.83') + reason = 'earthquake' + arglist = [ + '--disable', + '--disable-reason', + reason, + self.share_service.host, + self.share_service.binary, + ] + verifylist = [ + ('host', self.share_service.host), + ('binary', self.share_service.binary), + ('disable', True), + ('disable_reason', reason), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.services_mock.disable.assert_called_with( + self.share_service.host, + self.share_service.binary, + disable_reason=reason, + ) + self.assertIsNone(result) + + def test_share_service_set_disable_exception(self): + arglist = [ + self.share_service.host, + self.share_service.binary, + '--disable', + ] + verifylist = [ + ('host', self.share_service.host), + ('binary', self.share_service.binary), + ('disable', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.services_mock.disable.side_effect = Exception() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +@ddt.ddt +class TestShareServiceList(TestShareService): + columns = ['id', 'binary', 'host', 'zone', 'status', 'state', 'updated_at'] + columns_with_reason = [*columns, 'disabled_reason'] + + column_headers = utils.format_column_headers(columns) + column_headers_with_reason = utils.format_column_headers( + columns_with_reason + ) + + def setUp(self): + super().setUp() + + self.services_list = share_fakes.FakeShareService.create_fake_services( + {'disabled_reason': ''} + ) + self.services_mock.list.return_value = self.services_list + self.values = ( + oscutils.get_dict_properties(i._info, self.columns) + for i in self.services_list + ) + self.values_with_reason = ( + oscutils.get_dict_properties(i._info, self.columns_with_reason) + for i in self.services_list + ) + + self.cmd = services.ListShareService(self.app, None) + + @ddt.data('2.82', '2.83') + def test_share_service_list(self, version): + self.set_share_api_version(version) + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.services_mock.list.assert_called_with( + search_opts={ + 'host': None, + 'binary': None, + 'status': None, + 'state': None, + 'zone': None, + } + ) + if api_versions.APIVersion(version) >= api_versions.APIVersion("2.83"): + self.assertEqual(self.column_headers_with_reason, columns) + self.assertEqual(list(self.values_with_reason), list(data)) + else: + self.assertEqual(self.column_headers, columns) + self.assertEqual(list(self.values), list(data)) + + @ddt.data('2.82', '2.83') + def test_share_service_list_host_status(self, version): + self.set_share_api_version(version) + arglist = [ + '--host', + self.services_list[0].host, + '--status', + self.services_list[1].status, + ] + verifylist = [ + ('host', self.services_list[0].host), + ('status', self.services_list[1].status), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.services_mock.list.assert_called_with( + search_opts={ + 'host': self.services_list[0].host, + 'binary': None, + 'status': self.services_list[1].status, + 'state': None, + 'zone': None, + } + ) + if api_versions.APIVersion(version) >= api_versions.APIVersion("2.83"): + self.assertEqual(self.column_headers_with_reason, columns) + self.assertEqual(list(self.values_with_reason), list(data)) + else: + self.assertEqual(self.column_headers, columns) + self.assertEqual(list(self.values), list(data)) + + @ddt.data('2.82', '2.83') + def test_share_service_list_binary_state_zone(self, version): + self.set_share_api_version(version) + arglist = [ + '--binary', + self.services_list[0].binary, + '--state', + self.services_list[1].state, + '--zone', + self.services_list[1].zone, + ] + verifylist = [ + ('binary', self.services_list[0].binary), + ('state', self.services_list[1].state), + ('zone', self.services_list[1].zone), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.services_mock.list.assert_called_with( + search_opts={ + 'host': None, + 'binary': self.services_list[0].binary, + 'status': None, + 'state': self.services_list[1].state, + 'zone': self.services_list[1].zone, + } + ) + if api_versions.APIVersion(version) >= api_versions.APIVersion("2.83"): + self.assertEqual(self.column_headers_with_reason, columns) + self.assertEqual(list(self.values_with_reason), list(data)) + else: + self.assertEqual(self.column_headers, columns) + self.assertEqual(list(self.values), list(data)) + + +@ddt.ddt +class TestShareServiceEnsureShares(TestShareService): + def setUp(self): + super().setUp() + + self.cmd = services.EnsureShareService(self.app, None) + + def test_ensure_shares(self): + self.set_share_api_version('2.86') + fake_host = 'fake_host@fakebackend' + arglist = [ + fake_host, + ] + verifylist = [ + ('host', fake_host), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + self.services_mock.ensure_shares.assert_called_with(fake_host) + + def test_ensure_shares_invalid_version(self): + self.set_share_api_version('2.85') + fake_host = 'fake_host@fakebackend' + arglist = [ + fake_host, + ] + verifylist = [ + ('host', fake_host), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + def test_ensure_shares_command_error(self): + self.set_share_api_version('2.86') + self.services_mock.ensure_shares.side_effect = Exception() + fake_host = 'fake_host@fakebackend' + arglist = [ + fake_host, + ] + verifylist = [ + ('host', fake_host), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) diff --git a/openstackclient/tests/unit/share/v2/test_share_group_snapshots.py b/openstackclient/tests/unit/share/v2/test_share_group_snapshots.py new file mode 100644 index 0000000000..631a8dd7c2 --- /dev/null +++ b/openstackclient/tests/unit/share/v2/test_share_group_snapshots.py @@ -0,0 +1,618 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import logging +from unittest import mock +import uuid + +from manilaclient import api_versions +from osc_lib import exceptions +from osc_lib import utils as oscutils + +from openstackclient.share.v2 import share_group_snapshots +from openstackclient.tests.unit.share.v2 import fakes as share_fakes +from openstackclient.tests.unit import utils as test_utils + +LOG = logging.getLogger(__name__) + + +class TestShareGroupSnapshot(share_fakes.TestShare): + def setUp(self): + super().setUp() + + self.groups_mock = self.share_client.share_groups + self.groups_mock.reset_mock() + + self.group_snapshot_mocks = self.share_client.share_group_snapshots + self.group_snapshot_mocks.reset_mock() + + self.set_share_api_version(api_versions.MAX_VERSION) + + +class TestCreateShareGroupSnapshot(TestShareGroupSnapshot): + def setUp(self): + super().setUp() + + self.share_group = share_fakes.FakeShareGroup.create_one_share_group() + self.groups_mock.get.return_value = self.share_group + + self.share_group_snapshot = share_fakes.FakeShareGroupSnapshot.create_one_share_group_snapshot() + self.group_snapshot_mocks.create.return_value = ( + self.share_group_snapshot + ) + self.group_snapshot_mocks.get.return_value = self.share_group_snapshot + + self.cmd = share_group_snapshots.CreateShareGroupSnapshot( + self.app, None + ) + + self.data = tuple(self.share_group_snapshot._info.values()) + self.columns = tuple(self.share_group_snapshot._info.keys()) + + def test_share_group_snapshot_create_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_group_snapshot_create(self): + arglist = [self.share_group.id] + verifylist = [('share_group', self.share_group.id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.create.assert_called_with( + self.share_group, name=None, description=None + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_share_group_snapshot_create_options(self): + arglist = [ + self.share_group.id, + '--name', + self.share_group_snapshot.name, + '--description', + self.share_group_snapshot.description, + ] + verifylist = [ + ('share_group', self.share_group.id), + ('name', self.share_group_snapshot.name), + ('description', self.share_group_snapshot.description), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.create.assert_called_with( + self.share_group, + name=self.share_group_snapshot.name, + description=self.share_group_snapshot.description, + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_share_group_snapshot_create_wait(self): + arglist = [self.share_group.id, '--wait'] + verifylist = [('share_group', self.share_group.id), ('wait', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + with mock.patch('osc_lib.utils.wait_for_status', return_value=True): + self.group_snapshot_mocks.create.assert_called_with( + self.share_group, + name=None, + description=None, + ) + self.group_snapshot_mocks.get.assert_called_with( + self.share_group_snapshot.id + ) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + @mock.patch('openstackclient.share.v2.share_group_snapshots.LOG') + def test_share_group_snapshot_create_wait_exception(self, mock_logger): + arglist = [self.share_group.id, '--wait'] + verifylist = [('share_group', self.share_group.id), ('wait', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('osc_lib.utils.wait_for_status', return_value=False): + columns, data = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.create.assert_called_with( + self.share_group, + name=None, + description=None, + ) + + mock_logger.error.assert_called_with( + "ERROR: Share group snapshot is in error state." + ) + + self.group_snapshot_mocks.get.assert_called_with( + self.share_group_snapshot.id + ) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + +class TestDeleteShareGroupSnapshot(TestShareGroupSnapshot): + def setUp(self): + super().setUp() + + self.share_group_snapshot = share_fakes.FakeShareGroupSnapshot.create_one_share_group_snapshot() + self.group_snapshot_mocks.get.return_value = self.share_group_snapshot + + self.cmd = share_group_snapshots.DeleteShareGroupSnapshot( + self.app, None + ) + + def test_share_group_snapshot_delete_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_group_snapshot_delete(self): + arglist = [self.share_group_snapshot.id] + verifylist = [('share_group_snapshot', [self.share_group_snapshot.id])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.delete.assert_called_with( + self.share_group_snapshot, force=False + ) + self.assertIsNone(result) + + def test_share_group_snapshot_delete_force(self): + arglist = [self.share_group_snapshot.id, '--force'] + verifylist = [ + ('share_group_snapshot', [self.share_group_snapshot.id]), + ('force', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.delete.assert_called_with( + self.share_group_snapshot, force=True + ) + self.assertIsNone(result) + + def test_share_group_snapshot_delete_multiple(self): + share_group_snapshots = ( + share_fakes.FakeShareGroupSnapshot.create_share_group_snapshots( + count=2 + ) + ) + arglist = [share_group_snapshots[0].id, share_group_snapshots[1].id] + verifylist = [ + ( + 'share_group_snapshot', + [share_group_snapshots[0].id, (share_group_snapshots[1].id)], + ) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertEqual( + self.group_snapshot_mocks.delete.call_count, + len(share_group_snapshots), + ) + self.assertIsNone(result) + + def test_share_group_snapshot_delete_exception(self): + arglist = [self.share_group_snapshot.id] + verifylist = [('share_group_snapshot', [self.share_group_snapshot.id])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.group_snapshot_mocks.delete.side_effect = ( + exceptions.CommandError() + ) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + def test_share_group_snapshot_delete_wait(self): + arglist = [self.share_group_snapshot.id, '--wait'] + verifylist = [ + ('share_group_snapshot', [self.share_group_snapshot.id]), + ('wait', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('osc_lib.utils.wait_for_delete', return_value=True): + result = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.delete.assert_called_with( + self.share_group_snapshot, force=False + ) + self.group_snapshot_mocks.get.assert_called_with( + self.share_group_snapshot.id + ) + self.assertIsNone(result) + + def test_share_group_snapshot_delete_wait_exception(self): + arglist = [self.share_group_snapshot.id, '--wait'] + verifylist = [ + ('share_group_snapshot', [self.share_group_snapshot.id]), + ('wait', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('osc_lib.utils.wait_for_delete', return_value=False): + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestShowShareGroupSnapshot(TestShareGroupSnapshot): + def setUp(self): + super().setUp() + + self.share_group_snapshot = share_fakes.FakeShareGroupSnapshot.create_one_share_group_snapshot() + self.group_snapshot_mocks.get.return_value = self.share_group_snapshot + + self.cmd = share_group_snapshots.ShowShareGroupSnapshot(self.app, None) + + self.data = tuple(self.share_group_snapshot._info.values()) + self.columns = tuple(self.share_group_snapshot._info.keys()) + + def test_share_group_snapshot_show_missing_args(self): + arglist = [] + verifylist = [] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_group_show(self): + arglist = [self.share_group_snapshot.id] + verifylist = [('share_group_snapshot', self.share_group_snapshot.id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.get.assert_called_with( + self.share_group_snapshot.id + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + +class TestSetShareGroupSnapshot(TestShareGroupSnapshot): + def setUp(self): + super().setUp() + + self.share_group_snapshot = share_fakes.FakeShareGroupSnapshot.create_one_share_group_snapshot() + self.group_snapshot_mocks.get.return_value = self.share_group_snapshot + + self.cmd = share_group_snapshots.SetShareGroupSnapshot(self.app, None) + + self.data = tuple(self.share_group_snapshot._info.values()) + self.columns = tuple(self.share_group_snapshot._info.keys()) + + def test_set_share_group_snapshot_name_description(self): + group_snapshot_name = 'group-snapshot-name-' + uuid.uuid4().hex + group_snapshot_description = ( + 'group-snapshot-description-' + uuid.uuid4().hex + ) + arglist = [ + self.share_group_snapshot.id, + '--name', + group_snapshot_name, + '--description', + group_snapshot_description, + ] + verifylist = [ + ('share_group_snapshot', self.share_group_snapshot.id), + ('name', group_snapshot_name), + ('description', group_snapshot_description), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.update.assert_called_with( + self.share_group_snapshot, + name=parsed_args.name, + description=parsed_args.description, + ) + self.assertIsNone(result) + + def test_set_share_group_snapshot_status(self): + arglist = [self.share_group_snapshot.id, '--status', 'available'] + verifylist = [ + ('share_group_snapshot', self.share_group_snapshot.id), + ('status', 'available'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.reset_state.assert_called_with( + self.share_group_snapshot, 'available' + ) + self.assertIsNone(result) + + def test_set_share_group_snapshot_exception(self): + arglist = [self.share_group_snapshot.id, '--status', 'available'] + verifylist = [ + ('share_group_snapshot', self.share_group_snapshot.id), + ('status', 'available'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.group_snapshot_mocks.reset_state.side_effect = Exception() + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestUnsetShareGroupSnapshot(TestShareGroupSnapshot): + def setUp(self): + super().setUp() + + self.share_group_snapshot = share_fakes.FakeShareGroupSnapshot.create_one_share_group_snapshot() + self.group_snapshot_mocks.get.return_value = self.share_group_snapshot + + self.cmd = share_group_snapshots.UnsetShareGroupSnapshot( + self.app, None + ) + + def test_unset_share_group_snapshot_name_description(self): + arglist = [self.share_group_snapshot.id, '--name', '--description'] + verifylist = [ + ('share_group_snapshot', self.share_group_snapshot.id), + ('name', True), + ('description', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.update.assert_called_with( + self.share_group_snapshot, name='', description='' + ) + self.assertIsNone(result) + + def test_unset_share_group_snapshot_name_exception(self): + arglist = [ + self.share_group_snapshot.id, + '--name', + ] + verifylist = [ + ('share_group_snapshot', self.share_group_snapshot.id), + ('name', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.group_snapshot_mocks.update.side_effect = Exception() + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestListShareGroupSnapshot(TestShareGroupSnapshot): + columns = [ + 'ID', + 'Name', + 'Status', + 'Description', + ] + + def setUp(self): + super().setUp() + + self.share_group = share_fakes.FakeShareGroup.create_one_share_group() + self.groups_mock.get.return_value = self.share_group + + self.share_group_snapshot = ( + share_fakes.FakeShareGroupSnapshot.create_one_share_group_snapshot( + {'share_group_id': self.share_group.id} + ) + ) + + self.share_group_snapshots_list = [self.share_group_snapshot] + self.group_snapshot_mocks.list.return_value = ( + self.share_group_snapshots_list + ) + + self.values = ( + oscutils.get_dict_properties(s._info, self.columns) + for s in self.share_group_snapshots_list + ) + + self.cmd = share_group_snapshots.ListShareGroupSnapshot(self.app, None) + + def test_share_group_snapshot_list_no_options(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.list.assert_called_once_with( + search_opts={ + 'all_tenants': False, + 'name': None, + 'status': None, + 'share_group_id': None, + 'limit': None, + 'offset': None, + } + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.values), list(data)) + + def test_share_group_snapshot_list_detail_all_projects(self): + columns_detail = [ + 'ID', + 'Name', + 'Status', + 'Description', + 'Created At', + 'Share Group ID', + 'Project ID', + ] + + values = ( + oscutils.get_dict_properties(s._info, columns_detail) + for s in self.share_group_snapshots_list + ) + + arglist = [ + '--detailed', + '--all-projects', + ] + + verifylist = [ + ('detailed', True), + ('all_projects', True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.list.assert_called_once_with( + search_opts={ + 'all_tenants': True, + 'name': None, + 'status': None, + 'share_group_id': None, + 'limit': None, + 'offset': None, + } + ) + + self.assertEqual(columns_detail, columns) + self.assertEqual(list(values), list(data)) + + def test_share_group_snapshot_list_search_options(self): + arglist = [ + '--name', + self.share_group_snapshot.name, + '--status', + self.share_group_snapshot.status, + '--share-group', + self.share_group.id, + ] + verifylist = [ + ('name', self.share_group_snapshot.name), + ('status', self.share_group_snapshot.status), + ('share_group', self.share_group.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.groups_mock.get.assert_called_with(self.share_group.id) + self.group_snapshot_mocks.list.assert_called_once_with( + search_opts={ + 'all_tenants': False, + 'name': self.share_group_snapshot.name, + 'status': self.share_group_snapshot.status, + 'share_group_id': self.share_group.id, + 'limit': None, + 'offset': None, + } + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.values), list(data)) + + +class TestListShareGroupSnapshotMembers(TestShareGroupSnapshot): + columns = [ + 'Share ID', + 'Size', + ] + + def setUp(self): + super().setUp() + + self.share = share_fakes.FakeShare.create_one_share() + + self.share_group_snapshot = ( + share_fakes.FakeShareGroupSnapshot.create_one_share_group_snapshot( + { + 'members': [ + {'share_id': self.share.id, 'size': self.share.size} + ] + } + ) + ) + + self.group_snapshot_mocks.get.return_value = self.share_group_snapshot + + self.values = ( + oscutils.get_dict_properties(s, self.columns) + for s in self.share_group_snapshot.members + ) + + self.cmd = share_group_snapshots.ListShareGroupSnapshotMembers( + self.app, None + ) + + def test_share_group_snapshot_list_members(self): + arglist = [self.share_group_snapshot.id] + verifylist = [('share_group_snapshot', self.share_group_snapshot.id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.group_snapshot_mocks.get.assert_called_with( + self.share_group_snapshot.id + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(list(self.values), list(data)) diff --git a/openstackclient/tests/unit/share/v2/test_share_group_type.py b/openstackclient/tests/unit/share/v2/test_share_group_type.py new file mode 100644 index 0000000000..864f87d7a1 --- /dev/null +++ b/openstackclient/tests/unit/share/v2/test_share_group_type.py @@ -0,0 +1,532 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +from unittest import mock + +from manilaclient import api_versions +from manilaclient.common.apiclient.exceptions import BadRequest +from manilaclient.common.apiclient.exceptions import NotFound +from osc_lib import exceptions +from osc_lib import utils as oscutils + +from openstackclient.share import utils +from openstackclient.share.v2 import share_group_types +from openstackclient.tests.unit.share.v2 import fakes as share_fakes +from openstackclient.tests.unit import utils as test_utils + +COLUMNS = [ + 'ID', + 'Name', + 'Share Types', + 'Visibility', + 'Is Default', + 'Group Specs', +] + + +class TestShareGroupType(share_fakes.TestShare): + def setUp(self): + super().setUp() + + self.sgt_mock = self.share_client.share_group_types + self.sgt_mock.reset_mock() + + self.set_share_api_version(api_versions.MAX_VERSION) + + +class TestShareGroupTypeCreate(TestShareGroupType): + def setUp(self): + super().setUp() + + self.share_types = share_fakes.FakeShareType.create_share_types( + count=2 + ) + + formatted_share_types = [] + + for st in self.share_types: + formatted_share_types.append(st.name) + + self.share_group_type = ( + share_fakes.FakeShareGroupType.create_one_share_group_type( + attrs={'share_types': formatted_share_types} + ) + ) + + self.share_group_type_formatted = ( + share_fakes.FakeShareGroupType.create_one_share_group_type( + attrs={ + 'id': self.share_group_type['id'], + 'name': self.share_group_type['name'], + 'share_types': formatted_share_types, + } + ) + ) + + formatted_sgt = utils.format_share_group_type( + self.share_group_type_formatted + ) + + self.sgt_mock.create.return_value = self.share_group_type + self.sgt_mock.get.return_value = self.share_group_type + + # Get the command object to test + self.cmd = share_group_types.CreateShareGroupType(self.app, None) + + self.data = tuple(formatted_sgt.values()) + self.columns = tuple(formatted_sgt.keys()) + + def test_share_group_type_create_required_args(self): + """Verifies required arguments.""" + + arglist = [ + self.share_group_type.name, + self.share_types[0].name, + self.share_types[1].name, + ] + verifylist = [ + ('name', self.share_group_type.name), + ( + 'share_types', + [self.share_types[0].name, self.share_types[1].name], + ), + ] + + with mock.patch( + 'manilaclient.common.apiclient.utils.find_resource', + side_effect=[self.share_types[0], self.share_types[1]], + ): + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.sgt_mock.create.assert_called_with( + group_specs={}, + is_public=True, + name=self.share_group_type.name, + share_types=[ + self.share_types[0].name, + self.share_types[1].name, + ], + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_share_group_type_create_missing_required_arg(self): + """Verifies missing required arguments.""" + + arglist = [ + self.share_group_type.name, + ] + verifylist = [('name', self.share_group_type.name)] + + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_share_group_type_create_private(self): + arglist = [ + self.share_group_type.name, + self.share_types[0].name, + self.share_types[1].name, + '--public', + 'False', + ] + verifylist = [ + ('name', self.share_group_type.name), + ( + 'share_types', + [self.share_types[0].name, self.share_types[1].name], + ), + ('public', 'False'), + ] + + with mock.patch( + 'manilaclient.common.apiclient.utils.find_resource', + side_effect=[self.share_types[0], self.share_types[1]], + ): + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.sgt_mock.create.assert_called_with( + group_specs={}, + is_public=False, + name=self.share_group_type.name, + share_types=[ + self.share_types[0].name, + self.share_types[1].name, + ], + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_share_group_type_create_group_specs(self): + arglist = [ + self.share_group_type.name, + self.share_types[0].name, + self.share_types[1].name, + '--group-specs', + 'consistent_snapshot_support=true', + ] + verifylist = [ + ('name', self.share_group_type.name), + ( + 'share_types', + [self.share_types[0].name, self.share_types[1].name], + ), + ('group_specs', ['consistent_snapshot_support=true']), + ] + + with mock.patch( + 'manilaclient.common.apiclient.utils.find_resource', + side_effect=[self.share_types[0], self.share_types[1]], + ): + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.sgt_mock.create.assert_called_with( + group_specs={'consistent_snapshot_support': 'True'}, + is_public=True, + name=self.share_group_type.name, + share_types=[ + self.share_types[0].name, + self.share_types[1].name, + ], + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_create_share_group_type(self): + arglist = [ + self.share_group_type.name, + self.share_types[0].name, + self.share_types[1].name, + ] + verifylist = [ + ('name', self.share_group_type.name), + ( + 'share_types', + [self.share_types[0].name, self.share_types[1].name], + ), + ] + + with mock.patch( + 'manilaclient.common.apiclient.utils.find_resource', + side_effect=[ + self.share_types[0], + self.share_types[1], + self.share_group_type, + ], + ): + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.sgt_mock.create.assert_called_with( + group_specs={}, + is_public=True, + name=self.share_group_type.name, + share_types=[ + self.share_types[0].name, + self.share_types[1].name, + ], + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + +class TestShareGroupTypeDelete(TestShareGroupType): + def setUp(self): + super().setUp() + + self.share_group_types = ( + share_fakes.FakeShareGroupType.create_share_group_types(count=2) + ) + + self.sgt_mock.delete.return_value = None + self.sgt_mock.get = ( + share_fakes.FakeShareGroupType.get_share_group_types( + self.share_group_types + ) + ) + + # Get the command object to test + self.cmd = share_group_types.DeleteShareGroupType(self.app, None) + + def test_share_group_type_delete_one(self): + arglist = [self.share_group_types[0].name] + + verifylist = [('share_group_types', [self.share_group_types[0].name])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.sgt_mock.delete.assert_called_with(self.share_group_types[0]) + self.assertIsNone(result) + + def test_share_group_type_delete_multiple(self): + arglist = [] + for t in self.share_group_types: + arglist.append(t.name) + verifylist = [ + ('share_group_types', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for t in self.share_group_types: + calls.append(mock.call(t)) + self.sgt_mock.delete.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_share_group_type_with_exception(self): + arglist = [ + 'non_existing_type', + ] + verifylist = [ + ('share_group_types', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.sgt_mock.delete.side_effect = exceptions.CommandError() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + def test_delete_share_group_type(self): + arglist = [self.share_group_types[0].name] + + verifylist = [('share_group_types', [self.share_group_types[0].name])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.sgt_mock.delete.assert_called_with(self.share_group_types[0]) + + self.assertIsNone(result) + + +class TestShareGroupTypeSet(TestShareGroupType): + def setUp(self): + super().setUp() + + self.share_group_type = ( + share_fakes.FakeShareGroupType.create_one_share_group_type( + methods={'set_keys': None, 'update': None} + ) + ) + self.sgt_mock.get.return_value = self.share_group_type + + # Get the command object to test + self.cmd = share_group_types.SetShareGroupType(self.app, None) + + def test_share_group_type_set_group_specs(self): + arglist = [ + self.share_group_type.id, + '--group-specs', + 'consistent_snapshot_support=true', + ] + verifylist = [ + ('share_group_type', self.share_group_type.id), + ('group_specs', ['consistent_snapshot_support=true']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.share_group_type.set_keys.assert_called_with( + {'consistent_snapshot_support': 'True'} + ) + self.assertIsNone(result) + + def test_share_group_type_set_extra_specs_exception(self): + arglist = [ + self.share_group_type.id, + '--group-specs', + 'snapshot_support=true', + ] + verifylist = [ + ('share_group_type', self.share_group_type.id), + ('group_specs', ['snapshot_support=true']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.share_group_type.set_keys.side_effect = BadRequest() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestShareGroupTypeUnset(TestShareGroupType): + def setUp(self): + super().setUp() + + self.share_group_type = ( + share_fakes.FakeShareGroupType.create_one_share_group_type( + methods={'unset_keys': None} + ) + ) + self.sgt_mock.get.return_value = self.share_group_type + + # Get the command object to test + self.cmd = share_group_types.UnsetShareGroupType(self.app, None) + + def test_share_group_type_unset_extra_specs(self): + arglist = [self.share_group_type.id, 'consistent_snapshot_support'] + verifylist = [ + ('share_group_type', self.share_group_type.id), + ('group_specs', ['consistent_snapshot_support']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.share_group_type.unset_keys.assert_called_with( + ['consistent_snapshot_support'] + ) + self.assertIsNone(result) + + def test_share_group_type_unset_exception(self): + arglist = [self.share_group_type.id, 'snapshot_support'] + verifylist = [ + ('share_group_type', self.share_group_type.id), + ('group_specs', ['snapshot_support']), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.share_group_type.unset_keys.side_effect = NotFound() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestShareGroupTypeList(TestShareGroupType): + def setUp(self): + super().setUp() + + self.share_group_types = ( + share_fakes.FakeShareGroupType.create_share_group_types() + ) + + self.sgt_mock.list.return_value = self.share_group_types + + # Get the command object to test + self.cmd = share_group_types.ListShareGroupType(self.app, None) + + self.values = ( + oscutils.get_dict_properties(s._info, COLUMNS) + for s in self.share_group_types + ) + + def test_share_group_type_list_no_options(self): + arglist = [] + verifylist = [('all', False)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.sgt_mock.list.assert_called_once_with( + search_opts={}, show_all=False + ) + self.assertEqual(COLUMNS, columns) + self.assertEqual(list(self.values), list(data)) + + def test_share_group_type_list_all(self): + arglist = [ + '--all', + ] + verifylist = [('all', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.sgt_mock.list.assert_called_once_with( + search_opts={}, show_all=True + ) + self.assertEqual(COLUMNS, columns) + self.assertEqual(list(self.values), list(data)) + + def test_share_group_type_list_group_specs(self): + arglist = ['--group-specs', 'consistent_snapshot_support=true'] + verifylist = [('group_specs', ['consistent_snapshot_support=true'])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.sgt_mock.list.assert_called_once_with( + search_opts={ + 'group_specs': {'consistent_snapshot_support': 'True'} + }, + show_all=False, + ) + self.assertEqual(COLUMNS, columns) + self.assertEqual(list(self.values), list(data)) + + +class TestShareGroupTypeShow(TestShareGroupType): + def setUp(self): + super().setUp() + + self.share_types = share_fakes.FakeShareType.create_share_types( + count=2 + ) + + formatted_share_types = [] + + for st in self.share_types: + formatted_share_types.append(st.name) + + self.share_group_type = ( + share_fakes.FakeShareGroupType.create_one_share_group_type( + attrs={'share_types': formatted_share_types} + ) + ) + + self.share_group_type_formatted = ( + share_fakes.FakeShareGroupType.create_one_share_group_type( + attrs={ + 'id': self.share_group_type['id'], + 'name': self.share_group_type['name'], + 'share_types': formatted_share_types, + } + ) + ) + + formatted_sgt = utils.format_share_group_type( + self.share_group_type_formatted + ) + + self.sgt_mock.get.return_value = self.share_group_type + + # Get the command object to test + self.cmd = share_group_types.ShowShareGroupType(self.app, None) + + self.data = tuple(formatted_sgt.values()) + self.columns = tuple(formatted_sgt.keys()) + + def test_share_group_type_show(self): + arglist = [self.share_group_type.name] + verifylist = [("share_group_type", self.share_group_type.name)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.sgt_mock.get.assert_called_with(self.share_group_type) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) diff --git a/openstackclient/tests/unit/share/v2/test_share_group_type_access.py b/openstackclient/tests/unit/share/v2/test_share_group_type_access.py new file mode 100644 index 0000000000..e665e902b9 --- /dev/null +++ b/openstackclient/tests/unit/share/v2/test_share_group_type_access.py @@ -0,0 +1,191 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from manilaclient.common.apiclient.exceptions import BadRequest +from osc_lib import exceptions + +from openstackclient.share.v2 import share_group_type_access +from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from openstackclient.tests.unit.share.v2 import fakes as share_fakes + + +class TestShareGroupTypeAccess(share_fakes.TestShare): + def setUp(self): + super().setUp() + + self.type_access_mock = self.share_client.share_group_type_access + self.type_access_mock.reset_mock() + + self.share_group_types_mock = self.share_client.share_group_types + self.share_group_types_mock.reset_mock() + + self.projects_mock = self.identity_client.projects + self.projects_mock.reset_mock() + + +class TestShareGroupTypeAccessAllow(TestShareGroupTypeAccess): + def setUp(self): + super().setUp() + + self.project = identity_fakes.FakeProject.create_one_project() + + self.share_group_type = ( + share_fakes.FakeShareGroupType.create_one_share_group_type( + attrs={'is_public': False} + ) + ) + self.share_group_types_mock.get.return_value = self.share_group_type + self.projects_mock.get.return_value = self.project + + self.type_access_mock.add_project_access.return_value = None + + # Get the command object to test + self.cmd = share_group_type_access.ShareGroupTypeAccessAllow( + self.app, None + ) + + def test_share_group_type_access_create(self): + arglist = [self.share_group_type.id, self.project.id] + verifylist = [ + ('share_group_type', self.share_group_type.id), + ('projects', [self.project.id]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.type_access_mock.add_project_access.assert_called_with( + self.share_group_type, self.project.id + ) + + self.assertIsNone(result) + + def test_share_group_type_access_create_invalid_project_exception(self): + arglist = [self.share_group_type.id, 'invalid_project_format'] + verifylist = [ + ('share_group_type', self.share_group_type.id), + ('projects', ['invalid_project_format']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.type_access_mock.add_project_access.side_effect = BadRequest() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestShareGroupTypeAccessList(TestShareGroupTypeAccess): + columns = ['Project ID'] + data = (('',), ('',)) + + def setUp(self): + super().setUp() + + self.type_access_mock.list.return_value = (self.columns, self.data) + + # Get the command object to test + self.cmd = share_group_type_access.ListShareGroupTypeAccess( + self.app, None + ) + + def test_share_group_type_access_list(self): + share_group_type = ( + share_fakes.FakeShareGroupType.create_one_share_group_type( + attrs={'is_public': False} + ) + ) + self.share_group_types_mock.get.return_value = share_group_type + + arglist = [ + share_group_type.id, + ] + verifylist = [('share_group_type', share_group_type.id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.type_access_mock.list.assert_called_once_with(share_group_type) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, tuple(data)) + + def test_share_group_type_access_list_public_type(self): + share_group_type = ( + share_fakes.FakeShareGroupType.create_one_share_group_type( + attrs={'is_public': True} + ) + ) + + self.share_group_types_mock.get.return_value = share_group_type + + arglist = [ + share_group_type.id, + ] + verifylist = [('share_group_type', share_group_type.id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestShareGroupTypeAccessDeny(TestShareGroupTypeAccess): + def setUp(self): + super().setUp() + + self.project = identity_fakes.FakeProject.create_one_project() + + self.share_group_type = ( + share_fakes.FakeShareGroupType.create_one_share_group_type( + attrs={'is_public': False} + ) + ) + self.share_group_types_mock.get.return_value = self.share_group_type + self.projects_mock.get.return_value = self.project + + self.type_access_mock.remove_project_access.return_value = None + + # Get the command object to test + self.cmd = share_group_type_access.ShareGroupTypeAccessDeny( + self.app, None + ) + + def test_share_group_type_access_delete(self): + arglist = [self.share_group_type.id, self.project.id] + verifylist = [ + ('share_group_type', self.share_group_type.id), + ('projects', [self.project.id]), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.type_access_mock.remove_project_access.assert_called_with( + self.share_group_type, self.project.id + ) + + self.assertIsNone(result) + + def test_share_group_type_access_delete_exception(self): + arglist = [self.share_group_type.id, 'invalid_project_format'] + verifylist = [ + ('share_group_type', self.share_group_type.id), + ('projects', ['invalid_project_format']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.type_access_mock.remove_project_access.side_effect = BadRequest() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) diff --git a/openstackclient/tests/unit/share/v2/test_share_groups.py b/openstackclient/tests/unit/share/v2/test_share_groups.py new file mode 100644 index 0000000000..603f63fca5 --- /dev/null +++ b/openstackclient/tests/unit/share/v2/test_share_groups.py @@ -0,0 +1,713 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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 uuid + +from manilaclient import api_versions +from osc_lib import exceptions +from osc_lib import exceptions as osc_exceptions +from osc_lib import utils as oscutils + +from openstackclient.share import utils +from openstackclient.share.v2 import share_groups +from openstackclient.tests.unit.share.v2 import fakes as share_fakes +from openstackclient.tests.unit import utils as test_utils + + +class TestShareGroup(share_fakes.TestShare): + def setUp(self): + super().setUp() + + self.groups_mock = self.share_client.share_groups + self.groups_mock.reset_mock() + + self.share_types_mock = self.share_client.share_types + self.share_types_mock.reset_mock() + + self.set_share_api_version(api_versions.MAX_VERSION) + + +class TestShareGroupCreate(TestShareGroup): + def setUp(self): + super().setUp() + + self.share_group = share_fakes.FakeShareGroup.create_one_share_group() + self.formatted_result = ( + share_fakes.FakeShareGroup.create_one_share_group( + attrs={ + "id": self.share_group.id, + 'created_at': self.share_group.created_at, + "project_id": self.share_group.project_id, + 'share_group_type_id': ( + self.share_group.share_group_type_id + ), + 'share_types': '\n'.join(self.share_group.share_types), + } + ) + ) + + self.groups_mock.create.return_value = self.share_group + self.groups_mock.get.return_value = self.share_group + + self.cmd = share_groups.CreateShareGroup(self.app, None) + + self.data = tuple(self.formatted_result._info.values()) + self.columns = tuple(self.share_group._info.keys()) + + def test_share_group_create_no_args(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.groups_mock.create.assert_called_with( + name=None, + description=None, + share_types=[], + share_group_type=None, + share_network=None, + source_share_group_snapshot=None, + availability_zone=None, + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_share_group_create_with_options(self): + arglist = [ + '--name', + self.share_group.name, + '--description', + self.share_group.description, + ] + verifylist = [ + ('name', self.share_group.name), + ('description', self.share_group.description), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.groups_mock.create.assert_called_with( + name=self.share_group.name, + description=self.share_group.description, + share_types=[], + share_group_type=None, + share_network=None, + source_share_group_snapshot=None, + availability_zone=None, + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_share_group_create_az(self): + arglist = ['--availability-zone', self.share_group.availability_zone] + verifylist = [ + ('availability_zone', self.share_group.availability_zone) + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.groups_mock.create.assert_called_with( + name=None, + description=None, + share_types=[], + share_group_type=None, + share_network=None, + source_share_group_snapshot=None, + availability_zone=self.share_group.availability_zone, + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_share_group_create_share_types(self): + share_types = share_fakes.FakeShareType.create_share_types(count=2) + self.share_types_mock.get = share_fakes.FakeShareType.get_share_types( + share_types + ) + arglist = ['--share-types', share_types[0].id, share_types[1].id] + verifylist = [('share_types', [share_types[0].id, share_types[1].id])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.groups_mock.create.assert_called_with( + name=None, + description=None, + share_types=share_types, + share_group_type=None, + share_network=None, + source_share_group_snapshot=None, + availability_zone=None, + ) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_share_group_create_wait(self): + arglist = ['--wait'] + verifylist = [('wait', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.groups_mock.create.assert_called_with( + name=None, + description=None, + share_types=[], + share_group_type=None, + share_network=None, + source_share_group_snapshot=None, + availability_zone=None, + ) + + self.groups_mock.get.assert_called_with(self.share_group.id) + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + # TODO(archanaserver): Add test cases for share-group-type, + # share-network and source-share-group-snapshot when the + # options have OSC support. + + +class TestShareGroupDelete(TestShareGroup): + def setUp(self): + super().setUp() + + self.share_group = share_fakes.FakeShareGroup.create_one_share_group() + self.groups_mock.get.return_value = self.share_group + + self.cmd = share_groups.DeleteShareGroup(self.app, None) + + def test_share_group_delete(self): + arglist = [self.share_group.id] + verifylist = [('share_group', [self.share_group.id])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.groups_mock.delete.assert_called_with( + self.share_group, force=False + ) + self.assertIsNone(result) + + def test_share_group_delete_force(self): + arglist = [self.share_group.id, '--force'] + verifylist = [('share_group', [self.share_group.id]), ('force', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.groups_mock.delete.assert_called_with( + self.share_group, force=True + ) + self.assertIsNone(result) + + def test_share_group_delete_multiple(self): + share_groups = share_fakes.FakeShareGroup.create_share_groups(count=2) + arglist = [share_groups[0].id, share_groups[1].id] + verifylist = [ + ('share_group', [share_groups[0].id, share_groups[1].id]) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertEqual(self.groups_mock.delete.call_count, len(share_groups)) + self.assertIsNone(result) + + def test_share_group_delete_exception(self): + arglist = [self.share_group.id] + verifylist = [('share_group', [self.share_group.id])] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.groups_mock.delete.side_effect = exceptions.CommandError() + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + def test_share_group_delete_wait(self): + arglist = [self.share_group.id, '--wait'] + verifylist = [('share_group', [self.share_group.id]), ('wait', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('osc_lib.utils.wait_for_delete', return_value=True): + result = self.cmd.take_action(parsed_args) + + self.groups_mock.delete.assert_called_with( + self.share_group, force=False + ) + self.groups_mock.get.assert_called_with(self.share_group.id) + self.assertIsNone(result) + + def test_share_group_delete_wait_exception(self): + arglist = [self.share_group.id, '--wait'] + verifylist = [('share_group', [self.share_group.id]), ('wait', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + with mock.patch('osc_lib.utils.wait_for_delete', return_value=False): + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestShareGroupShow(TestShareGroup): + def setUp(self): + super().setUp() + + self.share_group = share_fakes.FakeShareGroup.create_one_share_group() + self.formatted_result = ( + share_fakes.FakeShareGroup.create_one_share_group( + attrs={ + "id": self.share_group.id, + 'created_at': self.share_group.created_at, + "project_id": self.share_group.project_id, + 'share_group_type_id': ( + self.share_group.share_group_type_id + ), + 'share_types': '\n'.join(self.share_group.share_types), + } + ) + ) + self.groups_mock.get.return_value = self.share_group + + self.data = tuple(self.formatted_result._info.values()) + self.columns = tuple(self.share_group._info.keys()) + + self.cmd = share_groups.ShowShareGroup(self.app, None) + + def test_share_group_show(self): + arglist = [self.share_group.id] + verifylist = [('share_group', self.share_group.id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.groups_mock.get.assert_called_with(self.share_group.id) + + self.assertCountEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + +class TestShareGroupSet(TestShareGroup): + def setUp(self): + super().setUp() + + self.share_group = share_fakes.FakeShareGroup.create_one_share_group() + self.share_group = share_fakes.FakeShare.create_one_share( + methods={"reset_state": None} + ) + self.groups_mock.get.return_value = self.share_group + + self.cmd = share_groups.SetShareGroup(self.app, None) + + def test_set_share_group_name(self): + new_name = uuid.uuid4().hex + arglist = [ + '--name', + new_name, + self.share_group.id, + ] + verifylist = [('name', new_name), ('share_group', self.share_group.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.groups_mock.update.assert_called_with( + self.share_group.id, name=parsed_args.name + ) + + def test_set_share_group_description(self): + new_description = uuid.uuid4().hex + arglist = [ + '--description', + new_description, + self.share_group.id, + ] + verifylist = [ + ('description', new_description), + ('share_group', self.share_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + self.groups_mock.update.assert_called_with( + self.share_group.id, description=parsed_args.description + ) + + def test_share_group_set_status(self): + new_status = 'available' + arglist = [self.share_group.id, '--status', new_status] + verifylist = [ + ('share_group', self.share_group.id), + ('status', new_status), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.share_group.reset_state.assert_called_with(new_status) + self.assertIsNone(result) + + def test_share_group_set_status_exception(self): + new_status = 'available' + arglist = [self.share_group.id, '--status', new_status] + verifylist = [ + ('share_group', self.share_group.id), + ('status', new_status), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.share_group.reset_state.side_effect = Exception() + self.assertRaises( + osc_exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestShareGroupUnset(TestShareGroup): + def setUp(self): + super().setUp() + + self.share_group = share_fakes.FakeShareGroup.create_one_share_group() + self.groups_mock.get.return_value = self.share_group + + self.cmd = share_groups.UnsetShareGroup(self.app, None) + + def test_unset_share_group_name(self): + arglist = [self.share_group.id, '--name'] + verifylist = [('share_group', self.share_group.id), ('name', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.groups_mock.update.assert_called_with(self.share_group, name=None) + self.assertIsNone(result) + + def test_unset_share_group_description(self): + arglist = [self.share_group.id, '--description'] + verifylist = [ + ('share_group', self.share_group.id), + ('description', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.groups_mock.update.assert_called_with( + self.share_group, description=None + ) + self.assertIsNone(result) + + def test_unset_share_group_name_exception(self): + arglist = [ + self.share_group.id, + '--name', + ] + verifylist = [ + ('share_group', self.share_group.id), + ('name', True), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.groups_mock.update.side_effect = Exception() + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestShareGroupList(TestShareGroup): + columns = ['id', 'name', 'status', 'description'] + + column_headers = utils.format_column_headers(columns) + + def setUp(self): + super().setUp() + + self.new_share_group = ( + share_fakes.FakeShareGroup.create_one_share_group() + ) + self.groups_mock.list.return_value = [self.new_share_group] + + self.share_group = share_fakes.FakeShareGroup.create_one_share_group() + self.groups_mock.get.return_value = self.share_group + + self.share_groups_list = ( + share_fakes.FakeShareGroup.create_share_groups(count=2) + ) + self.groups_mock.list.return_value = self.share_groups_list + + self.values = ( + oscutils.get_dict_properties(s._info, self.columns) + for s in self.share_groups_list + ) + + self.cmd = share_groups.ListShareGroup(self.app, None) + + def test_share_group_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.groups_mock.list.assert_called_with( + search_opts={ + 'all_tenants': False, + 'name': None, + 'status': None, + 'share_server_id': None, + 'share_group_type': None, + 'snapshot': None, + 'host': None, + 'share_network': None, + 'project_id': None, + 'limit': None, + 'offset': None, + 'name~': None, + 'description~': None, + 'description': None, + } + ) + + self.assertEqual(self.column_headers, columns) + self.assertEqual(list(self.values), list(data)) + + def test_share_group_list_pre_v236(self): + self.set_share_api_version('2.35') + + arglist = ['--description', 'Description'] + verifylist = [('description', 'Description')] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + def test_share_group_lists_all_projects(self): + all_tenants_list = self.column_headers.copy() + all_tenants_list.append('Project ID') + list_values = ( + oscutils.get_dict_properties(s._info, all_tenants_list) + for s in self.share_groups_list + ) + + arglist = ['--all-projects'] + + verifylist = [('all_projects', True)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.groups_mock.list.assert_called_with( + search_opts={ + 'all_tenants': True, + 'name': None, + 'status': None, + 'share_server_id': None, + 'share_group_type': None, + 'snapshot': None, + 'host': None, + 'share_network': None, + 'project_id': None, + 'limit': None, + 'offset': None, + 'name~': None, + 'description~': None, + 'description': None, + } + ) + + self.assertEqual(all_tenants_list, columns) + self.assertEqual(list(list_values), list(data)) + + def test_share_group_list_name(self): + arglist = ['--name', self.new_share_group.name] + verifylist = [('name', self.new_share_group.name)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'all_tenants': False, + 'name': None, + 'status': None, + 'share_server_id': None, + 'share_group_type': None, + 'snapshot': None, + 'host': None, + 'share_network': None, + 'project_id': None, + 'limit': None, + 'offset': None, + 'name~': None, + 'description~': None, + 'description': None, + } + + search_opts['name'] = self.new_share_group.name + + self.groups_mock.list.assert_called_once_with( + search_opts=search_opts, + ) + + self.assertEqual(self.column_headers, columns) + self.assertEqual(list(self.values), list(data)) + + def test_share_group_list_description(self): + arglist = ['--description', self.new_share_group.description] + verifylist = [('description', self.new_share_group.description)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'all_tenants': False, + 'name': None, + 'status': None, + 'share_server_id': None, + 'share_group_type': None, + 'snapshot': None, + 'host': None, + 'share_network': None, + 'project_id': None, + 'limit': None, + 'offset': None, + 'name~': None, + 'description~': None, + 'description': None, + } + + search_opts['description'] = self.new_share_group.description + + self.groups_mock.list.assert_called_once_with( + search_opts=search_opts, + ) + + self.assertEqual(self.column_headers, columns) + self.assertEqual(list(self.values), list(data)) + + def test_share_group_list_status(self): + arglist = [ + '--status', + self.new_share_group.status, + ] + verifylist = [ + ('status', self.new_share_group.status), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'all_tenants': False, + 'name': None, + 'status': None, + 'share_server_id': None, + 'share_group_type': None, + 'snapshot': None, + 'host': None, + 'share_network': None, + 'project_id': None, + 'limit': None, + 'offset': None, + 'name~': None, + 'description~': None, + 'description': None, + } + + search_opts['status'] = self.new_share_group.status + + self.groups_mock.list.assert_called_once_with( + search_opts=search_opts, + ) + + self.assertEqual(self.column_headers, columns) + self.assertEqual(list(self.values), list(data)) + + def test_share_group_list_marker_and_limit(self): + arglist = [ + "--marker", + self.new_share_group.id, + "--limit", + "2", + ] + verifylist = [ + ('marker', self.new_share_group.id), + ('limit', 2), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + search_opts = { + 'all_tenants': False, + 'name': None, + 'status': None, + 'share_server_id': None, + 'share_group_type': None, + 'snapshot': None, + 'host': None, + 'share_network': None, + 'project_id': None, + 'limit': 2, + 'offset': self.new_share_group.id, + 'name~': None, + 'description~': None, + 'description': None, + } + + self.groups_mock.list.assert_called_once_with( + search_opts=search_opts, + ) + + self.assertEqual(self.column_headers, columns) + self.assertEqual(list(self.values), list(data)) + + def test_share_group_list_negative_limit(self): + arglist = [ + "--limit", + "-2", + ] + verifylist = [ + ("limit", -2), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + # TODO(archanaserver): Add test cases for share-server-id, + # share-group-type, snapshot, share-network and source- + # share-group-share_group when the options have OSC support. diff --git a/openstackclient/tests/unit/test_hacking.py b/openstackclient/tests/unit/test_hacking.py new file mode 100644 index 0000000000..ff22ffb2c9 --- /dev/null +++ b/openstackclient/tests/unit/test_hacking.py @@ -0,0 +1,108 @@ +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +import importlib.util +import os +import re +import subprocess +import sys +import unittest + +import fixtures + +ROOT_DIR = os.path.normpath( + os.path.join(os.path.dirname(__file__), '..', '..', '..') +) +SELFTEST_REGEX = re.compile(r'\b(Okay|[HEW]\d{3}|O\d{3}):\s(.*)') + +# Checks that filter on 'openstackclient/tests/unit' in the filename need the +# temp file written into that path structure so the check is not skipped. +_UNIT_TEST_SUBDIRS = { + 'O401': os.path.join('openstackclient', 'tests', 'unit'), + 'O402': os.path.join('openstackclient', 'tests', 'unit'), +} + + +def _load_checks(): + spec = importlib.util.spec_from_file_location( + '_osc_hacking_checks', + os.path.join(ROOT_DIR, 'hacking', 'checks.py'), + ) + module = importlib.util.module_from_spec(spec) + spec.loader.exec_module(module) + return module + + +def _get_examples(check): + for line in check.__doc__.splitlines(): + line = line.lstrip() + match = SELFTEST_REGEX.match(line) + if match: + yield match.group(1), match.group(2) + + +class HackingTestCase(unittest.TestCase): + def _test_check(self, code, source): + lines = [ + part.replace(r'\t', '\t') + '\n' for part in source.split(r'\n') + ] + subdir = { + 'O401': os.path.join('openstackclient', 'tests', 'unit'), + 'O402': os.path.join('openstackclient', 'tests', 'unit'), + }.get(code, '') + + with fixtures.TempDir() as tmp: + dirpath = os.path.join(tmp.path, subdir) if subdir else tmp.path + if subdir: + os.makedirs(dirpath) + + fpath = os.path.join(dirpath, 'test_tmp.py') + with open(fpath, 'w') as f: + f.write(''.join(lines)) + + cmd = [ + sys.executable, + '-mflake8', + '--config', + os.path.join(ROOT_DIR, 'tox.ini'), + f'--select={code}', + '--format=%(code)s\t%(path)s\t%(row)d', + fpath, + ] + out, _ = subprocess.Popen( + cmd, stdout=subprocess.PIPE, cwd=ROOT_DIR + ).communicate() + out = out.decode('utf-8') + + if code == 'Okay': + self.assertEqual('', out) + else: + self.assertNotEqual('', out, f"Failed to trigger rule {code}") + self.assertEqual(code, out.split('\t')[0].rstrip(':'), out) + + def test_checks(self): + checks_module = _load_checks() + + for name in sorted(dir(checks_module)): + check = getattr(checks_module, name) + if not callable(check): + continue + + if getattr(check, 'skip_on_py3', None) is not False: + continue + + if not check.__doc__: + continue + + for code, source in _get_examples(check): + with self.subTest(check=name, example=source): + self._test_check(code, source) diff --git a/openstackclient/tests/unit/utils.py b/openstackclient/tests/unit/utils.py index 607047f14e..f742c46e78 100644 --- a/openstackclient/tests/unit/utils.py +++ b/openstackclient/tests/unit/utils.py @@ -17,6 +17,7 @@ import io import os +from cliff import columns as cliff_columns import fixtures import testtools @@ -69,6 +70,28 @@ def assertNotCalled(self, m, msg=None): class TestCommand(TestCase): """Test cliff command classes""" + # TODO(slaweq): Remove those methods in favour of the ones in + # osc_lib.test.base.TestCommand once osc-lib TestCommand + # provides all of the same functionality as this TestCommand + # like e.g. the monkey patching for sys.stderr and this one can be + # removed. + + def assertListItemEqual(self, expected, actual): + self.assertEqual(len(expected), len(actual)) + for item_expected, item_actual in zip(expected, actual): + self.assertItemEqual(item_expected, item_actual) + + def assertItemEqual(self, expected, actual): + self.assertEqual(len(expected), len(actual)) + for col_expected, col_actual in zip(expected, actual): + if isinstance(col_expected, cliff_columns.FormattableColumn): + self.assertIsInstance(col_actual, col_expected.__class__) + self.assertEqual( + col_expected.human_readable(), col_actual.human_readable() + ) + else: + self.assertEqual(col_expected, col_actual) + def setUp(self): super().setUp() # Build up a fake app diff --git a/openstackclient/tests/unit/volume/test_find_resource.py b/openstackclient/tests/unit/volume/test_find_resource.py deleted file mode 100644 index df087dd5d3..0000000000 --- a/openstackclient/tests/unit/volume/test_find_resource.py +++ /dev/null @@ -1,85 +0,0 @@ -# Copyright 2013 Nebula Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -# - -from unittest import mock - -from cinderclient.v3 import volume_snapshots -from cinderclient.v3 import volumes -from osc_lib import exceptions -from osc_lib import utils - -from openstackclient.tests.unit import utils as test_utils -from openstackclient.volume import client # noqa - - -# Monkey patch for v1 cinderclient -# NOTE(dtroyer): Do here because openstackclient.volume.client -# doesn't do it until the client object is created now. -volumes.Volume.NAME_ATTR = 'display_name' -volume_snapshots.Snapshot.NAME_ATTR = 'display_name' - - -ID = '1after909' -NAME = 'PhilSpector' - - -class TestFindResourceVolumes(test_utils.TestCase): - def setUp(self): - super().setUp() - api = mock.Mock() - api.client = mock.Mock() - api.client.get = mock.Mock() - resp = mock.Mock() - body = {"volumes": [{"id": ID, 'display_name': NAME}]} - api.client.get.side_effect = [Exception("Not found"), (resp, body)] - self.manager = volumes.VolumeManager(api) - - def test_find(self): - result = utils.find_resource(self.manager, NAME) - self.assertEqual(ID, result.id) - self.assertEqual(NAME, result.display_name) - - def test_not_find(self): - self.assertRaises( - exceptions.CommandError, - utils.find_resource, - self.manager, - 'GeorgeMartin', - ) - - -class TestFindResourceVolumeSnapshots(test_utils.TestCase): - def setUp(self): - super().setUp() - api = mock.Mock() - api.client = mock.Mock() - api.client.get = mock.Mock() - resp = mock.Mock() - body = {"snapshots": [{"id": ID, 'display_name': NAME}]} - api.client.get.side_effect = [Exception("Not found"), (resp, body)] - self.manager = volume_snapshots.SnapshotManager(api) - - def test_find(self): - result = utils.find_resource(self.manager, NAME) - self.assertEqual(ID, result.id) - self.assertEqual(NAME, result.display_name) - - def test_not_find(self): - self.assertRaises( - exceptions.CommandError, - utils.find_resource, - self.manager, - 'GeorgeMartin', - ) diff --git a/openstackclient/tests/unit/volume/v2/fakes.py b/openstackclient/tests/unit/volume/v2/fakes.py index c2303d3880..987d0b240a 100644 --- a/openstackclient/tests/unit/volume/v2/fakes.py +++ b/openstackclient/tests/unit/volume/v2/fakes.py @@ -10,85 +10,24 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -import copy -import random from unittest import mock -import uuid -# FIXME(stephenfin): We are using v3 resource versions despite being v2 fakes -from cinderclient import api_versions from keystoneauth1 import discover -from openstack.block_storage.v2 import _proxy as block_storage_v2_proxy -from openstack.block_storage.v2 import backup as _backup -from openstack.block_storage.v3 import capabilities as _capabilities -from openstack.block_storage.v3 import stats as _stats -from openstack.block_storage.v3 import volume as _volume -from openstack.image.v2 import _proxy as image_v2_proxy +from openstack.block_storage import v2 as block_storage_v2 +from openstack.image import v2 as image_v2 -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit import utils -class FakeVolumeClient: - def __init__(self, **kwargs): - self.auth_token = kwargs['token'] - self.management_url = kwargs['endpoint'] - self.api_version = api_versions.APIVersion('2.0') - - self.availability_zones = mock.Mock() - self.availability_zones.resource_class = fakes.FakeResource(None, {}) - self.backups = mock.Mock() - self.backups.resource_class = fakes.FakeResource(None, {}) - self.capabilities = mock.Mock() - self.capabilities.resource_class = fakes.FakeResource(None, {}) - self.cgsnapshots = mock.Mock() - self.cgsnapshots.resource_class = fakes.FakeResource(None, {}) - self.consistencygroups = mock.Mock() - self.consistencygroups.resource_class = fakes.FakeResource(None, {}) - self.limits = mock.Mock() - self.limits.resource_class = fakes.FakeResource(None, {}) - self.pools = mock.Mock() - self.pools.resource_class = fakes.FakeResource(None, {}) - self.qos_specs = mock.Mock() - self.qos_specs.resource_class = fakes.FakeResource(None, {}) - self.restores = mock.Mock() - self.restores.resource_class = fakes.FakeResource(None, {}) - self.services = mock.Mock() - self.services.resource_class = fakes.FakeResource(None, {}) - self.transfers = mock.Mock() - self.transfers.resource_class = fakes.FakeResource(None, {}) - self.volume_encryption_types = mock.Mock() - self.volume_encryption_types.resource_class = fakes.FakeResource( - None, {} - ) - self.volume_snapshots = mock.Mock() - self.volume_snapshots.resource_class = fakes.FakeResource(None, {}) - self.volume_type_access = mock.Mock() - self.volume_type_access.resource_class = fakes.FakeResource(None, {}) - self.volume_types = mock.Mock() - self.volume_types.resource_class = fakes.FakeResource(None, {}) - self.volumes = mock.Mock() - self.volumes.resource_class = fakes.FakeResource(None, {}) - - class FakeClientMixin: def setUp(self): super().setUp() - self.app.client_manager.volume = FakeVolumeClient( - endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN - ) + self.app.client_manager.volume = mock.Mock(spec=block_storage_v2.Proxy) + self.app.client_manager.volume.api_version = '2' self.volume_client = self.app.client_manager.volume - - # TODO(stephenfin): Rename to 'volume_client' once all commands are - # migrated to SDK - self.app.client_manager.sdk_connection.volume = mock.Mock( - spec=block_storage_v2_proxy.Proxy, - ) - self.volume_sdk_client = self.app.client_manager.sdk_connection.volume self.set_volume_api_version() # default to the lowest def set_volume_api_version(self, version: str | None = None): @@ -100,10 +39,8 @@ def set_volume_api_version(self, version: str | None = None): """ assert version is None - self.volume_client.api_version = None - - self.volume_sdk_client.default_microversion = None - self.volume_sdk_client.get_endpoint_data.return_value = ( + self.volume_client.default_microversion = None + self.volume_client.get_endpoint_data.return_value = ( discover.EndpointData( min_microversion=None, max_microversion=None, @@ -121,855 +58,8 @@ def setUp(self): # avoid circular imports by defining this manually rather than using # openstackclient.tests.unit.image.v2.fakes.FakeClientMixin - self.app.client_manager.image = mock.Mock(spec=image_v2_proxy.Proxy) + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.app.client_manager.image = mock.Mock(spec=image_v2.Proxy) self.image_client = self.app.client_manager.image - - -def create_one_transfer(attrs=None): - """Create a fake transfer. - - :param dict attrs: - A dictionary with all attributes of Transfer Request - :return: - A FakeResource object with volume_id, name, id. - """ - # Set default attribute - transfer_info = { - 'volume_id': 'volume-id-' + uuid.uuid4().hex, - 'name': 'fake_transfer_name', - 'id': 'id-' + uuid.uuid4().hex, - 'links': 'links-' + uuid.uuid4().hex, - } - - # Overwrite default attributes if there are some attributes set - attrs = attrs or {} - - transfer_info.update(attrs) - - transfer = fakes.FakeResource(None, transfer_info, loaded=True) - - return transfer - - -def create_transfers(attrs=None, count=2): - """Create multiple fake transfers. - - :param dict attrs: - A dictionary with all attributes of transfer - :param Integer count: - The number of transfers to be faked - :return: - A list of FakeResource objects - """ - transfers = [] - for n in range(0, count): - transfers.append(create_one_transfer(attrs)) - - return transfers - - -def get_transfers(transfers=None, count=2): - """Get an iterable MagicMock object with a list of faked transfers. - - If transfers list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List transfers: - A list of FakeResource objects faking transfers - :param Integer count: - The number of transfers to be faked - :return - An iterable Mock object with side_effect set to a list of faked - transfers - """ - if transfers is None: - transfers = create_transfers(count) - - return mock.Mock(side_effect=transfers) - - -def create_one_type_access(attrs=None): - """Create a fake volume type access for project. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object, with Volume_type_ID and Project_ID. - """ - if attrs is None: - attrs = {} - - # Set default attributes. - type_access_attrs = { - 'volume_type_id': 'volume-type-id-' + uuid.uuid4().hex, - 'project_id': 'project-id-' + uuid.uuid4().hex, - } - - # Overwrite default attributes. - type_access_attrs.update(attrs) - - type_access = fakes.FakeResource(None, type_access_attrs, loaded=True) - - return type_access - - -def create_one_service(attrs=None): - """Create a fake service. - - :param dict attrs: - A dictionary with all attributes of service - :return: - A FakeResource object with host, status, etc. - """ - # Set default attribute - service_info = { - 'host': 'host_test', - 'binary': 'cinder_test', - 'status': 'enabled', - 'disabled_reason': 'LongHoliday-GoldenWeek', - 'zone': 'fake_zone', - 'updated_at': 'fake_date', - 'state': 'fake_state', - } - - # Overwrite default attributes if there are some attributes set - attrs = attrs or {} - - service_info.update(attrs) - - service = fakes.FakeResource(None, service_info, loaded=True) - - return service - - -def create_services(attrs=None, count=2): - """Create multiple fake services. - - :param dict attrs: - A dictionary with all attributes of service - :param Integer count: - The number of services to be faked - :return: - A list of FakeResource objects - """ - services = [] - for n in range(0, count): - services.append(create_one_service(attrs)) - - return services - - -def create_one_capability(attrs=None): - """Create a fake volume backend capability. - - :param dict attrs: - A dictionary with all attributes of the Capabilities. - :return: - A FakeResource object with capability name and attrs. - """ - # Set default attribute - capability_info = { - "namespace": "OS::Storage::Capabilities::fake", - "vendor_name": "OpenStack", - "volume_backend_name": "lvmdriver-1", - "pool_name": "pool", - "driver_version": "2.0.0", - "storage_protocol": "iSCSI", - "display_name": "Capabilities of Cinder LVM driver", - "description": "Blah, blah.", - "visibility": "public", - "replication_targets": [], - "properties": { - "compression": { - "title": "Compression", - "description": "Enables compression.", - "type": "boolean", - }, - "qos": { - "title": "QoS", - "description": "Enables QoS.", - "type": "boolean", - }, - "replication": { - "title": "Replication", - "description": "Enables replication.", - "type": "boolean", - }, - "thin_provisioning": { - "title": "Thin Provisioning", - "description": "Sets thin provisioning.", - "type": "boolean", - }, - }, - } - - # Overwrite default attributes if there are some attributes set - capability_info.update(attrs or {}) - - capability = _capabilities.Capabilities(**capability_info) - - return capability - - -def create_one_pool(attrs=None): - """Create a fake pool. - - :param dict attrs: - A dictionary with all attributes of the pool - :return: - A FakeResource object with pool name and attrs. - """ - # Set default attribute - pool_info = { - 'name': 'host@lvmdriver-1#lvmdriver-1', - 'capabilities': { - 'storage_protocol': 'iSCSI', - 'thick_provisioning_support': False, - 'thin_provisioning_support': True, - 'total_volumes': 99, - 'total_capacity_gb': 1000.00, - 'allocated_capacity_gb': 100, - 'max_over_subscription_ratio': 200.0, - }, - } - - # Overwrite default attributes if there are some attributes set - pool_info.update(attrs or {}) - - pool = _stats.Pools(**pool_info) - - return pool - - -def create_one_volume(attrs=None): - """Create a fake volume. - - :param dict attrs: - A dictionary with all attributes of volume - :return: - A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - volume_info = { - 'id': 'volume-id' + uuid.uuid4().hex, - 'name': 'volume-name' + uuid.uuid4().hex, - 'description': 'description' + uuid.uuid4().hex, - 'status': random.choice(['available', 'in_use']), - 'size': random.randint(1, 20), - 'volume_type': random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), - 'bootable': random.randint(0, 1), - 'metadata': { - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - }, - 'snapshot_id': random.randint(1, 5), - 'availability_zone': 'zone' + uuid.uuid4().hex, - 'attachments': [ - { - 'device': '/dev/' + uuid.uuid4().hex, - 'server_id': uuid.uuid4().hex, - }, - ], - } - - # Overwrite default attributes if there are some attributes set - volume_info.update(attrs) - - volume = fakes.FakeResource(None, volume_info, loaded=True) - return volume - - -def create_volumes(attrs=None, count=2): - """Create multiple fake volumes. - - :param dict attrs: - A dictionary with all attributes of volume - :param Integer count: - The number of volumes to be faked - :return: - A list of FakeResource objects - """ - volumes = [] - for n in range(0, count): - volumes.append(create_one_volume(attrs)) - - return volumes - - -def create_one_sdk_volume(attrs=None): - """Create a fake volume. - - :param dict attrs: - A dictionary with all attributes of volume - :return: - A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - volume_info = { - 'id': 'volume-id' + uuid.uuid4().hex, - 'name': 'volume-name' + uuid.uuid4().hex, - 'description': 'description' + uuid.uuid4().hex, - 'status': random.choice(['available', 'in_use']), - 'size': random.randint(1, 20), - 'volume_type': random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), - 'bootable': random.choice(['true', 'false']), - 'metadata': { - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - }, - 'snapshot_id': random.randint(1, 5), - 'availability_zone': 'zone' + uuid.uuid4().hex, - 'attachments': [ - { - 'device': '/dev/' + uuid.uuid4().hex, - 'server_id': uuid.uuid4().hex, - }, - ], - } - - # Overwrite default attributes if there are some attributes set - volume_info.update(attrs) - return _volume.Volume(**volume_info) - - -def create_sdk_volumes(attrs=None, count=2): - """Create multiple fake volumes. - - :param dict attrs: - A dictionary with all attributes of volume - :param Integer count: - The number of volumes to be faked - :return: - A list of FakeResource objects - """ - volumes = [] - for n in range(0, count): - volumes.append(create_one_sdk_volume(attrs)) - - return volumes - - -def get_volumes(volumes=None, count=2): - """Get an iterable MagicMock object with a list of faked volumes. - - If volumes list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List volumes: - A list of FakeResource objects faking volumes - :param Integer count: - The number of volumes to be faked - :return - An iterable Mock object with side_effect set to a list of faked - volumes - """ - if volumes is None: - volumes = create_volumes(count) - - return mock.Mock(side_effect=volumes) - - -def create_one_backup(attrs=None): - """Create a fake backup. - - :param dict attrs: - A dictionary with all attributes - :return: A fake - openstack.block_storage.v2.backup.Backup object - """ - attrs = attrs or {} - - # Set default attributes. - backup_info = { - "availability_zone": 'zone' + uuid.uuid4().hex, - "container": 'container-' + uuid.uuid4().hex, - "created_at": 'time-' + uuid.uuid4().hex, - "data_timestamp": 'time-' + uuid.uuid4().hex, - "description": 'description-' + uuid.uuid4().hex, - "fail_reason": "Service not found for creating backup.", - "has_dependent_backups": False, - "id": 'backup-id-' + uuid.uuid4().hex, - "is_incremental": False, - "name": 'backup-name-' + uuid.uuid4().hex, - "object_count": None, - "size": random.randint(1, 20), - "snapshot_id": 'snapshot-id' + uuid.uuid4().hex, - "status": "error", - "updated_at": 'time-' + uuid.uuid4().hex, - "volume_id": 'volume-id-' + uuid.uuid4().hex, - } - - # Overwrite default attributes. - backup_info.update(attrs) - - backup = _backup.Backup(**backup_info) - return backup - - -def create_backups(attrs=None, count=2): - """Create multiple fake backups. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of backups to fake - :return: A list of fake - openstack.block_storage.v2.backup.Backup objects - """ - backups = [] - for i in range(0, count): - backup = create_one_backup(attrs) - backups.append(backup) - - return backups - - -def get_backups(backups=None, count=2): - """Get an iterable MagicMock object with a list of faked backups. - - If backups list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List backups: - A list of FakeResource objects faking backups - :param Integer count: - The number of backups to be faked - :return - An iterable Mock object with side_effect set to a list of faked - backups - """ - if backups is None: - backups = create_backups(count) - - return mock.Mock(side_effect=backups) - - -def create_backup_record(): - """Gets a fake backup record for a given backup. - - :return: An "exported" backup record. - """ - - return { - 'backup_service': 'cinder.backup.drivers.swift.SwiftBackupDriver', - 'backup_url': 'eyJzdGF0dXMiOiAiYXZh', - } - - -def import_backup_record(): - """Creates a fake backup record import response from a backup. - - :return: The fake backup object that was encoded. - """ - return { - 'backup': { - 'id': 'backup.id', - 'name': 'backup.name', - 'links': [ - {'href': 'link1', 'rel': 'self'}, - {'href': 'link2', 'rel': 'bookmark'}, - ], - }, - } - - -def create_one_consistency_group(attrs=None): - """Create a fake consistency group. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attributes. - consistency_group_info = { - "id": 'backup-id-' + uuid.uuid4().hex, - "name": 'backup-name-' + uuid.uuid4().hex, - "description": 'description-' + uuid.uuid4().hex, - "status": "error", - "availability_zone": 'zone' + uuid.uuid4().hex, - "created_at": 'time-' + uuid.uuid4().hex, - "volume_types": ['volume-type1'], - } - - # Overwrite default attributes. - consistency_group_info.update(attrs) - - consistency_group = fakes.FakeResource( - info=copy.deepcopy(consistency_group_info), loaded=True - ) - return consistency_group - - -def create_consistency_groups(attrs=None, count=2): - """Create multiple fake consistency groups. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of consistency groups to fake - :return: - A list of FakeResource objects faking the consistency groups - """ - consistency_groups = [] - for i in range(0, count): - consistency_group = create_one_consistency_group(attrs) - consistency_groups.append(consistency_group) - - return consistency_groups - - -def get_consistency_groups(consistency_groups=None, count=2): - """Note: - - Get an iterable MagicMock object with a list of faked - consistency_groups. - - If consistency_groups list is provided, then initialize - the Mock object with the list. Otherwise create one. - - :param List consistency_groups: - A list of FakeResource objects faking consistency_groups - :param Integer count: - The number of consistency_groups to be faked - :return - An iterable Mock object with side_effect set to a list of faked - consistency_groups - """ - if consistency_groups is None: - consistency_groups = create_consistency_groups(count) - - return mock.Mock(side_effect=consistency_groups) - - -def create_one_consistency_group_snapshot(attrs=None): - """Create a fake consistency group snapshot. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attributes. - consistency_group_snapshot_info = { - "id": 'id-' + uuid.uuid4().hex, - "name": 'backup-name-' + uuid.uuid4().hex, - "description": 'description-' + uuid.uuid4().hex, - "status": "error", - "consistencygroup_id": 'consistency-group-id' + uuid.uuid4().hex, - "created_at": 'time-' + uuid.uuid4().hex, - } - - # Overwrite default attributes. - consistency_group_snapshot_info.update(attrs) - - consistency_group_snapshot = fakes.FakeResource( - info=copy.deepcopy(consistency_group_snapshot_info), loaded=True - ) - return consistency_group_snapshot - - -def create_consistency_group_snapshots(attrs=None, count=2): - """Create multiple fake consistency group snapshots. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of consistency group snapshots to fake - :return: - A list of FakeResource objects faking the - consistency group snapshots - """ - consistency_group_snapshots = [] - for i in range(0, count): - consistency_group_snapshot = create_one_consistency_group_snapshot( - attrs, - ) - consistency_group_snapshots.append(consistency_group_snapshot) - - return consistency_group_snapshots - - -def get_consistency_group_snapshots(snapshots=None, count=2): - """Get an iterable MagicMock object with a list of faked cgsnapshots. - - If consistenct group snapshots list is provided, then initialize - the Mock object with the list. Otherwise create one. - - :param List snapshots: - A list of FakeResource objects faking consistency group snapshots - :param Integer count: - The number of consistency group snapshots to be faked - :return - An iterable Mock object with side_effect set to a list of faked - consistency groups - """ - if snapshots is None: - snapshots = create_consistency_group_snapshots(count) - - return mock.Mock(side_effect=snapshots) - - -def create_one_qos(attrs=None): - """Create a fake Qos specification. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, consumer, etc. - """ - attrs = attrs or {} - - # Set default attributes. - qos_info = { - "id": 'qos-id-' + uuid.uuid4().hex, - "name": 'qos-name-' + uuid.uuid4().hex, - "consumer": 'front-end', - "specs": {"foo": "bar", "iops": "9001"}, - } - - # Overwrite default attributes. - qos_info.update(attrs) - - qos = fakes.FakeResource(info=copy.deepcopy(qos_info), loaded=True) - return qos - - -def create_one_qos_association(attrs=None): - """Create a fake Qos specification association. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, association_type, etc. - """ - attrs = attrs or {} - - # Set default attributes. - qos_association_info = { - "id": 'type-id-' + uuid.uuid4().hex, - "name": 'type-name-' + uuid.uuid4().hex, - "association_type": 'volume_type', - } - - # Overwrite default attributes. - qos_association_info.update(attrs) - - qos_association = fakes.FakeResource( - info=copy.deepcopy(qos_association_info), loaded=True - ) - return qos_association - - -def create_qoses(attrs=None, count=2): - """Create multiple fake Qos specifications. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of Qos specifications to fake - :return: - A list of FakeResource objects faking the Qos specifications - """ - qoses = [] - for i in range(0, count): - qos = create_one_qos(attrs) - qoses.append(qos) - - return qoses - - -def get_qoses(qoses=None, count=2): - """Get an iterable MagicMock object with a list of faked qoses. - - If qoses list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List qoses: - A list of FakeResource objects faking qoses - :param Integer count: - The number of qoses to be faked - :return - An iterable Mock object with side_effect set to a list of faked - qoses - """ - if qoses is None: - qoses = create_qoses(count) - - return mock.Mock(side_effect=qoses) - - -def create_one_snapshot(attrs=None): - """Create a fake snapshot. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attributes. - snapshot_info = { - "id": 'snapshot-id-' + uuid.uuid4().hex, - "name": 'snapshot-name-' + uuid.uuid4().hex, - "description": 'snapshot-description-' + uuid.uuid4().hex, - "size": 10, - "status": "available", - "metadata": {"foo": "bar"}, - "created_at": "2015-06-03T18:49:19.000000", - "volume_id": 'vloume-id-' + uuid.uuid4().hex, - } - - # Overwrite default attributes. - snapshot_info.update(attrs) - - snapshot = fakes.FakeResource( - info=copy.deepcopy(snapshot_info), loaded=True - ) - return snapshot - - -def create_snapshots(attrs=None, count=2): - """Create multiple fake snapshots. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of snapshots to fake - :return: - A list of FakeResource objects faking the snapshots - """ - snapshots = [] - for i in range(0, count): - snapshot = create_one_snapshot(attrs) - snapshots.append(snapshot) - - return snapshots - - -def get_snapshots(snapshots=None, count=2): - """Get an iterable MagicMock object with a list of faked snapshots. - - If snapshots list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List snapshots: - A list of FakeResource objects faking snapshots - :param Integer count: - The number of snapshots to be faked - :return - An iterable Mock object with side_effect set to a list of faked - snapshots - """ - if snapshots is None: - snapshots = create_snapshots(count) - - return mock.Mock(side_effect=snapshots) - - -def create_one_volume_type(attrs=None, methods=None): - """Create a fake volume type. - - :param dict attrs: - A dictionary with all attributes - :param dict methods: - A dictionary with all methods - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - methods = methods or {} - - # Set default attributes. - volume_type_info = { - "id": 'type-id-' + uuid.uuid4().hex, - "name": 'type-name-' + uuid.uuid4().hex, - "description": 'type-description-' + uuid.uuid4().hex, - "extra_specs": {"foo": "bar"}, - "is_public": True, - } - - # Overwrite default attributes. - volume_type_info.update(attrs) - - volume_type = fakes.FakeResource( - info=copy.deepcopy(volume_type_info), methods=methods, loaded=True - ) - return volume_type - - -def create_volume_types(attrs=None, count=2): - """Create multiple fake volume_types. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of types to fake - :return: - A list of FakeResource objects faking the types - """ - volume_types = [] - for i in range(0, count): - volume_type = create_one_volume_type(attrs) - volume_types.append(volume_type) - - return volume_types - - -def get_volume_types(volume_types=None, count=2): - """Get an iterable MagicMock object with a list of faked volume types. - - If volume_types list is provided, then initialize the Mock object with - the list. Otherwise create one. - - :param List volume_types: - A list of FakeResource objects faking volume types - :param Integer count: - The number of volume types to be faked - :return - An iterable Mock object with side_effect set to a list of faked - volume types - """ - if volume_types is None: - volume_types = create_volume_types(count) - - return mock.Mock(side_effect=volume_types) - - -def create_one_encryption_volume_type(attrs=None): - """Create a fake encryption volume type. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with volume_type_id etc. - """ - attrs = attrs or {} - - # Set default attributes. - encryption_info = { - "volume_type_id": 'type-id-' + uuid.uuid4().hex, - 'provider': 'LuksEncryptor', - 'cipher': None, - 'key_size': None, - 'control_location': 'front-end', - } - - # Overwrite default attributes. - encryption_info.update(attrs) - - encryption_type = fakes.FakeResource( - info=copy.deepcopy(encryption_info), loaded=True - ) - return encryption_type diff --git a/openstackclient/tests/unit/volume/v2/test_backup_record.py b/openstackclient/tests/unit/volume/v2/test_backup_record.py index d677b9a284..c86e366fe4 100644 --- a/openstackclient/tests/unit/volume/v2/test_backup_record.py +++ b/openstackclient/tests/unit/volume/v2/test_backup_record.py @@ -10,86 +10,80 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# + +from openstack.block_storage.v3 import backup as _backup +from openstack.test import fakes as sdk_fakes from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import backup_record -class TestBackupRecord(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.backups_mock = self.volume_client.backups - self.backups_mock.reset_mock() - - -class TestBackupRecordExport(TestBackupRecord): - new_backup = volume_fakes.create_one_backup( - attrs={'volume_id': 'a54708a2-0388-4476-a909-09579f885c25'}, +class TestBackupRecordExport(volume_fakes.TestVolume): + fake_backup = sdk_fakes.generate_fake_resource( + _backup.Backup, + volume_id='a54708a2-0388-4476-a909-09579f885c25', ) - new_record = volume_fakes.create_backup_record() + fake_record = { + 'backup-record': { + 'backup_service': 'cinder.backup.drivers.swift.SwiftBackupDriver', + 'backup_url': 'eyJzdGF0dXMiOiAiYXZh', + } + } def setUp(self): super().setUp() - self.backups_mock.export_record.return_value = self.new_record - self.backups_mock.get.return_value = self.new_backup + self.volume_client.find_backup.return_value = self.fake_backup + self.volume_client.export_backup.return_value = self.fake_record - # Get the command object to mock self.cmd = backup_record.ExportBackupRecord(self.app, None) def test_backup_export_table(self): - arglist = [ - self.new_backup.name, - ] - verifylist = [ - ("backup", self.new_backup.name), - ] + arglist = [self.fake_backup.name] + verifylist = [("backup", self.fake_backup.name)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) parsed_args.formatter = 'table' columns, __ = self.cmd.take_action(parsed_args) - self.backups_mock.export_record.assert_called_with( - self.new_backup.id, + self.volume_client.find_backup.assert_called_once_with( + self.fake_backup.name, ignore_missing=False ) - - expected_columns = ('Backup Service', 'Metadata') - self.assertEqual(columns, expected_columns) + self.volume_client.export_backup.assert_called_once_with( + self.fake_backup + ) + self.assertEqual(('Backup Service', 'Metadata'), columns) def test_backup_export_json(self): - arglist = [ - self.new_backup.name, - ] - verifylist = [ - ("backup", self.new_backup.name), - ] + arglist = [self.fake_backup.name] + verifylist = [("backup", self.fake_backup.name)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) parsed_args.formatter = 'json' columns, __ = self.cmd.take_action(parsed_args) - self.backups_mock.export_record.assert_called_with( - self.new_backup.id, + self.volume_client.find_backup.assert_called_once_with( + self.fake_backup.name, ignore_missing=False ) - - expected_columns = ('backup_service', 'backup_url') - self.assertEqual(columns, expected_columns) + self.volume_client.export_backup.assert_called_once_with( + self.fake_backup + ) + self.assertEqual(('backup_service', 'backup_url'), columns) -class TestBackupRecordImport(TestBackupRecord): - new_backup = volume_fakes.create_one_backup( - attrs={'volume_id': 'a54708a2-0388-4476-a909-09579f885c25'}, - ) - new_import = volume_fakes.import_backup_record() +class TestBackupRecordImport(volume_fakes.TestVolume): + fake_import = { + 'backup': { + 'id': 'backup.id', + 'name': 'backup.name', + }, + } def setUp(self): super().setUp() - self.backups_mock.import_record.return_value = self.new_import + self.volume_client.import_backup.return_value = self.fake_import - # Get the command object to mock self.cmd = backup_record.ImportBackupRecord(self.app, None) def test_backup_import(self): @@ -108,8 +102,8 @@ def test_backup_import(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, __ = self.cmd.take_action(parsed_args) - self.backups_mock.import_record.assert_called_with( + self.volume_client.import_backup.assert_called_once_with( "cinder.backup.drivers.swift.SwiftBackupDriver", "fake_backup_record_data", ) - self.assertEqual(columns, ('backup',)) + self.assertEqual(('backup',), columns) diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group.py b/openstackclient/tests/unit/volume/v2/test_consistency_group.py index dc62e5e42f..a39bec16e8 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group.py @@ -13,142 +13,129 @@ # from unittest import mock -from unittest.mock import call +from openstack.block_storage.v2 import consistency_group as _consistency_group +from openstack.block_storage.v2 import ( + consistency_group_snapshot as _consistency_group_snapshot, +) +from openstack.block_storage.v2 import type as _type +from openstack.block_storage.v2 import volume as _volume +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import consistency_group -class TestConsistencyGroup(volume_fakes.TestVolume): +class TestConsistencyGroupAddVolume(volume_fakes.TestVolume): def setUp(self): super().setUp() - # Get a shortcut to the TransferManager Mock - self.consistencygroups_mock = self.volume_client.consistencygroups - self.consistencygroups_mock.reset_mock() - - self.cgsnapshots_mock = self.volume_client.cgsnapshots - self.cgsnapshots_mock.reset_mock() - - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - - self.types_mock = self.volume_client.volume_types - self.types_mock.reset_mock() - - -class TestConsistencyGroupAddVolume(TestConsistencyGroup): - _consistency_group = volume_fakes.create_one_consistency_group() - - def setUp(self): - super().setUp() - - self.consistencygroups_mock.get.return_value = self._consistency_group - # Get the command object to test + self.consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ) + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) self.cmd = consistency_group.AddVolumeToConsistencyGroup( self.app, None ) def test_add_one_volume_to_consistency_group(self): - volume = volume_fakes.create_one_volume() - self.volumes_mock.get.return_value = volume + volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = volume arglist = [ - self._consistency_group.id, + self.consistency_group.id, volume.id, ] verifylist = [ - ('consistency_group', self._consistency_group.id), + ('consistency_group', self.consistency_group.id), ('volumes', [volume.id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'add_volumes': volume.id, - } - self.consistencygroups_mock.update.assert_called_once_with( - self._consistency_group.id, **kwargs + self.volume_client.find_volume.assert_called_once_with( + volume.id, ignore_missing=False + ) + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, add_volumes=volume.id ) self.assertIsNone(result) def test_add_multiple_volumes_to_consistency_group(self): - volumes = volume_fakes.create_volumes(count=2) - self.volumes_mock.get = volume_fakes.get_volumes(volumes) + volumes = [ + sdk_fakes.generate_fake_resource(_volume.Volume), + sdk_fakes.generate_fake_resource(_volume.Volume), + ] + self.volume_client.find_volume.side_effect = volumes arglist = [ - self._consistency_group.id, + self.consistency_group.id, volumes[0].id, volumes[1].id, ] verifylist = [ - ('consistency_group', self._consistency_group.id), + ('consistency_group', self.consistency_group.id), ('volumes', [volumes[0].id, volumes[1].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'add_volumes': volumes[0].id + ',' + volumes[1].id, - } - self.consistencygroups_mock.update.assert_called_once_with( - self._consistency_group.id, **kwargs + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, + add_volumes=volumes[0].id + ',' + volumes[1].id, ) self.assertIsNone(result) @mock.patch.object(consistency_group.LOG, 'error') def test_add_multiple_volumes_to_consistency_group_with_exception( - self, - mock_error, + self, mock_error ): - volume = volume_fakes.create_one_volume() + volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.side_effect = [ + volume, + exceptions.CommandError, + ] arglist = [ - self._consistency_group.id, + self.consistency_group.id, volume.id, 'unexist_volume', ] verifylist = [ - ('consistency_group', self._consistency_group.id), + ('consistency_group', self.consistency_group.id), ('volumes', [volume.id, 'unexist_volume']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) - find_mock_result = [ - volume, - exceptions.CommandError, - self._consistency_group, - ] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - result = self.cmd.take_action(parsed_args) - mock_error.assert_called_with("1 of 2 volumes failed to add.") - self.assertIsNone(result) - find_mock.assert_any_call( - self.consistencygroups_mock, self._consistency_group.id - ) - find_mock.assert_any_call(self.volumes_mock, volume.id) - find_mock.assert_any_call(self.volumes_mock, 'unexist_volume') - self.assertEqual(3, find_mock.call_count) - self.consistencygroups_mock.update.assert_called_once_with( - self._consistency_group.id, add_volumes=volume.id - ) - + mock_error.assert_called_with( + '%(result)s of %(total)s volumes failed to add.', + {'result': 1, 'total': 2}, + ) + self.assertIsNone(result) + self.volume_client.find_volume.assert_any_call( + volume.id, ignore_missing=False + ) + self.volume_client.find_volume.assert_any_call( + 'unexist_volume', ignore_missing=False + ) + self.assertEqual(2, self.volume_client.find_volume.call_count) + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, add_volumes=volume.id + ) -class TestConsistencyGroupCreate(TestConsistencyGroup): - volume_type = volume_fakes.create_one_volume_type() - new_consistency_group = volume_fakes.create_one_consistency_group() - consistency_group_snapshot = ( - volume_fakes.create_one_consistency_group_snapshot() - ) +class TestConsistencyGroupCreate(volume_fakes.TestVolume): columns = ( 'availability_zone', 'created_at', @@ -158,33 +145,39 @@ class TestConsistencyGroupCreate(TestConsistencyGroup): 'status', 'volume_types', ) - data = ( - new_consistency_group.availability_zone, - new_consistency_group.created_at, - new_consistency_group.description, - new_consistency_group.id, - new_consistency_group.name, - new_consistency_group.status, - new_consistency_group.volume_types, - ) def setUp(self): super().setUp() - self.consistencygroups_mock.create.return_value = ( - self.new_consistency_group + + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.new_consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup ) - self.consistencygroups_mock.create_from_src.return_value = ( - self.new_consistency_group + self.consistency_group_snapshot = sdk_fakes.generate_fake_resource( + _consistency_group_snapshot.ConsistencyGroupSnapshot ) - self.consistencygroups_mock.get.return_value = ( + self.volume_client.create_consistency_group.return_value = ( self.new_consistency_group ) - self.types_mock.get.return_value = self.volume_type - self.cgsnapshots_mock.get.return_value = ( + self.volume_client.create_consistency_group_from_source.return_value = self.new_consistency_group + self.volume_client.find_type.return_value = self.volume_type + self.volume_client.find_consistency_group_snapshot.return_value = ( self.consistency_group_snapshot ) + self.volume_client.find_consistency_group.return_value = ( + self.new_consistency_group + ) + + self.data = ( + self.new_consistency_group.availability_zone, + self.new_consistency_group.created_at, + self.new_consistency_group.description, + self.new_consistency_group.id, + self.new_consistency_group.name, + self.new_consistency_group.status, + self.new_consistency_group.volume_types, + ) - # Get the command object to test self.cmd = consistency_group.CreateConsistencyGroup(self.app, None) def test_consistency_group_create(self): @@ -210,10 +203,12 @@ def test_consistency_group_create(self): columns, data = self.cmd.take_action(parsed_args) - self.types_mock.get.assert_called_once_with(self.volume_type.id) - self.consistencygroups_mock.get.assert_not_called() - self.consistencygroups_mock.create.assert_called_once_with( - self.volume_type.id, + self.volume_client.find_type.assert_called_once_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.find_consistency_group.assert_not_called() + self.volume_client.create_consistency_group.assert_called_once_with( + volume_types=self.volume_type.id, name=self.new_consistency_group.name, description=self.new_consistency_group.description, availability_zone=self.new_consistency_group.availability_zone, @@ -243,10 +238,12 @@ def test_consistency_group_create_without_name(self): columns, data = self.cmd.take_action(parsed_args) - self.types_mock.get.assert_called_once_with(self.volume_type.id) - self.consistencygroups_mock.get.assert_not_called() - self.consistencygroups_mock.create.assert_called_once_with( - self.volume_type.id, + self.volume_client.find_type.assert_called_once_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.find_consistency_group.assert_not_called() + self.volume_client.create_consistency_group.assert_called_once_with( + volume_types=self.volume_type.id, name=None, description=self.new_consistency_group.description, availability_zone=self.new_consistency_group.availability_zone, @@ -272,13 +269,13 @@ def test_consistency_group_create_from_source(self): columns, data = self.cmd.take_action(parsed_args) - self.types_mock.get.assert_not_called() - self.consistencygroups_mock.get.assert_called_once_with( - self.new_consistency_group.id + self.volume_client.find_type.assert_not_called() + self.volume_client.find_consistency_group.assert_called_once_with( + self.new_consistency_group.id, ignore_missing=False ) - self.consistencygroups_mock.create_from_src.assert_called_with( - None, - self.new_consistency_group.id, + self.volume_client.create_consistency_group_from_source.assert_called_once_with( + consistency_group_snapshot=None, + consistency_group=self.new_consistency_group.id, name=self.new_consistency_group.name, description=self.new_consistency_group.description, ) @@ -303,13 +300,13 @@ def test_consistency_group_create_from_snapshot(self): columns, data = self.cmd.take_action(parsed_args) - self.types_mock.get.assert_not_called() - self.cgsnapshots_mock.get.assert_called_once_with( - self.consistency_group_snapshot.id + self.volume_client.find_type.assert_not_called() + self.volume_client.find_consistency_group_snapshot.assert_called_once_with( + self.consistency_group_snapshot.id, ignore_missing=False ) - self.consistencygroups_mock.create_from_src.assert_called_with( - self.consistency_group_snapshot.id, - None, + self.volume_client.create_consistency_group_from_source.assert_called_once_with( + consistency_group_snapshot=self.consistency_group_snapshot.id, + consistency_group=None, name=self.new_consistency_group.name, description=self.new_consistency_group.description, ) @@ -318,18 +315,23 @@ def test_consistency_group_create_from_snapshot(self): self.assertCountEqual(self.data, data) -class TestConsistencyGroupDelete(TestConsistencyGroup): - consistency_groups = volume_fakes.create_consistency_groups(count=2) - +class TestConsistencyGroupDelete(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.consistencygroups_mock.get = volume_fakes.get_consistency_groups( - self.consistency_groups, + self.consistency_groups = [ + sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ), + sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ), + ] + self.volume_client.find_consistency_group.side_effect = ( + self.consistency_groups ) - self.consistencygroups_mock.delete.return_value = None + self.volume_client.delete_consistency_group.return_value = None - # Get the command object to mock self.cmd = consistency_group.DeleteConsistencyGroup(self.app, None) def test_consistency_group_delete(self): @@ -339,8 +341,11 @@ def test_consistency_group_delete(self): result = self.cmd.take_action(parsed_args) - self.consistencygroups_mock.delete.assert_called_with( - self.consistency_groups[0].id, False + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_groups[0].id, ignore_missing=False + ) + self.volume_client.delete_consistency_group.assert_called_once_with( + self.consistency_groups[0], force=False ) self.assertIsNone(result) @@ -357,15 +362,16 @@ def test_consistency_group_delete_with_force(self): result = self.cmd.take_action(parsed_args) - self.consistencygroups_mock.delete.assert_called_with( - self.consistency_groups[0].id, True + self.volume_client.delete_consistency_group.assert_called_once_with( + self.consistency_groups[0], force=True ) self.assertIsNone(result) def test_delete_multiple_consistency_groups(self): - arglist = [] - for b in self.consistency_groups: - arglist.append(b.id) + self.volume_client.find_consistency_group.side_effect = ( + self.consistency_groups + ) + arglist = [cg.id for cg in self.consistency_groups] verifylist = [ ('consistency_groups', arglist), ] @@ -373,13 +379,19 @@ def test_delete_multiple_consistency_groups(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - calls = [] - for b in self.consistency_groups: - calls.append(call(b.id, False)) - self.consistencygroups_mock.delete.assert_has_calls(calls) + self.volume_client.delete_consistency_group.assert_any_call( + self.consistency_groups[0], force=False + ) + self.volume_client.delete_consistency_group.assert_any_call( + self.consistency_groups[1], force=False + ) self.assertIsNone(result) def test_delete_multiple_consistency_groups_with_exception(self): + self.volume_client.find_consistency_group.side_effect = [ + self.consistency_groups[0], + exceptions.CommandError, + ] arglist = [ self.consistency_groups[0].id, 'unexist_consistency_group', @@ -390,43 +402,35 @@ def test_delete_multiple_consistency_groups_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [ - self.consistency_groups[0], - exceptions.CommandError, - ] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - '1 of 2 consistency groups failed to delete.', str(e) - ) - - find_mock.assert_any_call( - self.consistencygroups_mock, self.consistency_groups[0].id - ) - find_mock.assert_any_call( - self.consistencygroups_mock, 'unexist_consistency_group' - ) - - self.assertEqual(2, find_mock.call_count) - self.consistencygroups_mock.delete.assert_called_once_with( - self.consistency_groups[0].id, False + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual( + '1 of 2 consistency groups failed to delete.', str(e) ) + self.volume_client.find_consistency_group.assert_any_call( + self.consistency_groups[0].id, ignore_missing=False + ) + self.volume_client.find_consistency_group.assert_any_call( + 'unexist_consistency_group', ignore_missing=False + ) + self.assertEqual( + 2, self.volume_client.find_consistency_group.call_count + ) + self.volume_client.delete_consistency_group.assert_called_once_with( + self.consistency_groups[0], force=False + ) -class TestConsistencyGroupList(TestConsistencyGroup): - consistency_groups = volume_fakes.create_consistency_groups(count=2) - columns = [ +class TestConsistencyGroupList(volume_fakes.TestVolume): + column_headers = [ 'ID', 'Status', 'Name', ] - columns_long = [ + column_headers_long = [ 'ID', 'Status', 'Availability Zone', @@ -434,33 +438,44 @@ class TestConsistencyGroupList(TestConsistencyGroup): 'Description', 'Volume Types', ] - data = [] - for c in consistency_groups: - data.append( - ( - c.id, - c.status, - c.name, - ) - ) - data_long = [] - for c in consistency_groups: - data_long.append( - ( - c.id, - c.status, - c.availability_zone, - c.name, - c.description, - format_columns.ListColumn(c.volume_types), - ) - ) def setUp(self): super().setUp() - self.consistencygroups_mock.list.return_value = self.consistency_groups - # Get the command to test + self.consistency_groups = [ + sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ), + sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ), + ] + self.volume_client.consistency_groups.return_value = ( + self.consistency_groups + ) + + self.data = [] + for c in self.consistency_groups: + self.data.append( + ( + c.id, + c.status, + c.name, + ) + ) + self.data_long = [] + for c in self.consistency_groups: + self.data_long.append( + ( + c.id, + c.status, + c.availability_zone, + c.name, + c.description, + format_columns.ListColumn(c.volume_types), + ) + ) + self.cmd = consistency_group.ListConsistencyGroup(self.app, None) def test_consistency_group_list_without_options(self): @@ -473,10 +488,10 @@ def test_consistency_group_list_without_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.consistencygroups_mock.list.assert_called_once_with( - detailed=True, search_opts={'all_tenants': False} + self.volume_client.consistency_groups.assert_called_once_with( + all_tenants=False, ) - self.assertEqual(self.columns, columns) + self.assertEqual(self.column_headers, columns) self.assertCountEqual(self.data, list(data)) def test_consistency_group_list_with_all_project(self): @@ -489,10 +504,10 @@ def test_consistency_group_list_with_all_project(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.consistencygroups_mock.list.assert_called_once_with( - detailed=True, search_opts={'all_tenants': True} + self.volume_client.consistency_groups.assert_called_once_with( + all_tenants=True, ) - self.assertEqual(self.columns, columns) + self.assertEqual(self.column_headers, columns) self.assertCountEqual(self.data, list(data)) def test_consistency_group_list_with_long(self): @@ -507,71 +522,69 @@ def test_consistency_group_list_with_long(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.consistencygroups_mock.list.assert_called_once_with( - detailed=True, search_opts={'all_tenants': False} + self.volume_client.consistency_groups.assert_called_once_with( + all_tenants=False, ) - self.assertEqual(self.columns_long, columns) + self.assertEqual(self.column_headers_long, columns) self.assertCountEqual(self.data_long, list(data)) -class TestConsistencyGroupRemoveVolume(TestConsistencyGroup): - _consistency_group = volume_fakes.create_one_consistency_group() - +class TestConsistencyGroupRemoveVolume(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.consistencygroups_mock.get.return_value = self._consistency_group - # Get the command object to test + self.consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ) + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) self.cmd = consistency_group.RemoveVolumeFromConsistencyGroup( self.app, None ) def test_remove_one_volume_from_consistency_group(self): - volume = volume_fakes.create_one_volume() - self.volumes_mock.get.return_value = volume + volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = volume arglist = [ - self._consistency_group.id, + self.consistency_group.id, volume.id, ] verifylist = [ - ('consistency_group', self._consistency_group.id), + ('consistency_group', self.consistency_group.id), ('volumes', [volume.id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'remove_volumes': volume.id, - } - self.consistencygroups_mock.update.assert_called_once_with( - self._consistency_group.id, **kwargs + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, remove_volumes=volume.id ) self.assertIsNone(result) def test_remove_multi_volumes_from_consistency_group(self): - volumes = volume_fakes.create_volumes(count=2) - self.volumes_mock.get = volume_fakes.get_volumes(volumes) + volumes = [ + sdk_fakes.generate_fake_resource(_volume.Volume), + sdk_fakes.generate_fake_resource(_volume.Volume), + ] + self.volume_client.find_volume.side_effect = volumes arglist = [ - self._consistency_group.id, + self.consistency_group.id, volumes[0].id, volumes[1].id, ] verifylist = [ - ('consistency_group', self._consistency_group.id), + ('consistency_group', self.consistency_group.id), ('volumes', [volumes[0].id, volumes[1].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'remove_volumes': volumes[0].id + ',' + volumes[1].id, - } - self.consistencygroups_mock.update.assert_called_once_with( - self._consistency_group.id, **kwargs + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, + remove_volumes=volumes[0].id + ',' + volumes[1].id, ) self.assertIsNone(result) @@ -580,49 +593,54 @@ def test_remove_multiple_volumes_from_consistency_group_with_exception( self, mock_error, ): - volume = volume_fakes.create_one_volume() + volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.side_effect = [ + volume, + exceptions.CommandError, + ] arglist = [ - self._consistency_group.id, + self.consistency_group.id, volume.id, 'unexist_volume', ] verifylist = [ - ('consistency_group', self._consistency_group.id), + ('consistency_group', self.consistency_group.id), ('volumes', [volume.id, 'unexist_volume']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) - find_mock_result = [ - volume, - exceptions.CommandError, - self._consistency_group, - ] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - result = self.cmd.take_action(parsed_args) - mock_error.assert_called_with("1 of 2 volumes failed to remove.") - self.assertIsNone(result) - find_mock.assert_any_call( - self.consistencygroups_mock, self._consistency_group.id - ) - find_mock.assert_any_call(self.volumes_mock, volume.id) - find_mock.assert_any_call(self.volumes_mock, 'unexist_volume') - self.assertEqual(3, find_mock.call_count) - self.consistencygroups_mock.update.assert_called_once_with( - self._consistency_group.id, remove_volumes=volume.id - ) - + mock_error.assert_called_with( + '%(result)s of %(total)s volumes failed to remove.', + {'result': 1, 'total': 2}, + ) + self.assertIsNone(result) + self.volume_client.find_volume.assert_any_call( + volume.id, ignore_missing=False + ) + self.volume_client.find_volume.assert_any_call( + 'unexist_volume', ignore_missing=False + ) + self.assertEqual(2, self.volume_client.find_volume.call_count) + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, remove_volumes=volume.id + ) -class TestConsistencyGroupSet(TestConsistencyGroup): - consistency_group = volume_fakes.create_one_consistency_group() +class TestConsistencyGroupSet(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.consistencygroups_mock.get.return_value = self.consistency_group - # Get the command object to test + self.consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ) + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) self.cmd = consistency_group.SetConsistencyGroup(self.app, None) def test_consistency_group_set_name(self): @@ -641,12 +659,11 @@ def test_consistency_group_set_name(self): result = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'name': new_name, - } - self.consistencygroups_mock.update.assert_called_once_with( - self.consistency_group.id, **kwargs + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, name=new_name ) self.assertIsNone(result) @@ -666,17 +683,16 @@ def test_consistency_group_set_description(self): result = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'description': new_description, - } - self.consistencygroups_mock.update.assert_called_once_with( - self.consistency_group.id, **kwargs + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, description=new_description ) self.assertIsNone(result) -class TestConsistencyGroupShow(TestConsistencyGroup): +class TestConsistencyGroupShow(volume_fakes.TestVolume): columns = ( 'availability_zone', 'created_at', @@ -690,7 +706,9 @@ class TestConsistencyGroupShow(TestConsistencyGroup): def setUp(self): super().setUp() - self.consistency_group = volume_fakes.create_one_consistency_group() + self.consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ) self.data = ( self.consistency_group.availability_zone, self.consistency_group.created_at, @@ -700,7 +718,9 @@ def setUp(self): self.consistency_group.status, self.consistency_group.volume_types, ) - self.consistencygroups_mock.get.return_value = self.consistency_group + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) self.cmd = consistency_group.ShowConsistencyGroup(self.app, None) def test_consistency_group_show(self): @@ -708,8 +728,8 @@ def test_consistency_group_show(self): verifylist = [("consistency_group", self.consistency_group.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.consistencygroups_mock.get.assert_called_once_with( - self.consistency_group.id + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py b/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py index 5a5b9c0053..3a4eb2491a 100644 --- a/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py +++ b/openstackclient/tests/unit/volume/v2/test_consistency_group_snapshot.py @@ -14,26 +14,24 @@ from unittest.mock import call +from openstack.block_storage.v2 import consistency_group as _consistency_group +from openstack.block_storage.v2 import ( + consistency_group_snapshot as _cg_snapshot, +) +from openstack.test import fakes as sdk_fakes +from osc_lib import exceptions + from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import consistency_group_snapshot -class TestConsistencyGroupSnapshot(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - # Get a shortcut to the TransferManager Mock - self.cgsnapshots_mock = self.volume_client.cgsnapshots - self.cgsnapshots_mock.reset_mock() - self.consistencygroups_mock = self.volume_client.consistencygroups - self.consistencygroups_mock.reset_mock() - - -class TestConsistencyGroupSnapshotCreate(TestConsistencyGroupSnapshot): - _consistency_group_snapshot = ( - volume_fakes.create_one_consistency_group_snapshot() +class TestConsistencyGroupSnapshotCreate(volume_fakes.TestVolume): + _consistency_group_snapshot = sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot + ) + consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup ) - consistency_group = volume_fakes.create_one_consistency_group() columns = ( 'consistencygroup_id', @@ -54,10 +52,12 @@ class TestConsistencyGroupSnapshotCreate(TestConsistencyGroupSnapshot): def setUp(self): super().setUp() - self.cgsnapshots_mock.create.return_value = ( + self.volume_client.create_consistency_group_snapshot.return_value = ( self._consistency_group_snapshot ) - self.consistencygroups_mock.get.return_value = self.consistency_group + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) # Get the command object to test self.cmd = consistency_group_snapshot.CreateConsistencyGroupSnapshot( @@ -81,11 +81,11 @@ def test_consistency_group_snapshot_create(self): columns, data = self.cmd.take_action(parsed_args) - self.consistencygroups_mock.get.assert_called_once_with( - self.consistency_group.id + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False ) - self.cgsnapshots_mock.create.assert_called_once_with( - self.consistency_group.id, + self.volume_client.create_consistency_group_snapshot.assert_called_once_with( + consistencygroup_id=self.consistency_group.id, name=self._consistency_group_snapshot.name, description=self._consistency_group_snapshot.description, ) @@ -107,11 +107,11 @@ def test_consistency_group_snapshot_create_no_consistency_group(self): columns, data = self.cmd.take_action(parsed_args) - self.consistencygroups_mock.get.assert_called_once_with( - self._consistency_group_snapshot.name + self.volume_client.find_consistency_group.assert_called_once_with( + self._consistency_group_snapshot.name, ignore_missing=False ) - self.cgsnapshots_mock.create.assert_called_once_with( - self.consistency_group.id, + self.volume_client.create_consistency_group_snapshot.assert_called_once_with( + consistencygroup_id=self.consistency_group.id, name=self._consistency_group_snapshot.name, description=self._consistency_group_snapshot.description, ) @@ -120,20 +120,25 @@ def test_consistency_group_snapshot_create_no_consistency_group(self): self.assertEqual(self.data, data) -class TestConsistencyGroupSnapshotDelete(TestConsistencyGroupSnapshot): - consistency_group_snapshots = ( - volume_fakes.create_consistency_group_snapshots(count=2) - ) +class TestConsistencyGroupSnapshotDelete(volume_fakes.TestVolume): + consistency_group_snapshots = [ + sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot + ), + sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot + ), + ] def setUp(self): super().setUp() - self.cgsnapshots_mock.get = ( - volume_fakes.get_consistency_group_snapshots( - self.consistency_group_snapshots - ) + self.volume_client.find_consistency_group_snapshot.side_effect = ( + self.consistency_group_snapshots + ) + self.volume_client.delete_consistency_group_snapshot.return_value = ( + None ) - self.cgsnapshots_mock.delete.return_value = None # Get the command object to mock self.cmd = consistency_group_snapshot.DeleteConsistencyGroupSnapshot( @@ -152,8 +157,11 @@ def test_consistency_group_snapshot_delete(self): result = self.cmd.take_action(parsed_args) - self.cgsnapshots_mock.delete.assert_called_once_with( - self.consistency_group_snapshots[0].id + self.volume_client.find_consistency_group_snapshot.assert_called_once_with( + self.consistency_group_snapshots[0].id, ignore_missing=False + ) + self.volume_client.delete_consistency_group_snapshot.assert_called_once_with( + self.consistency_group_snapshots[0] ) self.assertIsNone(result) @@ -168,25 +176,52 @@ def test_multiple_consistency_group_snapshots_delete(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - calls = [] + find_calls = [] + delete_calls = [] for c in self.consistency_group_snapshots: - calls.append(call(c.id)) - self.cgsnapshots_mock.delete.assert_has_calls(calls) + find_calls.append(call(c.id, ignore_missing=False)) + delete_calls.append(call(c)) + self.volume_client.find_consistency_group_snapshot.assert_has_calls( + find_calls + ) + self.volume_client.delete_consistency_group_snapshot.assert_has_calls( + delete_calls + ) self.assertIsNone(result) + def test_delete_with_exception(self): + arglist = ['missing-snapshot'] + verifylist = [('consistency_group_snapshot', ['missing-snapshot'])] + + self.volume_client.find_consistency_group_snapshot.side_effect = ( + exceptions.CommandError + ) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) -class TestConsistencyGroupSnapshotList(TestConsistencyGroupSnapshot): - consistency_group_snapshots = ( - volume_fakes.create_consistency_group_snapshots(count=2) + +class TestConsistencyGroupSnapshotList(volume_fakes.TestVolume): + consistency_group_snapshots = [ + sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot, status='available' + ), + sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot, status='available' + ), + ] + consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup ) - consistency_group = volume_fakes.create_one_consistency_group() - columns = [ + column_headers = [ 'ID', 'Status', 'Name', ] - columns_long = [ + column_headers_long = [ 'ID', 'Status', 'ConsistencyGroup ID', @@ -219,10 +254,12 @@ class TestConsistencyGroupSnapshotList(TestConsistencyGroupSnapshot): def setUp(self): super().setUp() - self.cgsnapshots_mock.list.return_value = ( + self.volume_client.consistency_group_snapshots.return_value = ( self.consistency_group_snapshots ) - self.consistencygroups_mock.get.return_value = self.consistency_group + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) # Get the command to test self.cmd = consistency_group_snapshot.ListConsistencyGroupSnapshot( self.app, None @@ -240,15 +277,12 @@ def test_consistency_group_snapshot_list_without_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'status': None, - 'consistencygroup_id': None, - } - self.cgsnapshots_mock.list.assert_called_once_with( - detailed=True, search_opts=search_opts + self.volume_client.consistency_group_snapshots.assert_called_once_with( + all_tenants=False, + status=None, + consistencygroup_id=None, ) - self.assertEqual(self.columns, columns) + self.assertEqual(self.column_headers, columns) self.assertEqual(self.data, list(data)) def test_consistency_group_snapshot_list_with_long(self): @@ -265,15 +299,12 @@ def test_consistency_group_snapshot_list_with_long(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'status': None, - 'consistencygroup_id': None, - } - self.cgsnapshots_mock.list.assert_called_once_with( - detailed=True, search_opts=search_opts + self.volume_client.consistency_group_snapshots.assert_called_once_with( + all_tenants=False, + status=None, + consistencygroup_id=None, ) - self.assertEqual(self.columns_long, columns) + self.assertEqual(self.column_headers_long, columns) self.assertEqual(self.data_long, list(data)) def test_consistency_group_snapshot_list_with_options(self): @@ -294,24 +325,21 @@ def test_consistency_group_snapshot_list_with_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': True, - 'status': self.consistency_group_snapshots[0].status, - 'consistencygroup_id': self.consistency_group.id, - } - self.consistencygroups_mock.get.assert_called_once_with( - self.consistency_group.id + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False ) - self.cgsnapshots_mock.list.assert_called_once_with( - detailed=True, search_opts=search_opts + self.volume_client.consistency_group_snapshots.assert_called_once_with( + all_tenants=True, + status=self.consistency_group_snapshots[0].status, + consistencygroup_id=self.consistency_group.id, ) - self.assertEqual(self.columns, columns) + self.assertEqual(self.column_headers, columns) self.assertEqual(self.data, list(data)) -class TestConsistencyGroupSnapshotShow(TestConsistencyGroupSnapshot): - _consistency_group_snapshot = ( - volume_fakes.create_one_consistency_group_snapshot() +class TestConsistencyGroupSnapshotShow(volume_fakes.TestVolume): + _consistency_group_snapshot = sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot ) columns = ( @@ -334,7 +362,7 @@ class TestConsistencyGroupSnapshotShow(TestConsistencyGroupSnapshot): def setUp(self): super().setUp() - self.cgsnapshots_mock.get.return_value = ( + self.volume_client.find_consistency_group_snapshot.return_value = ( self._consistency_group_snapshot ) self.cmd = consistency_group_snapshot.ShowConsistencyGroupSnapshot( @@ -348,8 +376,8 @@ def test_consistency_group_snapshot_show(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.cgsnapshots_mock.get.assert_called_once_with( - self._consistency_group_snapshot.id + self.volume_client.find_consistency_group_snapshot.assert_called_once_with( + self._consistency_group_snapshot.id, ignore_missing=False ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_qos_specs.py b/openstackclient/tests/unit/volume/v2/test_qos_specs.py index 50c7419f1e..2113e0c5cb 100644 --- a/openstackclient/tests/unit/volume/v2/test_qos_specs.py +++ b/openstackclient/tests/unit/volume/v2/test_qos_specs.py @@ -11,41 +11,32 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -import copy -from unittest import mock from unittest.mock import call +from openstack.block_storage.v2 import qos_spec as _qos_spec +from openstack.block_storage.v2 import type as _type +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import qos_specs -class TestQos(volume_fakes.TestVolume): +class TestQosAssociate(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.qos_mock = self.volume_client.qos_specs - self.qos_mock.reset_mock() - - self.types_mock = self.volume_client.volume_types - self.types_mock.reset_mock() - - -class TestQosAssociate(TestQos): - volume_type = volume_fakes.create_one_volume_type() - qos_spec = volume_fakes.create_one_qos() - - def setUp(self): - super().setUp() + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ) - self.qos_mock.get.return_value = self.qos_spec - self.types_mock.get.return_value = self.volume_type - # Get the command object to test + self.volume_client.find_qos_spec.return_value = self.qos_spec + self.volume_client.find_type.return_value = self.volume_type self.cmd = qos_specs.AssociateQos(self.app, None) def test_qos_associate(self): @@ -58,20 +49,30 @@ def test_qos_associate(self): result = self.cmd.take_action(parsed_args) - self.qos_mock.associate.assert_called_with( + self.volume_client.find_qos_spec.assert_called_once_with( + self.qos_spec.id, ignore_missing=False + ) + self.volume_client.find_type.assert_called_once_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.associate_qos_spec.assert_called_once_with( self.qos_spec.id, self.volume_type.id ) self.assertIsNone(result) -class TestQosCreate(TestQos): +class TestQosCreate(volume_fakes.TestVolume): columns = ('consumer', 'id', 'name', 'properties') def setUp(self): super().setUp() - self.new_qos_spec = volume_fakes.create_one_qos() - self.qos_mock.create.return_value = self.new_qos_spec + self.new_qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ) + self.volume_client.create_qos_spec.return_value = self.new_qos_spec self.data = ( self.new_qos_spec.consumer, @@ -80,7 +81,6 @@ def setUp(self): format_columns.DictColumn(self.new_qos_spec.specs), ) - # Get the command object to test self.cmd = qos_specs.CreateQos(self.app, None) def test_qos_create_without_properties(self): @@ -94,8 +94,8 @@ def test_qos_create_without_properties(self): columns, data = self.cmd.take_action(parsed_args) - self.qos_mock.create.assert_called_with( - self.new_qos_spec.name, {'consumer': 'both'} + self.volume_client.create_qos_spec.assert_called_once_with( + name=self.new_qos_spec.name, consumer='both' ) self.assertEqual(self.columns, columns) @@ -115,8 +115,9 @@ def test_qos_create_with_consumer(self): columns, data = self.cmd.take_action(parsed_args) - self.qos_mock.create.assert_called_with( - self.new_qos_spec.name, {'consumer': self.new_qos_spec.consumer} + self.volume_client.create_qos_spec.assert_called_once_with( + name=self.new_qos_spec.name, + consumer=self.new_qos_spec.consumer, ) self.assertEqual(self.columns, columns) @@ -134,34 +135,36 @@ def test_qos_create_with_properties(self): ] verifylist = [ ('consumer', self.new_qos_spec.consumer), - ('property', self.new_qos_spec.specs), + ('properties', self.new_qos_spec.specs), ('name', self.new_qos_spec.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.qos_mock.create.assert_called_with( - self.new_qos_spec.name, - { - 'consumer': self.new_qos_spec.consumer, - 'foo': 'bar', - 'iops': '9001', - }, + self.volume_client.create_qos_spec.assert_called_once_with( + name=self.new_qos_spec.name, + consumer=self.new_qos_spec.consumer, + foo='bar', + iops='9001', ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) -class TestQosDelete(TestQos): - qos_specs = volume_fakes.create_qoses(count=2) - +class TestQosDelete(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.qos_mock.get = volume_fakes.get_qoses(self.qos_specs) - # Get the command object to test + self.qos_specs = [ + sdk_fakes.generate_fake_resource(_qos_spec.QoSSpec, specs={}), + sdk_fakes.generate_fake_resource(_qos_spec.QoSSpec, specs={}), + ] + qos_by_id = {q.id: q for q in self.qos_specs} + self.volume_client.find_qos_spec.side_effect = lambda x, **kwargs: ( + qos_by_id[x] + ) self.cmd = qos_specs.DeleteQos(self.app, None) def test_qos_delete(self): @@ -171,7 +174,9 @@ def test_qos_delete(self): result = self.cmd.take_action(parsed_args) - self.qos_mock.delete.assert_called_with(self.qos_specs[0].id, False) + self.volume_client.delete_qos_spec.assert_called_once_with( + self.qos_specs[0].id, ignore_missing=False, force=False + ) self.assertIsNone(result) def test_qos_delete_with_force(self): @@ -181,7 +186,9 @@ def test_qos_delete_with_force(self): result = self.cmd.take_action(parsed_args) - self.qos_mock.delete.assert_called_with(self.qos_specs[0].id, True) + self.volume_client.delete_qos_spec.assert_called_once_with( + self.qos_specs[0].id, ignore_missing=False, force=True + ) self.assertIsNone(result) def test_delete_multiple_qoses(self): @@ -197,8 +204,8 @@ def test_delete_multiple_qoses(self): calls = [] for q in self.qos_specs: - calls.append(call(q.id, False)) - self.qos_mock.delete.assert_has_calls(calls) + calls.append(call(q.id, ignore_missing=False, force=False)) + self.volume_client.delete_qos_spec.assert_has_calls(calls) self.assertIsNone(result) def test_delete_multiple_qoses_with_exception(self): @@ -212,37 +219,30 @@ def test_delete_multiple_qoses_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.qos_specs[0], exceptions.CommandError] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - '1 of 2 QoS specifications failed to delete.', str(e) - ) - - find_mock.assert_any_call(self.qos_mock, self.qos_specs[0].id) - find_mock.assert_any_call(self.qos_mock, 'unexist_qos') - - self.assertEqual(2, find_mock.call_count) - self.qos_mock.delete.assert_called_once_with( - self.qos_specs[0].id, False + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual( + '1 of 2 QoS specifications failed to delete.', str(e) ) + self.volume_client.delete_qos_spec.assert_called_once_with( + self.qos_specs[0].id, ignore_missing=False, force=False + ) -class TestQosDisassociate(TestQos): - volume_type = volume_fakes.create_one_volume_type() - qos_spec = volume_fakes.create_one_qos() +class TestQosDisassociate(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.qos_mock.get.return_value = self.qos_spec - self.types_mock.get.return_value = self.volume_type - # Get the command object to test + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, consumer='front-end', specs={} + ) + + self.volume_client.find_qos_spec.return_value = self.qos_spec + self.volume_client.find_type.return_value = self.volume_type self.cmd = qos_specs.DisassociateQos(self.app, None) def test_qos_disassociate_with_volume_type(self): @@ -259,7 +259,7 @@ def test_qos_disassociate_with_volume_type(self): result = self.cmd.take_action(parsed_args) - self.qos_mock.disassociate.assert_called_with( + self.volume_client.disassociate_qos_spec.assert_called_once_with( self.qos_spec.id, self.volume_type.id ) self.assertIsNone(result) @@ -274,14 +274,13 @@ def test_qos_disassociate_with_all_volume_types(self): result = self.cmd.take_action(parsed_args) - self.qos_mock.disassociate_all.assert_called_with(self.qos_spec.id) + self.volume_client.disassociate_all_qos_spec.assert_called_once_with( + self.qos_spec.id + ) self.assertIsNone(result) -class TestQosList(TestQos): - qos_specs = volume_fakes.create_qoses(count=2) - qos_association = volume_fakes.create_one_qos_association() - +class TestQosList(volume_fakes.TestVolume): columns = ( 'ID', 'Name', @@ -289,25 +288,42 @@ class TestQosList(TestQos): 'Associations', 'Properties', ) - data = [] - for q in qos_specs: - data.append( - ( - q.id, - q.name, - q.consumer, - format_columns.ListColumn([qos_association.name]), - format_columns.DictColumn(q.specs), - ) - ) def setUp(self): super().setUp() - self.qos_mock.list.return_value = self.qos_specs - self.qos_mock.get_associations.return_value = [self.qos_association] + self.qos_specs = [ + sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ), + sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ), + ] + self.qos_association = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpecAssociation + ) + self.volume_client.qos_specs.return_value = self.qos_specs + self.volume_client.qos_spec_associations.return_value = [ + self.qos_association + ] + + self.data = [] + for q in self.qos_specs: + self.data.append( + ( + q.id, + q.name, + q.consumer, + format_columns.ListColumn([self.qos_association.name]), + format_columns.DictColumn(q.specs), + ) + ) - # Get the command object to test self.cmd = qos_specs.ListQos(self.app, None) def test_qos_list(self): @@ -317,47 +333,23 @@ def test_qos_list(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.qos_mock.list.assert_called_with() + self.volume_client.qos_specs.assert_called_once_with() self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) - def test_qos_list_no_association(self): - self.qos_mock.reset_mock() - self.qos_mock.get_associations.side_effect = [ - [self.qos_association], - exceptions.NotFound("NotFound"), - ] - - arglist = [] - verifylist = [] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - self.qos_mock.list.assert_called_with() - - self.assertEqual(self.columns, columns) - - ex_data = copy.deepcopy(self.data) - ex_data[1] = ( - self.qos_specs[1].id, - self.qos_specs[1].name, - self.qos_specs[1].consumer, - format_columns.ListColumn(None), - format_columns.DictColumn(self.qos_specs[1].specs), - ) - self.assertCountEqual(ex_data, list(data)) - - -class TestQosSet(TestQos): - qos_spec = volume_fakes.create_one_qos() +class TestQosSet(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.qos_mock.get.return_value = self.qos_spec - # Get the command object to test + self.qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ) + + self.volume_client.find_qos_spec.return_value = self.qos_spec self.cmd = qos_specs.SetQos(self.app, None) def test_qos_set_with_properties_with_id(self): @@ -372,43 +364,50 @@ def test_qos_set_with_properties_with_id(self): new_property = {"a": "b", "c": "d"} verifylist = [ ('no_property', True), - ('property', new_property), + ('properties', new_property), ('qos_spec', self.qos_spec.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.qos_mock.unset_keys.assert_called_with( + self.volume_client.delete_qos_spec_metadata.assert_called_once_with( self.qos_spec.id, list(self.qos_spec.specs.keys()), ) - self.qos_mock.set_keys.assert_called_with( - self.qos_spec.id, {"a": "b", "c": "d"} + self.volume_client.update_qos_spec.assert_called_once_with( + self.qos_spec.id, a='b', c='d' ) self.assertIsNone(result) -class TestQosShow(TestQos): - qos_spec = volume_fakes.create_one_qos() - qos_association = volume_fakes.create_one_qos_association() - +class TestQosShow(volume_fakes.TestVolume): columns = ('associations', 'consumer', 'id', 'name', 'properties') - data = ( - format_columns.ListColumn([qos_association.name]), - qos_spec.consumer, - qos_spec.id, - qos_spec.name, - format_columns.DictColumn(qos_spec.specs), - ) def setUp(self): super().setUp() - self.qos_mock.get.return_value = self.qos_spec - self.qos_mock.get_associations.return_value = [self.qos_association] + self.qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ) + self.qos_association = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpecAssociation + ) + self.volume_client.find_qos_spec.return_value = self.qos_spec + self.volume_client.qos_spec_associations.return_value = [ + self.qos_association + ] + + self.data = ( + format_columns.ListColumn([self.qos_association.name]), + self.qos_spec.consumer, + self.qos_spec.id, + self.qos_spec.name, + format_columns.DictColumn(self.qos_spec.specs), + ) - # Get the command object to test self.cmd = qos_specs.ShowQos(self.app, None) def test_qos_show(self): @@ -418,20 +417,22 @@ def test_qos_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.qos_mock.get.assert_called_with(self.qos_spec.id) + self.volume_client.find_qos_spec.assert_called_once_with( + self.qos_spec.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, tuple(data)) -class TestQosUnset(TestQos): - qos_spec = volume_fakes.create_one_qos() - +class TestQosUnset(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.qos_mock.get.return_value = self.qos_spec - # Get the command object to test + self.qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, consumer='front-end', specs={} + ) + self.volume_client.find_qos_spec.return_value = self.qos_spec self.cmd = qos_specs.UnsetQos(self.app, None) def test_qos_unset_with_properties(self): @@ -443,14 +444,14 @@ def test_qos_unset_with_properties(self): self.qos_spec.id, ] verifylist = [ - ('property', ['iops', 'foo']), + ('properties', ['iops', 'foo']), ('qos_spec', self.qos_spec.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.qos_mock.unset_keys.assert_called_with( + self.volume_client.delete_qos_spec_metadata.assert_called_once_with( self.qos_spec.id, ['iops', 'foo'] ) self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v2/test_service.py b/openstackclient/tests/unit/volume/v2/test_service.py index a553985647..836c67c398 100644 --- a/openstackclient/tests/unit/volume/v2/test_service.py +++ b/openstackclient/tests/unit/volume/v2/test_service.py @@ -12,108 +12,83 @@ # under the License. # +from unittest import mock + +from openstack.block_storage.v2 import service as _service +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import service -class TestService(volume_fakes.TestVolume): +class TestServiceList(volume_fakes.TestVolume): def setUp(self): super().setUp() - # Get a shortcut to the ServiceManager Mock - self.service_mock = self.volume_client.services - self.service_mock.reset_mock() - - -class TestServiceList(TestService): - # The service to be listed - services = volume_fakes.create_one_service() + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.volume_client.services.return_value = [self.service] - def setUp(self): - super().setUp() - - self.service_mock.list.return_value = [self.services] - - # Get the command object to test self.cmd = service.ListService(self.app, None) def test_service_list(self): arglist = [ '--host', - self.services.host, + self.service.host, '--service', - self.services.binary, + self.service.binary, ] verifylist = [ - ('host', self.services.host), - ('service', self.services.binary), + ('host', self.service.host), + ('service', self.service.binary), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - expected_columns = [ + expected_columns = ( 'Binary', 'Host', 'Zone', 'Status', 'State', 'Updated At', - ] - - # confirming if all expected columns are present in the result. - self.assertEqual(expected_columns, columns) - + ) datalist = ( ( - self.services.binary, - self.services.host, - self.services.zone, - self.services.status, - self.services.state, - self.services.updated_at, + self.service.binary, + self.service.host, + self.service.availability_zone, + self.service.status, + self.service.state, + self.service.updated_at, ), ) - - # confirming if all expected values are present in the result. + self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - - # checking if proper call was made to list services - self.service_mock.list.assert_called_with( - self.services.host, - self.services.binary, + self.volume_client.services.assert_called_with( + host=self.service.host, + binary=self.service.binary, ) - # checking if prohibited columns are present in output - self.assertNotIn("Disabled Reason", columns) - self.assertNotIn(self.services.disabled_reason, tuple(data)) - def test_service_list_with_long_option(self): arglist = [ '--host', - self.services.host, + self.service.host, '--service', - self.services.binary, + self.service.binary, '--long', ] verifylist = [ - ('host', self.services.host), - ('service', self.services.binary), + ('host', self.service.host), + ('service', self.service.binary), ('long', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - expected_columns = [ + expected_columns = ( 'Binary', 'Host', 'Zone', @@ -121,41 +96,34 @@ def test_service_list_with_long_option(self): 'State', 'Updated At', 'Disabled Reason', - ] - - # confirming if all expected columns are present in the result. - self.assertEqual(expected_columns, columns) - + ) datalist = ( ( - self.services.binary, - self.services.host, - self.services.zone, - self.services.status, - self.services.state, - self.services.updated_at, - self.services.disabled_reason, + self.service.binary, + self.service.host, + self.service.availability_zone, + self.service.status, + self.service.state, + self.service.updated_at, + self.service.disabled_reason, ), ) - - # confirming if all expected values are present in the result. + self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - - self.service_mock.list.assert_called_with( - self.services.host, - self.services.binary, + self.volume_client.services.assert_called_with( + host=self.service.host, + binary=self.service.binary, ) -class TestServiceSet(TestService): - service = volume_fakes.create_one_service() - +class TestServiceSet(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.service_mock.enable.return_value = self.service - self.service_mock.disable.return_value = self.service - self.service_mock.disable_log_reason.return_value = self.service + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.service.enable = mock.Mock(autospec=True) + self.service.disable = mock.Mock(autospec=True) + self.volume_client.find_service.return_value = self.service self.cmd = service.SetService(self.app, None) @@ -171,9 +139,8 @@ def test_service_set_nothing(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.service_mock.enable.assert_not_called() - self.service_mock.disable.assert_not_called() - self.service_mock.disable_log_reason.assert_not_called() + self.service.enable.assert_not_called() + self.service.disable.assert_not_called() self.assertIsNone(result) def test_service_set_enable(self): @@ -191,11 +158,8 @@ def test_service_set_enable(self): result = self.cmd.take_action(parsed_args) - self.service_mock.enable.assert_called_with( - self.service.host, self.service.binary - ) - self.service_mock.disable.assert_not_called() - self.service_mock.disable_log_reason.assert_not_called() + self.service.enable.assert_called_with(self.volume_client) + self.service.disable.assert_not_called() self.assertIsNone(result) def test_service_set_disable(self): @@ -213,11 +177,10 @@ def test_service_set_disable(self): result = self.cmd.take_action(parsed_args) - self.service_mock.disable.assert_called_with( - self.service.host, self.service.binary + self.service.enable.assert_not_called() + self.service.disable.assert_called_with( + self.volume_client, reason=None ) - self.service_mock.enable.assert_not_called() - self.service_mock.disable_log_reason.assert_not_called() self.assertIsNone(result) def test_service_set_disable_with_reason(self): @@ -239,8 +202,9 @@ def test_service_set_disable_with_reason(self): result = self.cmd.take_action(parsed_args) - self.service_mock.disable_log_reason.assert_called_with( - self.service.host, self.service.binary, reason + self.service.enable.assert_not_called() + self.service.disable.assert_called_with( + self.volume_client, reason=reason ) self.assertIsNone(result) @@ -258,6 +222,7 @@ def test_service_set_only_with_disable_reason(self): ('service', self.service.binary), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: self.cmd.take_action(parsed_args) self.fail("CommandError should be raised.") @@ -284,6 +249,7 @@ def test_service_set_enable_with_disable_reason(self): ('service', self.service.binary), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + try: self.cmd.take_action(parsed_args) self.fail("CommandError should be raised.") diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index 5df96f27d4..fac09dc6a0 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -12,170 +12,176 @@ # under the License. from unittest import mock - +import uuid + +from openstack.block_storage.v2 import snapshot as _snapshot +from openstack.block_storage.v2 import type as _type +from openstack.block_storage.v2 import volume as _volume +from openstack import exceptions as sdk_exceptions +from openstack.identity.v3 import project as _project +from openstack.identity.v3 import user as _user +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from openstackclient.api import volume_v2 from openstackclient.tests.unit.image.v2 import fakes as image_fakes from openstackclient.tests.unit import utils as test_utils from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume -class TestVolume(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() - - self.users_mock = self.identity_client.users - self.users_mock.reset_mock() - - self.snapshots_mock = self.volume_client.volume_snapshots - self.snapshots_mock.reset_mock() - - self.types_mock = self.volume_client.volume_types - self.types_mock.reset_mock() - - self.consistencygroups_mock = self.volume_client.consistencygroups - self.consistencygroups_mock.reset_mock() - - def setup_volumes_mock(self, count): - volumes = volume_fakes.create_volumes(count=count) - - self.volumes_mock.get = volume_fakes.get_volumes(volumes, 0) - return volumes - - -class TestVolumeCreate(TestVolume): - project = identity_fakes.FakeProject.create_one_project() - user = identity_fakes.FakeUser.create_one_user() - +class TestVolumeCreate(volume_fakes.TestVolume): columns = ( 'attachments', 'availability_zone', 'bootable', + 'consistencygroup_id', + 'created_at', 'description', + 'encrypted', 'id', + 'multiattach', 'name', + 'os-vol-host-attr:host', + 'os-vol-mig-status-attr:migstat', + 'os-vol-mig-status-attr:name_id', + 'os-vol-tenant-attr:tenant_id', + 'os-volume-replication:driver_data', + 'os-volume-replication:extended_status', 'properties', + 'replication_status', 'size', 'snapshot_id', + 'source_volid', 'status', 'type', + 'updated_at', + 'user_id', + 'volume_image_metadata', ) def setUp(self): super().setUp() - self.new_volume = volume_fakes.create_one_volume() - self.volumes_mock.create.return_value = self.new_volume + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.create_volume.return_value = self.volume self.datalist = ( - self.new_volume.attachments, - self.new_volume.availability_zone, - self.new_volume.bootable, - self.new_volume.description, - self.new_volume.id, - self.new_volume.name, - format_columns.DictColumn(self.new_volume.metadata), - self.new_volume.size, - self.new_volume.snapshot_id, - self.new_volume.status, - self.new_volume.volume_type, + self.volume.attachments, + self.volume.availability_zone, + self.volume.is_bootable, + self.volume.consistency_group_id, + self.volume.created_at, + self.volume.description, + self.volume.is_encrypted, + self.volume.id, + self.volume.is_multiattach, + self.volume.name, + self.volume.host, + self.volume.migration_status, + self.volume.migration_id, + self.volume.project_id, + self.volume.replication_driver_data, + self.volume.extended_replication_status, + format_columns.DictColumn(self.volume.metadata), + self.volume.replication_status, + self.volume.size, + self.volume.snapshot_id, + self.volume.source_volume_id, + self.volume.status, + self.volume.volume_type, + self.volume.updated_at, + self.volume.user_id, + self.volume.volume_image_metadata, ) - # Get the command object to test self.cmd = volume.CreateVolume(self.app, None) def test_volume_create_min_options(self): arglist = [ '--size', - str(self.new_volume.size), + str(self.volume.size), ] verifylist = [ - ('size', self.new_volume.size), + ('size', self.volume.size), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, name=None, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_options(self): - consistency_group = volume_fakes.create_one_consistency_group() - self.consistencygroups_mock.get.return_value = consistency_group + consistency_group_id = 'cg123' arglist = [ '--size', - str(self.new_volume.size), + str(self.volume.size), '--description', - self.new_volume.description, + self.volume.description, '--type', - self.new_volume.volume_type, + self.volume.volume_type, '--availability-zone', - self.new_volume.availability_zone, + self.volume.availability_zone, '--consistency-group', - consistency_group.id, + consistency_group_id, '--hint', 'k=v', - self.new_volume.name, + self.volume.name, ] verifylist = [ - ('size', self.new_volume.size), - ('description', self.new_volume.description), - ('type', self.new_volume.volume_type), - ('availability_zone', self.new_volume.availability_zone), - ('consistency_group', consistency_group.id), + ('size', self.volume.size), + ('description', self.volume.description), + ('type', self.volume.volume_type), + ('availability_zone', self.volume.availability_zone), + ('consistency_group', consistency_group_id), ('hint', {'k': 'v'}), - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. - columns, data = self.cmd.take_action(parsed_args) - - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + with mock.patch.object( + volume_v2, + 'find_consistency_group', + return_value={'id': consistency_group_id}, + ) as mock_find_cg: + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, - description=self.new_volume.description, - volume_type=self.new_volume.volume_type, - availability_zone=self.new_volume.availability_zone, + name=self.volume.name, + description=self.volume.description, + volume_type=self.volume.volume_type, + availability_zone=self.volume.availability_zone, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=consistency_group.id, + image_id=None, + source_volume_id=None, + consistency_group_id=consistency_group_id, scheduler_hints={'k': 'v'}, ) + mock_find_cg.assert_called_once_with( + self.volume_client, consistency_group_id + ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_properties(self): arglist = [ @@ -184,39 +190,36 @@ def test_volume_create_properties(self): '--property', 'Beta=b', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('properties', {'Alpha': 'a', 'Beta': 'b'}), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata={'Alpha': 'a', 'Beta': 'b'}, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) - def test_volume_create_image_id(self): + def test_volume_create_image(self): image = image_fakes.create_one_image() self.image_client.find_image.return_value = image @@ -224,152 +227,111 @@ def test_volume_create_image_id(self): '--image', image.id, '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('image', image.id), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=image.id, - source_volid=None, - consistencygroup_id=None, + image_id=image.id, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, ) - - self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) - - def test_volume_create_image_name(self): - image = image_fakes.create_one_image() - self.image_client.find_image.return_value = image - - arglist = [ - '--image', - image.name, - '--size', - str(self.new_volume.size), - self.new_volume.name, - ] - verifylist = [ - ('image', image.name), - ('size', self.new_volume.size), - ('name', self.new_volume.name), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. - columns, data = self.cmd.take_action(parsed_args) - - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, - snapshot_id=None, - name=self.new_volume.name, - description=None, - volume_type=None, - availability_zone=None, - metadata=None, - imageRef=image.id, - source_volid=None, - consistencygroup_id=None, - scheduler_hints=None, + self.image_client.find_image.assert_called_once_with( + image.id, ignore_missing=False ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_with_snapshot(self): - snapshot = volume_fakes.create_one_snapshot() - self.new_volume.snapshot_id = snapshot.id + snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) + self.volume_client.find_snapshot.return_value = snapshot + arglist = [ '--snapshot', - self.new_volume.snapshot_id, - self.new_volume.name, + snapshot.id, + self.volume.name, ] verifylist = [ - ('snapshot', self.new_volume.snapshot_id), - ('name', self.new_volume.name), + ('snapshot', snapshot.id), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.snapshots_mock.get.return_value = snapshot - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_once_with( + self.volume_client.create_volume.assert_called_with( size=snapshot.size, snapshot_id=snapshot.id, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, ) + self.volume_client.find_snapshot.assert_called_once_with( + snapshot.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_with_source_volume(self): - source_vol = "source_vol" + source_volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = source_volume + arglist = [ '--source', - self.new_volume.id, - source_vol, + source_volume.id, + self.volume.name, ] verifylist = [ - ('source', self.new_volume.id), - ('name', source_vol), + ('source', source_volume.id), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.volumes_mock.get.return_value = self.new_volume - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_once_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=source_volume.size, snapshot_id=None, - name=source_vol, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=self.new_volume.id, - consistencygroup_id=None, + image_id=None, + source_volume_id=source_volume.id, + consistency_group_id=None, scheduler_hints=None, ) + self.volume_client.find_volume.assert_called_once_with( + source_volume.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) @mock.patch.object(utils, 'wait_for_status', return_value=True) def test_volume_create_with_bootable_and_readonly(self, mock_wait): @@ -377,188 +339,187 @@ def test_volume_create_with_bootable_and_readonly(self, mock_wait): '--bootable', '--read-only', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('bootable', True), ('read_only', True), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, ) - - self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) - self.volumes_mock.set_bootable.assert_called_with( - self.new_volume.id, True + self.volume_client.set_volume_bootable_status.assert_called_once_with( + self.volume, True ) - self.volumes_mock.update_readonly_flag.assert_called_with( - self.new_volume.id, True + self.volume_client.set_volume_readonly.assert_called_once_with( + self.volume, True ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + @mock.patch.object(utils, 'wait_for_status', return_value=True) def test_volume_create_with_nonbootable_and_readwrite(self, mock_wait): arglist = [ '--non-bootable', '--read-write', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('bootable', False), ('read_only', False), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, ) - - self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) - self.volumes_mock.set_bootable.assert_called_with( - self.new_volume.id, False + self.volume_client.set_volume_bootable_status.assert_called_once_with( + self.volume, False ) - self.volumes_mock.update_readonly_flag.assert_called_with( - self.new_volume.id, False + self.volume_client.set_volume_readonly.assert_called_once_with( + self.volume, False ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + @mock.patch.object(volume.LOG, 'error') @mock.patch.object(utils, 'wait_for_status', return_value=True) def test_volume_create_with_bootable_and_readonly_fail( self, mock_wait, mock_error ): - self.volumes_mock.set_bootable.side_effect = exceptions.CommandError() - - self.volumes_mock.update_readonly_flag.side_effect = ( - exceptions.CommandError() + self.volume_client.set_volume_bootable_status.side_effect = ( + sdk_exceptions.NotFoundException('foo') + ) + self.volume_client.set_volume_readonly.side_effect = ( + sdk_exceptions.NotFoundException('foo') ) arglist = [ '--bootable', '--read-only', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('bootable', True), ('read_only', True), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, ) + self.volume_client.set_volume_bootable_status.assert_called_once_with( + self.volume, True + ) + self.volume_client.set_volume_readonly.assert_called_once_with( + self.volume, True + ) self.assertEqual(2, mock_error.call_count) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) - self.volumes_mock.set_bootable.assert_called_with( - self.new_volume.id, True - ) - self.volumes_mock.update_readonly_flag.assert_called_with( - self.new_volume.id, True - ) + self.assertEqual(self.datalist, data) @mock.patch.object(volume.LOG, 'error') @mock.patch.object(utils, 'wait_for_status', return_value=False) def test_volume_create_non_available_with_readonly( - self, - mock_wait, - mock_error, + self, mock_wait, mock_error ): arglist = [ '--non-bootable', '--read-only', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('bootable', False), ('read_only', True), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, ) self.assertEqual(2, mock_error.call_count) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_without_size(self): arglist = [ - self.new_volume.name, + self.volume.name, ] verifylist = [ - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -575,15 +536,15 @@ def test_volume_create_with_multi_source(self): '--snapshot', 'source_snapshot', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('image', 'source_image'), ('source', 'source_volume'), ('snapshot', 'source_snapshot'), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] self.assertRaises( @@ -602,7 +563,7 @@ def test_volume_create_hints(self): """ arglist = [ '--size', - str(self.new_volume.size), + str(self.volume.size), '--hint', 'k=v', '--hint', @@ -617,10 +578,10 @@ def test_volume_create_hints(self): 'local_to_instance=v6', '--hint', 'different_host=v7', - self.new_volume.name, + self.volume.name, ] verifylist = [ - ('size', self.new_volume.size), + ('size', self.volume.size), ( 'hint', { @@ -630,26 +591,23 @@ def test_volume_create_hints(self): 'different_host': ['v5', 'v7'], }, ), - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints={ 'k': 'v2', 'same_host': ['v3', 'v4'], @@ -659,149 +617,159 @@ def test_volume_create_hints(self): ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) -class TestVolumeDelete(TestVolume): +class TestVolumeDelete(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volumes_mock.delete.return_value = None + self.volumes = list(sdk_fakes.generate_fake_resources(_volume.Volume)) + self.volume_client.find_volume.side_effect = self.volumes + self.volume_client.delete_volume.return_value = None - # Get the command object to mock self.cmd = volume.DeleteVolume(self.app, None) def test_volume_delete_one_volume(self): - volumes = self.setup_volumes_mock(count=1) - - arglist = [volumes[0].id] + arglist = [self.volumes[0].id] verifylist = [ ("force", False), - ("purge", False), - ("volumes", [volumes[0].id]), + ("cascade", False), + ("volumes", [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.volumes_mock.delete.assert_called_once_with( - volumes[0].id, cascade=False + self.volume_client.find_volume.assert_called_once_with( + self.volumes[0].id, ignore_missing=False + ) + self.volume_client.delete_volume.assert_called_once_with( + self.volumes[0].id, cascade=False, force=False ) - self.assertIsNone(result) def test_volume_delete_multi_volumes(self): - volumes = self.setup_volumes_mock(count=3) - - arglist = [v.id for v in volumes] + arglist = [v.id for v in self.volumes] verifylist = [ ('force', False), - ('purge', False), + ('cascade', False), ('volumes', arglist), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - - calls = [mock.call(v.id, cascade=False) for v in volumes] - self.volumes_mock.delete.assert_has_calls(calls) self.assertIsNone(result) + self.volume_client.find_volume.assert_has_calls( + [mock.call(v.id, ignore_missing=False) for v in self.volumes] + ) + self.volume_client.delete_volume.assert_has_calls( + [mock.call(v.id, cascade=False, force=False) for v in self.volumes] + ) + def test_volume_delete_multi_volumes_with_exception(self): - volumes = self.setup_volumes_mock(count=2) + self.volume_client.find_volume.side_effect = [ + self.volumes[0], + sdk_exceptions.NotFoundException(), + ] arglist = [ - volumes[0].id, + self.volumes[0].id, 'unexist_volume', ] verifylist = [ ('force', False), - ('purge', False), - ('volumes', arglist), + ('cascade', False), + ('volumes', [self.volumes[0].id, 'unexist_volume']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [volumes[0], exceptions.CommandError] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('1 of 2 volumes failed to delete.', str(e)) - - find_mock.assert_any_call(self.volumes_mock, volumes[0].id) - find_mock.assert_any_call(self.volumes_mock, 'unexist_volume') - - self.assertEqual(2, find_mock.call_count) - self.volumes_mock.delete.assert_called_once_with( - volumes[0].id, cascade=False - ) + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertEqual('1 of 2 volumes failed to delete.', str(exc)) - def test_volume_delete_with_purge(self): - volumes = self.setup_volumes_mock(count=1) + self.volume_client.find_volume.assert_has_calls( + [ + mock.call(self.volumes[0].id, ignore_missing=False), + mock.call('unexist_volume', ignore_missing=False), + ] + ) + self.volume_client.delete_volume.assert_has_calls( + [ + mock.call(self.volumes[0].id, cascade=False, force=False), + ] + ) + def test_volume_delete_with_purge(self): arglist = [ '--purge', - volumes[0].id, + self.volumes[0].id, ] verifylist = [ ('force', False), - ('purge', True), - ('volumes', [volumes[0].id]), + ('cascade', True), + ('volumes', [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.volumes_mock.delete.assert_called_once_with( - volumes[0].id, cascade=True + self.volume_client.find_volume.assert_called_once_with( + self.volumes[0].id, ignore_missing=False + ) + self.volume_client.delete_volume.assert_called_once_with( + self.volumes[0].id, cascade=True, force=False ) - self.assertIsNone(result) def test_volume_delete_with_force(self): - volumes = self.setup_volumes_mock(count=1) - arglist = [ '--force', - volumes[0].id, + self.volumes[0].id, ] verifylist = [ ('force', True), - ('purge', False), - ('volumes', [volumes[0].id]), + ('cascade', False), + ('volumes', [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - - self.volumes_mock.force_delete.assert_called_once_with(volumes[0].id) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_once_with( + self.volumes[0].id, ignore_missing=False + ) + self.volume_client.delete_volume.assert_called_once_with( + self.volumes[0].id, cascade=False, force=True + ) + -class TestVolumeList(TestVolume): - project = identity_fakes.FakeProject.create_one_project() - user = identity_fakes.FakeUser.create_one_user() +class TestVolumeList(volume_fakes.TestVolume): + project = sdk_fakes.generate_fake_resource(_project.Project) + user = sdk_fakes.generate_fake_resource(_user.User) - columns = [ + columns = ( 'ID', 'Name', 'Status', 'Size', 'Attached to', - ] + ) def setUp(self): super().setUp() - self.mock_volume = volume_fakes.create_one_volume() - self.volumes_mock.list.return_value = [self.mock_volume] + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.volumes.return_value = [self.volume] - self.users_mock.get.return_value = self.user + self.identity_sdk_client.find_user.return_value = self.user + self.identity_sdk_client.find_project.return_value = self.project - self.projects_mock.get.return_value = self.project - - # Get the command object to test self.cmd = volume.ListVolume(self.app, None) def test_volume_list_no_options(self): @@ -818,28 +786,15 @@ def test_volume_list_no_options(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, - ) - + self.volume_client.volumes.assert_called_once_with(all_projects=False) self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -861,28 +816,17 @@ def test_volume_list_project(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': True, - 'project_id': self.project.id, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + project_id=self.project.id, all_projects=True ) - self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -907,28 +851,17 @@ def test_volume_list_project_domain(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': True, - 'project_id': self.project.id, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + project_id=self.project.id, all_projects=True ) - self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -950,27 +883,17 @@ def test_volume_list_user(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': self.user.id, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + user_id=self.user.id, all_projects=False ) self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -995,28 +918,17 @@ def test_volume_list_user_domain(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': self.user.id, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + user_id=self.user.id, all_projects=False ) - self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1024,12 +936,12 @@ def test_volume_list_user_domain(self): def test_volume_list_name(self): arglist = [ '--name', - self.mock_volume.name, + self.volume.name, ] verifylist = [ ('long', False), ('all_projects', False), - ('name', self.mock_volume.name), + ('name', self.volume.name), ('status', None), ('marker', None), ('limit', None), @@ -1038,28 +950,17 @@ def test_volume_list_name(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': None, - 'name': self.mock_volume.name, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + name=self.volume.name, all_projects=False ) - self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1067,13 +968,13 @@ def test_volume_list_name(self): def test_volume_list_status(self): arglist = [ '--status', - self.mock_volume.status, + self.volume.status, ] verifylist = [ ('long', False), ('all_projects', False), ('name', None), - ('status', self.mock_volume.status), + ('status', self.volume.status), ('marker', None), ('limit', None), ] @@ -1081,28 +982,17 @@ def test_volume_list_status(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': None, - 'name': None, - 'status': self.mock_volume.status, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + status=self.volume.status, all_projects=False ) - self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1123,28 +1013,15 @@ def test_volume_list_all_projects(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': True, - 'project_id': None, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, - ) - + self.volume_client.volumes.assert_called_once_with(all_projects=True) self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1166,20 +1043,8 @@ def test_volume_list_long(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, - ) - - collist = [ + self.volume_client.volumes.assert_called_once_with(all_projects=False) + columns_long = ( 'ID', 'Name', 'Status', @@ -1188,27 +1053,27 @@ def test_volume_list_long(self): 'Bootable', 'Attached to', 'Properties', - ] - self.assertEqual(collist, columns) - - datalist = ( + ) + data_long = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - self.mock_volume.volume_type, - self.mock_volume.bootable, - volume.AttachmentsColumn(self.mock_volume.attachments), - format_columns.DictColumn(self.mock_volume.metadata), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + self.volume.volume_type, + self.volume.is_bootable, + volume.AttachmentsColumn(self.volume.attachments), + format_columns.DictColumn(self.volume.metadata), ), ) - self.assertCountEqual(datalist, tuple(data)) + print(self.volume.is_bootable) + self.assertEqual(columns_long, columns) + self.assertCountEqual(data_long, tuple(data)) def test_volume_list_with_marker_and_limit(self): arglist = [ "--marker", - self.mock_volume.id, + self.volume.id, "--limit", "2", ] @@ -1217,36 +1082,26 @@ def test_volume_list_with_marker_and_limit(self): ('all_projects', False), ('name', None), ('status', None), - ('marker', self.mock_volume.id), + ('marker', self.volume.id), ('limit', 2), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) + self.volume_client.volumes.assert_called_once_with( + limit=2, marker=self.volume.id, all_projects=False + ) self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) - - self.volumes_mock.list.assert_called_once_with( - marker=self.mock_volume.id, - limit=2, - search_opts={ - 'status': None, - 'project_id': None, - 'user_id': None, - 'name': None, - 'all_tenants': False, - }, - ) self.assertCountEqual(datalist, tuple(data)) def test_volume_list_negative_limit(self): @@ -1265,107 +1120,81 @@ def test_volume_list_negative_limit(self): verifylist, ) - def test_volume_list_backward_compatibility(self): - arglist = [ - '-c', - 'Display Name', - ] - verifylist = [ - ('columns', ['Display Name']), - ('long', False), - ('all_projects', False), - ('name', None), - ('status', None), - ('marker', None), - ('limit', None), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, - ) - - self.assertIn('Display Name', columns) - self.assertNotIn('Name', columns) - - for each_volume in data: - self.assertIn(self.mock_volume.name, each_volume) - - -class TestVolumeMigrate(TestVolume): - _volume = volume_fakes.create_one_volume() +class TestVolumeMigrate(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volumes_mock.get.return_value = self._volume - self.volumes_mock.migrate_volume.return_value = None - # Get the command object to test + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.volume_client.migrate_volume.return_value = None + self.cmd = volume.MigrateVolume(self.app, None) def test_volume_migrate(self): arglist = [ "--host", "host@backend-name#pool", - self._volume.id, + self.volume.id, ] verifylist = [ ("force_host_copy", False), ("lock_volume", False), ("host", "host@backend-name#pool"), - ("volume", self._volume.id), + ("volume", self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.volumes_mock.get.assert_called_once_with(self._volume.id) - self.volumes_mock.migrate_volume.assert_called_once_with( - self._volume.id, "host@backend-name#pool", False, False - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.migrate_volume.assert_called_once_with( + self.volume.id, + host="host@backend-name#pool", + force_host_copy=False, + lock_volume=False, + ) + def test_volume_migrate_with_option(self): arglist = [ "--force-host-copy", "--lock-volume", "--host", "host@backend-name#pool", - self._volume.id, + self.volume.id, ] verifylist = [ ("force_host_copy", True), ("lock_volume", True), ("host", "host@backend-name#pool"), - ("volume", self._volume.id), + ("volume", self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.volumes_mock.get.assert_called_once_with(self._volume.id) - self.volumes_mock.migrate_volume.assert_called_once_with( - self._volume.id, "host@backend-name#pool", True, True - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.migrate_volume.assert_called_once_with( + self.volume.id, + host="host@backend-name#pool", + force_host_copy=True, + lock_volume=True, + ) + def test_volume_migrate_without_host(self): arglist = [ - self._volume.id, + self.volume.id, ] verifylist = [ ("force_host_copy", False), ("lock_volume", False), - ("volume", self._volume.id), + ("volume", self.volume.id), ] self.assertRaises( @@ -1376,16 +1205,18 @@ def test_volume_migrate_without_host(self): verifylist, ) + self.volume_client.find_volume.assert_not_called() + self.volume_client.migrate_volume.assert_not_called() -class TestVolumeSet(TestVolume): - volume_type = volume_fakes.create_one_volume_type() +class TestVolumeSet(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.new_volume = volume_fakes.create_one_volume() - self.volumes_mock.get.return_value = self.new_volume - self.types_mock.get.return_value = self.volume_type + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.volume_client.find_type.return_value = self.volume_type # Get the command object to test self.cmd = volume.SetVolume(self.app, None) @@ -1396,19 +1227,24 @@ def test_volume_set_property(self): 'a=b', '--property', 'c=d', - self.new_volume.id, + self.volume.id, ] verifylist = [ ('properties', {'a': 'b', 'c': 'd'}), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ('bootable', None), ('read_only', None), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.cmd.take_action(parsed_args) - self.volumes_mock.set_metadata.assert_called_with( - self.new_volume.id, parsed_args.properties + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.set_volume_metadata.assert_called_once_with( + self.volume, **parsed_args.properties ) def test_volume_set_image_property(self): @@ -1417,302 +1253,390 @@ def test_volume_set_image_property(self): 'Alpha=a', '--image-property', 'Beta=b', - self.new_volume.id, + self.volume.id, ] verifylist = [ ('image_properties', {'Alpha': 'a', 'Beta': 'b'}), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ('bootable', None), ('read_only', None), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - # In base command class ShowOne in cliff, abstract method take_action() - # returns nothing - self.cmd.take_action(parsed_args) - self.volumes_mock.set_image_metadata.assert_called_with( - self.new_volume.id, parsed_args.image_properties + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.set_volume_image_metadata.assert_called_once_with( + self.volume, **parsed_args.image_properties ) def test_volume_set_state(self): - arglist = ['--state', 'error', self.new_volume.id] + arglist = ['--state', 'error', self.volume.id] verifylist = [ ('read_only', None), ('state', 'error'), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.reset_state.assert_called_with( - self.new_volume.id, 'error' - ) - self.volumes_mock.update_readonly_flag.assert_not_called() self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.reset_volume_status.assert_called_with( + self.volume, status='error' + ) + def test_volume_set_state_failed(self): - self.volumes_mock.reset_state.side_effect = exceptions.CommandError() - arglist = ['--state', 'error', self.new_volume.id] - verifylist = [('state', 'error'), ('volume', self.new_volume.id)] + self.volume_client.reset_volume_status.side_effect = ( + sdk_exceptions.NotFoundException('foo') + ) + + arglist = ['--state', 'error', self.volume.id] + verifylist = [('state', 'error'), ('volume', self.volume.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - 'One or more of the set operations failed', str(e) - ) - self.volumes_mock.reset_state.assert_called_with( - self.new_volume.id, 'error' + exc = self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + self.assertEqual('One or more of the set operations failed', str(exc)) + + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.reset_volume_status.assert_called_with( + self.volume, status='error' ) def test_volume_set_attached(self): - arglist = ['--attached', self.new_volume.id] + arglist = ['--attached', self.volume.id] verifylist = [ ('attached', True), ('detached', False), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.reset_state.assert_called_with( - self.new_volume.id, attach_status='attached', state=None - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.reset_volume_status.assert_called_with( + self.volume, attach_status='attached' + ) + def test_volume_set_detached(self): - arglist = ['--detached', self.new_volume.id] + arglist = ['--detached', self.volume.id] verifylist = [ ('attached', False), ('detached', True), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.reset_state.assert_called_with( - self.new_volume.id, attach_status='detached', state=None - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.reset_volume_status.assert_called_with( + self.volume, attach_status='detached' + ) + def test_volume_set_bootable(self): arglist = [ '--bootable', - self.new_volume.id, + self.volume.id, ] verifylist = [ ('bootable', True), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.cmd.take_action(parsed_args) - self.volumes_mock.set_bootable.assert_called_with( - self.new_volume.id, verifylist[0][1] + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False ) + self.volume_client.set_volume_bootable_status(self.volume, True) def test_volume_set_non_bootable(self): arglist = [ '--non-bootable', - self.new_volume.id, + self.volume.id, ] verifylist = [ ('bootable', False), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.cmd.take_action(parsed_args) - self.volumes_mock.set_bootable.assert_called_with( - self.new_volume.id, verifylist[0][1] + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False ) + self.volume_client.set_volume_bootable_status(self.volume, False) def test_volume_set_read_only(self): - arglist = ['--read-only', self.new_volume.id] + arglist = ['--read-only', self.volume.id] verifylist = [ ('read_only', True), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.update_readonly_flag.assert_called_once_with( - self.new_volume.id, True - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.set_volume_readonly.assert_called_with( + self.volume, True + ) + def test_volume_set_read_write(self): - arglist = ['--read-write', self.new_volume.id] + arglist = ['--read-write', self.volume.id] verifylist = [ ('read_only', False), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.update_readonly_flag.assert_called_once_with( - self.new_volume.id, False - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.set_volume_readonly.assert_called_with( + self.volume, False + ) + def test_volume_set_type(self): - arglist = ['--type', self.volume_type.id, self.new_volume.id] + arglist = ['--type', self.volume_type.id, self.volume.id] verifylist = [ ('retype_policy', None), ('type', self.volume_type.id), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.retype.assert_called_once_with( - self.new_volume.id, self.volume_type.id, 'never' - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.find_type.assert_called_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.retype_volume.assert_called_once_with( + self.volume.id, self.volume_type.id, 'never' + ) + def test_volume_set_type_with_policy(self): arglist = [ '--retype-policy', 'on-demand', '--type', self.volume_type.id, - self.new_volume.id, + self.volume.id, ] verifylist = [ ('retype_policy', 'on-demand'), ('type', self.volume_type.id), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.retype.assert_called_once_with( - self.new_volume.id, self.volume_type.id, 'on-demand' - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.find_type.assert_called_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.retype_volume.assert_called_once_with( + self.volume.id, self.volume_type.id, 'on-demand' + ) + @mock.patch.object(volume.LOG, 'warning') def test_volume_set_with_only_retype_policy(self, mock_warning): - arglist = ['--retype-policy', 'on-demand', self.new_volume.id] + arglist = ['--retype-policy', 'on-demand', self.volume.id] verifylist = [ ('retype_policy', 'on-demand'), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.retype.assert_not_called() + self.assertIsNone(result) + + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.find_type.assert_not_called() + self.volume_client.retype_volume.assert_not_called() mock_warning.assert_called_with( - "'--retype-policy' option will not work without '--type' option" + "'%s' option will not work without '--type' option", + '--retype-policy', ) - self.assertIsNone(result) -class TestVolumeShow(TestVolume): +class TestVolumeShow(volume_fakes.TestVolume): def setUp(self): super().setUp() - self._volume = volume_fakes.create_one_volume() - self.volumes_mock.get.return_value = self._volume - # Get the command object to test + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + + self.columns = ( + 'attachments', + 'availability_zone', + 'bootable', + 'consistencygroup_id', + 'created_at', + 'description', + 'encrypted', + 'id', + 'multiattach', + 'name', + 'os-vol-host-attr:host', + 'os-vol-mig-status-attr:migstat', + 'os-vol-mig-status-attr:name_id', + 'os-vol-tenant-attr:tenant_id', + 'os-volume-replication:driver_data', + 'os-volume-replication:extended_status', + 'properties', + 'replication_status', + 'size', + 'snapshot_id', + 'source_volid', + 'status', + 'type', + 'updated_at', + 'user_id', + 'volume_image_metadata', + ) + self.data = ( + self.volume.attachments, + self.volume.availability_zone, + self.volume.is_bootable, + self.volume.consistency_group_id, + self.volume.created_at, + self.volume.description, + self.volume.is_encrypted, + self.volume.id, + self.volume.is_multiattach, + self.volume.name, + self.volume.host, + self.volume.migration_status, + self.volume.migration_id, + self.volume.project_id, + self.volume.replication_driver_data, + self.volume.extended_replication_status, + format_columns.DictColumn(self.volume.metadata), + self.volume.replication_status, + self.volume.size, + self.volume.snapshot_id, + self.volume.source_volume_id, + self.volume.status, + self.volume.volume_type, + self.volume.updated_at, + self.volume.user_id, + self.volume.volume_image_metadata, + ) + self.cmd = volume.ShowVolume(self.app, None) def test_volume_show(self): - arglist = [self._volume.id] - verifylist = [("volume", self._volume.id)] + arglist = [self.volume.id] + verifylist = [("volume", self.volume.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.get.assert_called_with(self._volume.id) - self.assertEqual( - tuple(sorted(self._volume.keys())), - columns, - ) - self.assertTupleEqual( - ( - self._volume.attachments, - self._volume.availability_zone, - self._volume.bootable, - self._volume.description, - self._volume.id, - self._volume.name, - format_columns.DictColumn(self._volume.metadata), - self._volume.size, - self._volume.snapshot_id, - self._volume.status, - self._volume.volume_type, - ), - data, + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False ) -class TestVolumeUnset(TestVolume): +class TestVolumeUnset(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.new_volume = volume_fakes.create_one_volume() - self.volumes_mock.get.return_value = self.new_volume + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.volume_client.delete_volume_metadata.return_value = None + self.volume_client.delete_volume_image_metadata.return_value = None - # Get the command object to set property - self.cmd_set = volume.SetVolume(self.app, None) + self.cmd = volume.UnsetVolume(self.app, None) - # Get the command object to unset property - self.cmd_unset = volume.UnsetVolume(self.app, None) - - def test_volume_unset_image_property(self): - # Arguments for setting image properties + def test_volume_unset_property(self): arglist = [ - '--image-property', - 'Alpha=a', - '--image-property', - 'Beta=b', - self.new_volume.id, + '--property', + 'a', + '--property', + 'c', + self.volume.id, ] verifylist = [ - ('image_properties', {'Alpha': 'a', 'Beta': 'b'}), - ('volume', self.new_volume.id), + ('properties', ['a', 'c']), + ('volume', self.volume.id), ] - parsed_args = self.check_parser(self.cmd_set, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns nothing - self.cmd_set.take_action(parsed_args) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.delete_volume_metadata.assert_called_once_with( + self.volume.id, keys=parsed_args.properties + ) - # Arguments for unsetting image properties - arglist_unset = [ + def test_volume_unset_image_property(self): + arglist = [ '--image-property', 'Alpha', - self.new_volume.id, + self.volume.id, ] - verifylist_unset = [ + verifylist = [ ('image_properties', ['Alpha']), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] - parsed_args_unset = self.check_parser( - self.cmd_unset, arglist_unset, verifylist_unset - ) - # In base command class ShowOne in cliff, abstract method take_action() - # returns nothing - self.cmd_unset.take_action(parsed_args_unset) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.volumes_mock.delete_image_metadata.assert_called_with( - self.new_volume.id, parsed_args_unset.image_properties + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.delete_volume_image_metadata.assert_called_once_with( + self.volume.id, keys=parsed_args.image_properties ) def test_volume_unset_image_property_fail(self): - self.volumes_mock.delete_image_metadata.side_effect = ( + self.volume_client.delete_volume_image_metadata.side_effect = ( exceptions.CommandError() ) arglist = [ @@ -1720,55 +1644,72 @@ def test_volume_unset_image_property_fail(self): 'Alpha', '--property', 'Beta', - self.new_volume.id, + self.volume.id, ] verifylist = [ ('image_properties', ['Alpha']), ('properties', ['Beta']), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] - parsed_args = self.check_parser(self.cmd_unset, arglist, verifylist) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd_unset.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - 'One or more of the unset operations failed', str(e) - ) - self.volumes_mock.delete_image_metadata.assert_called_with( - self.new_volume.id, parsed_args.image_properties + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertEqual( + 'One or more of the unset operations failed', str(exc) ) - self.volumes_mock.delete_metadata.assert_called_with( - self.new_volume.id, parsed_args.properties + self.volume_client.delete_volume_metadata.assert_called_once_with( + self.volume.id, keys=parsed_args.properties + ) + self.volume_client.delete_volume_image_metadata.assert_called_once_with( + self.volume.id, keys=parsed_args.image_properties ) -class TestColumns(TestVolume): +class TestColumns(volume_fakes.TestVolume): def test_attachments_column_without_server_cache(self): - _volume = volume_fakes.create_one_volume() - server_id = _volume.attachments[0]['server_id'] - device = _volume.attachments[0]['device'] + vol = sdk_fakes.generate_fake_resource( + _volume.Volume, + attachments=[ + { + 'device': '/dev/' + uuid.uuid4().hex, + 'server_id': uuid.uuid4().hex, + }, + ], + ) + server_id = vol.attachments[0]['server_id'] + device = vol.attachments[0]['device'] - col = volume.AttachmentsColumn(_volume.attachments, {}) + col = volume.AttachmentsColumn(vol.attachments, {}) self.assertEqual( f'Attached to {server_id} on {device} ', col.human_readable(), ) - self.assertEqual(_volume.attachments, col.machine_readable()) + self.assertEqual(vol.attachments, col.machine_readable()) def test_attachments_column_with_server_cache(self): - _volume = volume_fakes.create_one_volume() + vol = sdk_fakes.generate_fake_resource( + _volume.Volume, + attachments=[ + { + 'device': '/dev/' + uuid.uuid4().hex, + 'server_id': uuid.uuid4().hex, + }, + ], + ) - server_id = _volume.attachments[0]['server_id'] - device = _volume.attachments[0]['device'] + server_id = vol.attachments[0]['server_id'] + device = vol.attachments[0]['device'] fake_server = mock.Mock() fake_server.name = 'fake-server-name' server_cache = {server_id: fake_server} - col = volume.AttachmentsColumn(_volume.attachments, server_cache) + col = volume.AttachmentsColumn(vol.attachments, server_cache) self.assertEqual( 'Attached to {} on {} '.format('fake-server-name', device), col.human_readable(), ) - self.assertEqual(_volume.attachments, col.machine_readable()) + self.assertEqual(vol.attachments, col.machine_readable()) diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backend.py b/openstackclient/tests/unit/volume/v2/test_volume_backend.py index 8a11f01c53..09adae1308 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backend.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backend.py @@ -10,25 +10,83 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# +from openstack.block_storage.v2 import capabilities as _capabilities +from openstack.block_storage.v2 import stats as _stats from osc_lib.cli import format_columns from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume_backend +def _create_fake_capability(): + """Create a fake volume backend capability. + + :param dict attrs: + A dictionary with all attributes of the Capabilities. + :return: + A FakeResource object with capability name and attrs. + """ + # Set default attribute + return _capabilities.Capabilities( + namespace="OS::Storage::Capabilities::fake", + vendor_name="OpenStack", + volume_backend_name="lvmdriver-1", + pool_name="pool", + driver_version="2.0.0", + storage_protocol="iSCSI", + display_name="Capabilities of Cinder LVM driver", + description="Blah, blah.", + visibility="public", + replication_targets=[], + properties={ + "compression": { + "title": "Compression", + "description": "Enables compression.", + "type": "boolean", + }, + "qos": { + "title": "QoS", + "description": "Enables QoS.", + "type": "boolean", + }, + "replication": { + "title": "Replication", + "description": "Enables replication.", + "type": "boolean", + }, + "thin_provisioning": { + "title": "Thin Provisioning", + "description": "Sets thin provisioning.", + "type": "boolean", + }, + }, + ) + + +def _create_fake_pool(): + return _stats.Pools( + name='host@lvmdriver-1#lvmdriver-1', + capabilities={ + 'storage_protocol': 'iSCSI', + 'thick_provisioning_support': False, + 'thin_provisioning_support': True, + 'total_volumes': 99, + 'total_capacity_gb': 1000.00, + 'allocated_capacity_gb': 100, + 'max_over_subscription_ratio': 200.0, + }, + ) + + class TestShowVolumeCapability(volume_fakes.TestVolume): """Test backend capability functionality.""" - # The capability to be listed - capability = volume_fakes.create_one_capability() - def setUp(self): super().setUp() - # Assign return value to capabilities mock - self.volume_sdk_client.get_capabilities.return_value = self.capability + self.capability = _create_fake_capability() + self.volume_client.get_capabilities.return_value = self.capability # Get the command object to test self.cmd = volume_backend.ShowCapability(self.app, None) @@ -69,7 +127,7 @@ def test_capability_show(self): self.assertIn(cap[0], capabilities) # checking if proper call was made to get capabilities - self.volume_sdk_client.get_capabilities.assert_called_with( + self.volume_client.get_capabilities.assert_called_with( 'fake', ) @@ -77,13 +135,11 @@ def test_capability_show(self): class TestListVolumePool(volume_fakes.TestVolume): """Tests for volume backend pool listing.""" - # The pool to be listed - pools = volume_fakes.create_one_pool() - def setUp(self): super().setUp() - self.volume_sdk_client.backend_pools.return_value = [self.pools] + self.pool = _create_fake_pool() + self.volume_client.backend_pools.return_value = [self.pool] # Get the command object to test self.cmd = volume_backend.ListPool(self.app, None) @@ -105,13 +161,13 @@ def test_pool_list(self): # confirming if all expected columns are present in the result. self.assertEqual(expected_columns, columns) - datalist = ((self.pools.name,),) + datalist = ((self.pool.name,),) # confirming if all expected values are present in the result. self.assertEqual(datalist, tuple(data)) # checking if proper call was made to list pools - self.volume_sdk_client.backend_pools.assert_called_with( + self.volume_client.backend_pools.assert_called_with( detailed=False, ) @@ -139,14 +195,14 @@ def test_service_list_with_long_option(self): datalist = ( ( - self.pools.name, - format_columns.DictColumn(self.pools.capabilities), + self.pool.name, + format_columns.DictColumn(self.pool.capabilities), ), ) # confirming if all expected values are present in the result. self.assertEqual(datalist, tuple(data)) - self.volume_sdk_client.backend_pools.assert_called_with( + self.volume_client.backend_pools.assert_called_with( detailed=True, ) diff --git a/openstackclient/tests/unit/volume/v2/test_volume_backup.py b/openstackclient/tests/unit/volume/v2/test_volume_backup.py index faf24899af..bba14f5dcb 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backup.py @@ -13,89 +13,81 @@ from unittest.mock import call +from openstack.block_storage.v2 import backup as _backup +from openstack.block_storage.v2 import snapshot as _snapshot +from openstack.block_storage.v2 import volume as _volume +from openstack import exceptions as sdk_exceptions +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume_backup -class TestBackupLegacy(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.backups_mock = self.volume_client.backups - self.backups_mock.reset_mock() - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - self.snapshots_mock = self.volume_client.volume_snapshots - self.snapshots_mock.reset_mock() - self.restores_mock = self.volume_client.restores - self.restores_mock.reset_mock() - - class TestBackupCreate(volume_fakes.TestVolume): - volume = volume_fakes.create_one_volume() - snapshot = volume_fakes.create_one_snapshot() - new_backup = volume_fakes.create_one_backup( - attrs={'volume_id': volume.id, 'snapshot_id': snapshot.id} - ) - columns = ( 'id', 'name', 'volume_id', ) - data = ( - new_backup.id, - new_backup.name, - new_backup.volume_id, - ) def setUp(self): super().setUp() - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.find_snapshot.return_value = self.snapshot - self.volume_sdk_client.create_backup.return_value = self.new_backup + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) + self.volume_client.find_snapshot.return_value = self.snapshot + self.backup = sdk_fakes.generate_fake_resource( + _backup.Backup, + volume_id=self.volume.id, + snapshot_id=self.snapshot.id, + ) + self.volume_client.create_backup.return_value = self.backup + + self.data = ( + self.backup.id, + self.backup.name, + self.backup.volume_id, + ) - # Get the command object to test self.cmd = volume_backup.CreateVolumeBackup(self.app, None) def test_backup_create(self): arglist = [ "--name", - self.new_backup.name, + self.backup.name, "--description", - self.new_backup.description, + self.backup.description, "--container", - self.new_backup.container, + self.backup.container, "--force", "--incremental", "--snapshot", - self.new_backup.snapshot_id, - self.new_backup.volume_id, + self.backup.snapshot_id, + self.backup.volume_id, ] verifylist = [ - ("name", self.new_backup.name), - ("description", self.new_backup.description), - ("container", self.new_backup.container), + ("name", self.backup.name), + ("description", self.backup.description), + ("container", self.backup.container), ("force", True), ("incremental", True), - ("snapshot", self.new_backup.snapshot_id), - ("volume", self.new_backup.volume_id), + ("snapshot", self.backup.snapshot_id), + ("volume", self.backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( - volume_id=self.new_backup.volume_id, - container=self.new_backup.container, - name=self.new_backup.name, - description=self.new_backup.description, + self.volume_client.create_backup.assert_called_with( + volume_id=self.backup.volume_id, + container=self.backup.container, + name=self.backup.name, + description=self.backup.description, force=True, is_incremental=True, - snapshot_id=self.new_backup.snapshot_id, + snapshot_id=self.backup.snapshot_id, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -103,25 +95,25 @@ def test_backup_create(self): def test_backup_create_without_name(self): arglist = [ "--description", - self.new_backup.description, + self.backup.description, "--container", - self.new_backup.container, - self.new_backup.volume_id, + self.backup.container, + self.backup.volume_id, ] verifylist = [ - ("description", self.new_backup.description), - ("container", self.new_backup.container), - ("volume", self.new_backup.volume_id), + ("description", self.backup.description), + ("container", self.backup.container), + ("volume", self.backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( - volume_id=self.new_backup.volume_id, - container=self.new_backup.container, + self.volume_client.create_backup.assert_called_with( + volume_id=self.backup.volume_id, + container=self.backup.container, name=None, - description=self.new_backup.description, + description=self.backup.description, force=False, is_incremental=False, ) @@ -130,17 +122,13 @@ def test_backup_create_without_name(self): class TestBackupDelete(volume_fakes.TestVolume): - backups = volume_fakes.create_backups(count=2) - def setUp(self): super().setUp() - self.volume_sdk_client.find_backup = volume_fakes.get_backups( - self.backups - ) - self.volume_sdk_client.delete_backup.return_value = None + self.backups = list(sdk_fakes.generate_fake_resources(_backup.Backup)) + self.volume_client.find_backup.side_effect = self.backups + self.volume_client.delete_backup.return_value = None - # Get the command object to mock self.cmd = volume_backup.DeleteVolumeBackup(self.app, None) def test_backup_delete(self): @@ -150,7 +138,7 @@ def test_backup_delete(self): result = self.cmd.take_action(parsed_args) - self.volume_sdk_client.delete_backup.assert_called_with( + self.volume_client.delete_backup.assert_called_with( self.backups[0].id, ignore_missing=False, force=False ) self.assertIsNone(result) @@ -165,7 +153,7 @@ def test_backup_delete_with_force(self): result = self.cmd.take_action(parsed_args) - self.volume_sdk_client.delete_backup.assert_called_with( + self.volume_client.delete_backup.assert_called_with( self.backups[0].id, ignore_missing=False, force=True ) self.assertIsNone(result) @@ -184,7 +172,7 @@ def test_delete_multiple_backups(self): calls = [] for b in self.backups: calls.append(call(b.id, ignore_missing=False, force=False)) - self.volume_sdk_client.delete_backup.assert_has_calls(calls) + self.volume_client.delete_backup.assert_has_calls(calls) self.assertIsNone(result) def test_delete_multiple_backups_with_exception(self): @@ -199,7 +187,7 @@ def test_delete_multiple_backups_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self.backups[0], exceptions.CommandError] - self.volume_sdk_client.find_backup.side_effect = find_mock_result + self.volume_client.find_backup.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -207,15 +195,15 @@ def test_delete_multiple_backups_with_exception(self): except exceptions.CommandError as e: self.assertEqual('1 of 2 backups failed to delete.', str(e)) - self.volume_sdk_client.find_backup.assert_any_call( + self.volume_client.find_backup.assert_any_call( self.backups[0].id, ignore_missing=False ) - self.volume_sdk_client.find_backup.assert_any_call( + self.volume_client.find_backup.assert_any_call( 'unexist_backup', ignore_missing=False ) - self.assertEqual(2, self.volume_sdk_client.find_backup.call_count) - self.volume_sdk_client.delete_backup.assert_called_once_with( + self.assertEqual(2, self.volume_client.find_backup.call_count) + self.volume_client.delete_backup.assert_called_once_with( self.backups[0].id, ignore_missing=False, force=False, @@ -223,11 +211,6 @@ def test_delete_multiple_backups_with_exception(self): class TestBackupList(volume_fakes.TestVolume): - volume = volume_fakes.create_one_volume() - backups = volume_fakes.create_backups( - attrs={'volume_id': volume.name}, count=3 - ) - columns = ( 'ID', 'Name', @@ -237,51 +220,53 @@ class TestBackupList(volume_fakes.TestVolume): 'Incremental', 'Created At', ) - columns_long = columns + ( - 'Availability Zone', - 'Volume', - 'Container', - ) - - data = [] - for b in backups: - data.append( - ( - b.id, - b.name, - b.description, - b.status, - b.size, - b.is_incremental, - b.created_at, - ) - ) - data_long = [] - for b in backups: - data_long.append( - ( - b.id, - b.name, - b.description, - b.status, - b.size, - b.is_incremental, - b.created_at, - b.availability_zone, - volume_backup.VolumeIdColumn(b.volume_id), - b.container, - ) - ) + columns_long = (*columns, 'Availability Zone', 'Volume', 'Container') def setUp(self): super().setUp() - self.volume_sdk_client.volumes.return_value = [self.volume] - self.volume_sdk_client.backups.return_value = self.backups - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.find_backup.return_value = self.backups[0] + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.volume_client.volumes.return_value = [self.volume] + self.backups = list( + sdk_fakes.generate_fake_resources( + _backup.Backup, + attrs={'volume_id': self.volume.id}, + ) + ) + self.volume_client.backups.return_value = self.backups + self.volume_client.find_backup.return_value = self.backups[0] + + self.data = [] + for b in self.backups: + self.data.append( + ( + b.id, + b.name, + b.description, + b.status, + b.size, + b.is_incremental, + b.created_at, + ) + ) + self.data_long = [] + for b in self.backups: + self.data_long.append( + ( + b.id, + b.name, + b.description, + b.status, + b.size, + b.is_incremental, + b.created_at, + b.availability_zone, + volume_backup.VolumeIdColumn(b.volume_id), + b.container, + ) + ) - # Get the command to test self.cmd = volume_backup.ListVolumeBackup(self.app, None) def test_backup_list_without_options(self): @@ -299,15 +284,16 @@ def test_backup_list_without_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.find_volume.assert_not_called() - self.volume_sdk_client.find_backup.assert_not_called() - self.volume_sdk_client.backups.assert_called_with( + self.volume_client.find_volume.assert_not_called() + self.volume_client.find_backup.assert_not_called() + self.volume_client.backups.assert_called_with( name=None, status=None, volume_id=None, all_tenants=False, marker=None, limit=None, + max_items=None, ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) @@ -340,60 +326,57 @@ def test_backup_list_with_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.volume.id, ignore_missing=False ) - self.volume_sdk_client.find_backup.assert_called_once_with( + self.volume_client.find_backup.assert_called_once_with( self.backups[0].id, ignore_missing=False ) - self.volume_sdk_client.backups.assert_called_with( + self.volume_client.backups.assert_called_with( name=self.backups[0].name, status="error", volume_id=self.volume.id, all_tenants=True, marker=self.backups[0].id, limit=3, + max_items=None, ) self.assertEqual(self.columns_long, columns) self.assertCountEqual(self.data_long, list(data)) class TestBackupRestore(volume_fakes.TestVolume): - volume = volume_fakes.create_one_volume() - backup = volume_fakes.create_one_backup( - attrs={'volume_id': volume.id}, - ) - columns = ( "id", "volume_id", "volume_name", ) - data = ( - backup.id, - volume.id, - volume.name, - ) - def setUp(self): super().setUp() - self.volume_sdk_client.find_backup.return_value = self.backup - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.restore_backup.return_value = { + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.backup = sdk_fakes.generate_fake_resource( + _backup.Backup, volume_id=self.volume.id + ) + self.volume_client.find_backup.return_value = self.backup + self.volume_client.restore_backup.return_value = { 'id': self.backup['id'], 'volume_id': self.volume['id'], 'volume_name': self.volume['name'], } - # Get the command object to mock + self.data = ( + self.backup.id, + self.volume.id, + self.volume.name, + ) + self.cmd = volume_backup.RestoreVolumeBackup(self.app, None) def test_backup_restore(self): - self.volume_sdk_client.find_volume.side_effect = ( - exceptions.CommandError() - ) + self.volume_client.find_volume.side_effect = exceptions.CommandError() arglist = [self.backup.id] verifylist = [ ("backup", self.backup.id), @@ -402,19 +385,15 @@ def test_backup_restore(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.restore_backup.assert_called_with( - self.backup.id, - volume_id=None, - name=None, + self.volume_client.restore_backup.assert_called_with( + self.backup.id, volume=None, name=None ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) def test_backup_restore_with_volume(self): - self.volume_sdk_client.find_volume.side_effect = ( - exceptions.CommandError() - ) + self.volume_client.find_volume.side_effect = exceptions.CommandError() arglist = [ self.backup.id, self.backup.volume_id, @@ -426,10 +405,8 @@ def test_backup_restore_with_volume(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.restore_backup.assert_called_with( - self.backup.id, - volume_id=None, - name=self.backup.volume_id, + self.volume_client.restore_backup.assert_called_with( + self.backup.id, volume=None, name=self.backup.volume_id ) self.assertEqual(self.columns, columns) @@ -449,10 +426,8 @@ def test_backup_restore_with_volume_force(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.restore_backup.assert_called_with( - self.backup.id, - volume_id=self.volume.id, - name=None, + self.volume_client.restore_backup.assert_called_with( + self.backup.id, volume=self.volume.id, name=None ) self.assertEqual(self.columns, columns) @@ -476,17 +451,15 @@ def test_backup_restore_with_volume_existing(self): ) -class TestBackupSet(TestBackupLegacy): - backup = volume_fakes.create_one_backup( - attrs={'metadata': {'wow': 'cool'}}, - ) - +class TestBackupSet(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.backups_mock.get.return_value = self.backup + self.backup = sdk_fakes.generate_fake_resource( + _backup.Backup, metadata={'wow': 'cool'} + ) + self.volume_client.find_backup.return_value = self.backup - # Get the command object to test self.cmd = volume_backup.SetVolumeBackup(self.app, None) def test_backup_set_state(self): @@ -496,32 +469,38 @@ def test_backup_set_state(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.backups_mock.reset_state.assert_called_once_with( - self.backup.id, 'error' - ) self.assertIsNone(result) + self.volume_client.find_backup.assert_called_with( + self.backup.id, ignore_missing=False + ) + self.volume_client.reset_backup_status.assert_called_with( + self.backup, status='error' + ) + def test_backup_set_state_failed(self): - self.backups_mock.reset_state.side_effect = exceptions.CommandError() + self.volume_client.reset_backup_status.side_effect = ( + sdk_exceptions.NotFoundException('foo') + ) + arglist = ['--state', 'error', self.backup.id] verifylist = [('state', 'error'), ('backup', self.backup.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - 'One or more of the set operations failed', str(e) - ) - self.backups_mock.reset_state.assert_called_with( - self.backup.id, 'error' + exc = self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args ) + self.assertEqual('One or more of the set operations failed', str(exc)) + self.volume_client.find_backup.assert_called_with( + self.backup.id, ignore_missing=False + ) + self.volume_client.reset_backup_status.assert_called_with( + self.backup, status='error' + ) -class TestBackupShow(volume_fakes.TestVolume): - backup = volume_fakes.create_one_backup() +class TestBackupShow(volume_fakes.TestVolume): columns = ( "availability_zone", "container", @@ -540,30 +519,32 @@ class TestBackupShow(volume_fakes.TestVolume): "updated_at", "volume_id", ) - data = ( - backup.availability_zone, - backup.container, - backup.created_at, - backup.data_timestamp, - backup.description, - backup.fail_reason, - backup.has_dependent_backups, - backup.id, - backup.is_incremental, - backup.name, - backup.object_count, - backup.size, - backup.snapshot_id, - backup.status, - backup.updated_at, - backup.volume_id, - ) def setUp(self): super().setUp() - self.volume_sdk_client.find_backup.return_value = self.backup - # Get the command object to test + self.backup = sdk_fakes.generate_fake_resource(_backup.Backup) + self.volume_client.find_backup.return_value = self.backup + + self.data = ( + self.backup.availability_zone, + self.backup.container, + self.backup.created_at, + self.backup.data_timestamp, + self.backup.description, + self.backup.fail_reason, + self.backup.has_dependent_backups, + self.backup.id, + self.backup.is_incremental, + self.backup.name, + self.backup.object_count, + self.backup.size, + self.backup.snapshot_id, + self.backup.status, + self.backup.updated_at, + self.backup.volume_id, + ) + self.cmd = volume_backup.ShowVolumeBackup(self.app, None) def test_backup_show(self): @@ -572,7 +553,9 @@ def test_backup_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.find_backup.assert_called_with(self.backup.id) + self.volume_client.find_backup.assert_called_with( + self.backup.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_volume_host.py b/openstackclient/tests/unit/volume/v2/test_volume_host.py index 498ab0d383..ca9605c8d7 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_host.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_host.py @@ -10,98 +10,66 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# + +from openstack.block_storage.v2 import service as _service +from openstack.test import fakes as sdk_fakes from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume_host -class TestVolumeHost(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.host_mock = self.volume_client.services - self.host_mock.reset_mock() - - -class TestVolumeHostSet(TestVolumeHost): - service = volume_fakes.create_one_service() - +class TestVolumeHostSet(volume_fakes.TestVolume): def setUp(self): super().setUp() - - self.host_mock.freeze_host.return_value = None - self.host_mock.thaw_host.return_value = None - - # Get the command object to mock + self.service = sdk_fakes.generate_fake_resource(_service.Service) self.cmd = volume_host.SetVolumeHost(self.app, None) def test_volume_host_set_nothing(self): - arglist = [ - self.service.host, - ] - verifylist = [ - ('host', self.service.host), - ] + arglist = [self.service.host] + verifylist = [('host', self.service.host)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) - self.host_mock.freeze_host.assert_not_called() - self.host_mock.thaw_host.assert_not_called() + self.volume_client.freeze_service.assert_not_called() + self.volume_client.thaw_service.assert_not_called() self.assertIsNone(result) def test_volume_host_set_enable(self): - arglist = [ - '--enable', - self.service.host, - ] - verifylist = [ - ('enable', True), - ('host', self.service.host), - ] + arglist = ['--enable', self.service.host] + verifylist = [('enable', True), ('host', self.service.host)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.host_mock.thaw_host.assert_called_with(self.service.host) - self.host_mock.freeze_host.assert_not_called() + self.volume_client.thaw_service.assert_called_once_with( + _service.Service(host=self.service.host) + ) + self.volume_client.freeze_service.assert_not_called() self.assertIsNone(result) def test_volume_host_set_disable(self): - arglist = [ - '--disable', - self.service.host, - ] - verifylist = [ - ('disable', True), - ('host', self.service.host), - ] + arglist = ['--disable', self.service.host] + verifylist = [('disable', True), ('host', self.service.host)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.host_mock.freeze_host.assert_called_with(self.service.host) - self.host_mock.thaw_host.assert_not_called() + self.volume_client.freeze_service.assert_called_once_with( + _service.Service(host=self.service.host) + ) + self.volume_client.thaw_service.assert_not_called() self.assertIsNone(result) -class TestVolumeHostFailover(TestVolumeHost): - service = volume_fakes.create_one_service() - +class TestVolumeHostFailover(volume_fakes.TestVolume): def setUp(self): super().setUp() - - self.host_mock.failover_host.return_value = None - - # Get the command object to mock + self.service = sdk_fakes.generate_fake_resource(_service.Service) self.cmd = volume_host.FailoverVolumeHost(self.app, None) def test_volume_host_failover(self): - arglist = [ - '--volume-backend', - 'backend_test', - self.service.host, - ] + arglist = ['--volume-backend', 'backend_test', self.service.host] verifylist = [ ('volume_backend', 'backend_test'), ('host', self.service.host), @@ -110,7 +78,8 @@ def test_volume_host_failover(self): result = self.cmd.take_action(parsed_args) - self.host_mock.failover_host.assert_called_with( - self.service.host, 'backend_test' + self.volume_client.failover_service.assert_called_once_with( + _service.Service(host=self.service.host), + backend_id='backend_test', ) self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py b/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py index 6e7e02ae0f..122cddf5ab 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py @@ -13,29 +13,20 @@ from unittest import mock +from openstack.block_storage.v2 import snapshot as _snapshot +from openstack.block_storage.v3 import volume as _volume +from openstack import exceptions as sdk_exceptions +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils -from openstackclient.tests.unit.identity.v3 import fakes as project_fakes from openstackclient.tests.unit import utils as test_utils from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume_snapshot -class TestVolumeSnapshot(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.snapshots_mock = self.volume_client.volume_snapshots - self.snapshots_mock.reset_mock() - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - self.project_mock = self.identity_client.projects - self.project_mock.reset_mock() - - -class TestVolumeSnapshotCreate(TestVolumeSnapshot): +class TestVolumeSnapshotCreate(volume_fakes.TestVolume): columns = ( 'created_at', 'description', @@ -50,69 +41,71 @@ class TestVolumeSnapshotCreate(TestVolumeSnapshot): def setUp(self): super().setUp() - self.volume = volume_fakes.create_one_volume() - self.new_snapshot = volume_fakes.create_one_snapshot( - attrs={'volume_id': self.volume.id} + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.snapshot = sdk_fakes.generate_fake_resource( + _snapshot.Snapshot, volume_id=self.volume.id ) + self.volume_client.create_snapshot.return_value = self.snapshot + self.volume_client.manage_snapshot.return_value = self.snapshot self.data = ( - self.new_snapshot.created_at, - self.new_snapshot.description, - self.new_snapshot.id, - self.new_snapshot.name, - format_columns.DictColumn(self.new_snapshot.metadata), - self.new_snapshot.size, - self.new_snapshot.status, - self.new_snapshot.volume_id, - ) - - self.volumes_mock.get.return_value = self.volume - self.snapshots_mock.create.return_value = self.new_snapshot - self.snapshots_mock.manage.return_value = self.new_snapshot - # Get the command object to test + self.snapshot.created_at, + self.snapshot.description, + self.snapshot.id, + self.snapshot.name, + format_columns.DictColumn(self.snapshot.metadata), + self.snapshot.size, + self.snapshot.status, + self.snapshot.volume_id, + ) + self.cmd = volume_snapshot.CreateVolumeSnapshot(self.app, None) def test_snapshot_create(self): arglist = [ "--volume", - self.new_snapshot.volume_id, + self.snapshot.volume_id, "--description", - self.new_snapshot.description, + self.snapshot.description, "--force", '--property', 'Alpha=a', '--property', 'Beta=b', - self.new_snapshot.name, + self.snapshot.name, ] verifylist = [ - ("volume", self.new_snapshot.volume_id), - ("description", self.new_snapshot.description), + ("volume", self.snapshot.volume_id), + ("description", self.snapshot.description), ("force", True), - ('property', {'Alpha': 'a', 'Beta': 'b'}), - ("snapshot_name", self.new_snapshot.name), + ('properties', {'Alpha': 'a', 'Beta': 'b'}), + ("snapshot_name", self.snapshot.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.create.assert_called_with( - self.new_snapshot.volume_id, + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + self.volume_client.find_volume.assert_called_once_with( + self.snapshot.volume_id, ignore_missing=False + ) + self.volume_client.create_snapshot.assert_called_with( + volume_id=self.snapshot.volume_id, force=True, - name=self.new_snapshot.name, - description=self.new_snapshot.description, + name=self.snapshot.name, + description=self.snapshot.description, metadata={'Alpha': 'a', 'Beta': 'b'}, ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) def test_snapshot_create_without_name(self): arglist = [ "--volume", - self.new_snapshot.volume_id, + self.snapshot.volume_id, ] verifylist = [ - ("volume", self.new_snapshot.volume_id), + ("volume", self.snapshot.volume_id), ] self.assertRaises( test_utils.ParserException, @@ -125,29 +118,31 @@ def test_snapshot_create_without_name(self): def test_snapshot_create_without_volume(self): arglist = [ "--description", - self.new_snapshot.description, + self.snapshot.description, "--force", - self.new_snapshot.name, + self.snapshot.name, ] verifylist = [ - ("description", self.new_snapshot.description), + ("description", self.snapshot.description), ("force", True), - ("snapshot_name", self.new_snapshot.name), + ("snapshot_name", self.snapshot.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.get.assert_called_once_with(self.new_snapshot.name) - self.snapshots_mock.create.assert_called_once_with( - self.new_snapshot.volume_id, + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + self.volume_client.find_volume.assert_called_once_with( + self.snapshot.name, ignore_missing=False + ) + self.volume_client.create_snapshot.assert_called_once_with( + volume_id=self.snapshot.volume_id, force=True, - name=self.new_snapshot.name, - description=self.new_snapshot.description, + name=self.snapshot.name, + description=self.snapshot.description, metadata=None, ) - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) def test_snapshot_create_with_remote_source(self): arglist = [ @@ -156,8 +151,8 @@ def test_snapshot_create_with_remote_source(self): '--remote-source', 'source-id=test_source_id', '--volume', - self.new_snapshot.volume_id, - self.new_snapshot.name, + self.snapshot.volume_id, + self.snapshot.name, ] ref_dict = { 'source-name': 'test_source_name', @@ -165,35 +160,38 @@ def test_snapshot_create_with_remote_source(self): } verifylist = [ ('remote_source', ref_dict), - ('volume', self.new_snapshot.volume_id), - ("snapshot_name", self.new_snapshot.name), + ('volume', self.snapshot.volume_id), + ("snapshot_name", self.snapshot.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.manage.assert_called_with( - volume_id=self.new_snapshot.volume_id, + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + self.volume_client.find_volume.assert_called_once_with( + self.snapshot.volume_id, ignore_missing=False + ) + self.volume_client.manage_snapshot.assert_called_with( + volume_id=self.snapshot.volume_id, ref=ref_dict, - name=self.new_snapshot.name, + name=self.snapshot.name, description=None, metadata=None, ) - self.snapshots_mock.create.assert_not_called() - self.assertEqual(self.columns, columns) - self.assertEqual(self.data, data) + self.volume_client.create_snapshot.assert_not_called() -class TestVolumeSnapshotDelete(TestVolumeSnapshot): - snapshots = volume_fakes.create_snapshots(count=2) - +class TestVolumeSnapshotDelete(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.snapshots_mock.get = volume_fakes.get_snapshots(self.snapshots) - self.snapshots_mock.delete.return_value = None + self.snapshots = list( + sdk_fakes.generate_fake_resources(_snapshot.Snapshot) + ) + self.volume_client.find_snapshot.side_effect = self.snapshots + self.volume_client.delete_snapshot.return_value = None - # Get the command object to mock self.cmd = volume_snapshot.DeleteVolumeSnapshot(self.app, None) def test_snapshot_delete(self): @@ -202,11 +200,14 @@ def test_snapshot_delete(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.snapshots_mock.delete.assert_called_with( - self.snapshots[0].id, False + self.volume_client.find_snapshot.assert_called_once_with( + self.snapshots[0].id, ignore_missing=False + ) + self.volume_client.delete_snapshot.assert_called_once_with( + self.snapshots[0].id, force=False ) - self.assertIsNone(result) def test_snapshot_delete_with_force(self): arglist = ['--force', self.snapshots[0].id] @@ -214,11 +215,14 @@ def test_snapshot_delete_with_force(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.snapshots_mock.delete.assert_called_with( - self.snapshots[0].id, True + self.volume_client.find_snapshot.assert_called_once_with( + self.snapshots[0].id, ignore_missing=False + ) + self.volume_client.delete_snapshot.assert_called_once_with( + self.snapshots[0].id, force=True ) - self.assertIsNone(result) def test_delete_multiple_snapshots(self): arglist = [] @@ -227,17 +231,24 @@ def test_delete_multiple_snapshots(self): verifylist = [ ('snapshots', arglist), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - calls = [] - for s in self.snapshots: - calls.append(mock.call(s.id, False)) - self.snapshots_mock.delete.assert_has_calls(calls) + result = self.cmd.take_action(parsed_args) self.assertIsNone(result) + self.volume_client.find_snapshot.assert_has_calls( + [mock.call(x.id, ignore_missing=False) for x in self.snapshots] + ) + self.volume_client.delete_snapshot.assert_has_calls( + [mock.call(x.id, force=False) for x in self.snapshots] + ) + def test_delete_multiple_snapshots_with_exception(self): + self.volume_client.find_snapshot.side_effect = [ + self.snapshots[0], + sdk_exceptions.NotFoundException(), + ] + arglist = [ self.snapshots[0].id, 'unexist_snapshot', @@ -248,73 +259,75 @@ def test_delete_multiple_snapshots_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.snapshots[0], exceptions.CommandError] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('1 of 2 snapshots failed to delete.', str(e)) - - find_mock.assert_any_call( - self.snapshots_mock, self.snapshots[0].id - ) - find_mock.assert_any_call(self.snapshots_mock, 'unexist_snapshot') + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertEqual('1 of 2 snapshots failed to delete.', str(exc)) - self.assertEqual(2, find_mock.call_count) - self.snapshots_mock.delete.assert_called_once_with( - self.snapshots[0].id, False - ) + self.volume_client.find_snapshot.assert_has_calls( + [ + mock.call(self.snapshots[0].id, ignore_missing=False), + mock.call('unexist_snapshot', ignore_missing=False), + ] + ) + self.volume_client.delete_snapshot.assert_has_calls( + [ + mock.call(self.snapshots[0].id, force=False), + ] + ) -class TestVolumeSnapshotList(TestVolumeSnapshot): - volume = volume_fakes.create_one_volume() - project = project_fakes.FakeProject.create_one_project() - snapshots = volume_fakes.create_snapshots( - attrs={'volume_id': volume.name}, count=3 - ) +class TestVolumeSnapshotList(volume_fakes.TestVolume): + def setUp(self): + super().setUp() - columns = ["ID", "Name", "Description", "Status", "Size"] - columns_long = columns + ["Created At", "Volume", "Properties"] - - data = [] - for s in snapshots: - data.append( - ( - s.id, - s.name, - s.description, - s.status, - s.size, + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.snapshots = list( + sdk_fakes.generate_fake_resources( + _snapshot.Snapshot, attrs={'volume_id': self.volume.name} ) ) - data_long = [] - for s in snapshots: - data_long.append( - ( - s.id, - s.name, - s.description, - s.status, - s.size, - s.created_at, - volume_snapshot.VolumeIdColumn( - s.volume_id, volume_cache={volume.id: volume} - ), - format_columns.DictColumn(s.metadata), - ) + self.volume_client.volumes.return_value = [self.volume] + self.volume_client.find_volume.return_value = self.volume + self.volume_client.snapshots.return_value = self.snapshots + + self.columns = ("ID", "Name", "Description", "Status", "Size") + self.columns_long = ( + *self.columns, + "Created At", + "Volume", + "Properties", ) - def setUp(self): - super().setUp() + self.data = [] + self.data_long = [] + for s in self.snapshots: + self.data.append( + ( + s.id, + s.name, + s.description, + s.status, + s.size, + ) + ) + self.data_long.append( + ( + s.id, + s.name, + s.description, + s.status, + s.size, + s.created_at, + volume_snapshot.VolumeIdColumn( + s.volume_id, volume_cache={self.volume.id: self.volume} + ), + format_columns.DictColumn(s.metadata), + ) + ) - self.volumes_mock.list.return_value = [self.volume] - self.volumes_mock.get.return_value = self.volume - self.project_mock.get.return_value = self.project - self.snapshots_mock.list.return_value = self.snapshots - # Get the command to test self.cmd = volume_snapshot.ListVolumeSnapshot(self.app, None) def test_snapshot_list_without_options(self): @@ -324,34 +337,36 @@ def test_snapshot_list_without_options(self): columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.list.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, - search_opts={ - 'all_tenants': False, - 'name': None, - 'status': None, - 'project_id': None, - 'volume_id': None, - }, + max_items=None, + all_projects=False, + name=None, + status=None, + project_id=None, + volume_id=None, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) def test_snapshot_list_with_options(self): + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project + arglist = [ "--long", "--limit", "2", "--project", - self.project.id, + project.id, "--marker", self.snapshots[0].id, ] verifylist = [ ("long", True), ("limit", 2), - ("project", self.project.id), + ("project", project.id), ("marker", self.snapshots[0].id), ('all_projects', False), ] @@ -359,16 +374,15 @@ def test_snapshot_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.list.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=2, marker=self.snapshots[0].id, - search_opts={ - 'all_tenants': True, - 'project_id': self.project.id, - 'name': None, - 'status': None, - 'volume_id': None, - }, + max_items=None, + all_projects=True, + project_id=project.id, + name=None, + status=None, + volume_id=None, ) self.assertEqual(self.columns_long, columns) self.assertEqual(self.data_long, list(data)) @@ -382,16 +396,15 @@ def test_snapshot_list_all_projects(self): columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.list.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, - search_opts={ - 'all_tenants': True, - 'name': None, - 'status': None, - 'project_id': None, - 'volume_id': None, - }, + max_items=None, + all_projects=True, + name=None, + status=None, + project_id=None, + volume_id=None, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -410,16 +423,15 @@ def test_snapshot_list_name_option(self): columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.list.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, - search_opts={ - 'all_tenants': False, - 'name': self.snapshots[0].name, - 'status': None, - 'project_id': None, - 'volume_id': None, - }, + max_items=None, + all_projects=False, + name=self.snapshots[0].name, + status=None, + project_id=None, + volume_id=None, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -427,27 +439,26 @@ def test_snapshot_list_name_option(self): def test_snapshot_list_status_option(self): arglist = [ '--status', - self.snapshots[0].status, + 'available', ] verifylist = [ ('all_projects', False), ('long', False), - ('status', self.snapshots[0].status), + ('status', 'available'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.list.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, - search_opts={ - 'all_tenants': False, - 'name': None, - 'status': self.snapshots[0].status, - 'project_id': None, - 'volume_id': None, - }, + max_items=None, + all_projects=False, + name=None, + status='available', + project_id=None, + volume_id=None, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -466,16 +477,15 @@ def test_snapshot_list_volumeid_option(self): columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.list.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, - search_opts={ - 'all_tenants': False, - 'name': None, - 'status': None, - 'project_id': None, - 'volume_id': self.volume.id, - }, + max_items=None, + all_projects=False, + name=None, + status=None, + project_id=None, + volume_id=self.volume.id, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, list(data)) @@ -497,16 +507,18 @@ def test_snapshot_list_negative_limit(self): ) -class TestVolumeSnapshotSet(TestVolumeSnapshot): - snapshot = volume_fakes.create_one_snapshot() - +class TestVolumeSnapshotSet(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.snapshots_mock.get.return_value = self.snapshot - self.snapshots_mock.set_metadata.return_value = None - self.snapshots_mock.update.return_value = None - # Get the command object to mock + self.snapshot = sdk_fakes.generate_fake_resource( + _snapshot.Snapshot, metadata={'foo': 'bar'} + ) + self.volume_client.find_snapshot.return_value = self.snapshot + self.volume_client.delete_snapshot_metadata.return_value = None + self.volume_client.set_snapshot_metadata.return_value = None + self.volume_client.update_snapshot.return_value = None + self.cmd = volume_snapshot.SetVolumeSnapshot(self.app, None) def test_snapshot_set_no_option(self): @@ -519,11 +531,14 @@ def test_snapshot_set_no_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) - self.assertNotCalled(self.snapshots_mock.reset_state) - self.assertNotCalled(self.snapshots_mock.update) - self.assertNotCalled(self.snapshots_mock.set_metadata) + self.assertIsNone(result) + self.volume_client.find_snapshot.assert_called_once_with( + parsed_args.snapshot, ignore_missing=False + ) + self.volume_client.reset_snapshot_status.assert_not_called() + self.volume_client.update_snapshot.assert_not_called() + self.volume_client.set_snapshot_metadata.assert_not_called() def test_snapshot_set_name_and_property(self): arglist = [ @@ -535,26 +550,22 @@ def test_snapshot_set_name_and_property(self): "foo=foo", self.snapshot.id, ] - new_property = {"x": "y", "foo": "foo"} verifylist = [ ("name", "new_snapshot"), - ("property", new_property), + ("properties", {"x": "y", "foo": "foo"}), ("snapshot", self.snapshot.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - kwargs = { - "name": "new_snapshot", - } - self.snapshots_mock.update.assert_called_with( - self.snapshot.id, **kwargs + self.assertIsNone(result) + self.volume_client.update_snapshot.assert_called_with( + self.snapshot.id, name="new_snapshot" ) - self.snapshots_mock.set_metadata.assert_called_with( - self.snapshot.id, new_property + self.volume_client.set_snapshot_metadata.assert_called_with( + self.snapshot.id, x="y", foo="foo" ) - self.assertIsNone(result) def test_snapshot_set_with_no_property(self): arglist = [ @@ -568,14 +579,17 @@ def test_snapshot_set_with_no_property(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) - self.assertNotCalled(self.snapshots_mock.reset_state) - self.assertNotCalled(self.snapshots_mock.update) - self.assertNotCalled(self.snapshots_mock.set_metadata) - self.snapshots_mock.delete_metadata.assert_called_with( - self.snapshot.id, ["foo"] - ) + self.assertIsNone(result) + self.volume_client.find_snapshot.assert_called_once_with( + parsed_args.snapshot, ignore_missing=False + ) + self.volume_client.reset_snapshot_status.assert_not_called() + self.volume_client.update_snapshot.assert_not_called() + self.volume_client.set_snapshot_metadata.assert_not_called() + self.volume_client.delete_snapshot_metadata.assert_called_with( + self.snapshot.id, keys=["foo"] + ) def test_snapshot_set_with_no_property_and_property(self): arglist = [ @@ -586,22 +600,26 @@ def test_snapshot_set_with_no_property_and_property(self): ] verifylist = [ ("no_property", True), - ("property", {"foo_1": "bar_1"}), + ("properties", {"foo_1": "bar_1"}), ("snapshot", self.snapshot.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.snapshots_mock.get.assert_called_once_with(parsed_args.snapshot) - self.assertNotCalled(self.snapshots_mock.reset_state) - self.assertNotCalled(self.snapshots_mock.update) - self.snapshots_mock.delete_metadata.assert_called_with( - self.snapshot.id, ["foo"] + + self.assertIsNone(result) + self.volume_client.find_snapshot.assert_called_once_with( + parsed_args.snapshot, ignore_missing=False ) - self.snapshots_mock.set_metadata.assert_called_once_with( - self.snapshot.id, {"foo_1": "bar_1"} + self.volume_client.reset_snapshot_status.assert_not_called() + self.volume_client.update_snapshot.assert_not_called() + self.volume_client.delete_snapshot_metadata.assert_called_with( + self.snapshot.id, keys=["foo"] + ) + self.volume_client.set_snapshot_metadata.assert_called_once_with( + self.snapshot.id, + foo_1="bar_1", ) - self.assertIsNone(result) def test_snapshot_set_state_to_error(self): arglist = ["--state", "error", self.snapshot.id] @@ -610,30 +628,32 @@ def test_snapshot_set_state_to_error(self): result = self.cmd.take_action(parsed_args) - self.snapshots_mock.reset_state.assert_called_with( + self.assertIsNone(result) + self.volume_client.reset_snapshot_status.assert_called_with( self.snapshot.id, "error" ) - self.assertIsNone(result) def test_volume_set_state_failed(self): - self.snapshots_mock.reset_state.side_effect = exceptions.CommandError() + self.volume_client.reset_snapshot_status.side_effect = ( + exceptions.CommandError() + ) arglist = ['--state', 'error', self.snapshot.id] verifylist = [('state', 'error'), ('snapshot', self.snapshot.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - 'One or more of the set operations failed', str(e) - ) - self.snapshots_mock.reset_state.assert_called_once_with( + + exc = self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + self.assertEqual('One or more of the set operations failed', str(exc)) + self.volume_client.reset_snapshot_status.assert_called_once_with( self.snapshot.id, 'error' ) def test_volume_set_name_and_state_failed(self): - self.snapshots_mock.reset_state.side_effect = exceptions.CommandError() + self.volume_client.reset_snapshot_status.side_effect = ( + exceptions.CommandError() + ) arglist = [ '--state', 'error', @@ -646,43 +666,39 @@ def test_volume_set_name_and_state_failed(self): ("name", "new_snapshot"), ('snapshot', self.snapshot.id), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - 'One or more of the set operations failed', str(e) - ) - kwargs = { - "name": "new_snapshot", - } - self.snapshots_mock.update.assert_called_once_with( - self.snapshot.id, **kwargs + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, ) - self.snapshots_mock.reset_state.assert_called_once_with( + + self.assertEqual('One or more of the set operations failed', str(exc)) + self.volume_client.update_snapshot.assert_called_once_with( + self.snapshot.id, name="new_snapshot" + ) + self.volume_client.reset_snapshot_status.assert_called_once_with( self.snapshot.id, 'error' ) -class TestVolumeSnapshotShow(TestVolumeSnapshot): - columns = ( - 'created_at', - 'description', - 'id', - 'name', - 'properties', - 'size', - 'status', - 'volume_id', - ) - +class TestVolumeSnapshotShow(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.snapshot = volume_fakes.create_one_snapshot() - + self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) + + self.columns = ( + 'created_at', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'status', + 'volume_id', + ) self.data = ( self.snapshot.created_at, self.snapshot.description, @@ -694,8 +710,8 @@ def setUp(self): self.snapshot.volume_id, ) - self.snapshots_mock.get.return_value = self.snapshot - # Get the command object to test + self.volume_client.find_snapshot.return_value = self.snapshot + self.cmd = volume_snapshot.ShowVolumeSnapshot(self.app, None) def test_snapshot_show(self): @@ -704,21 +720,22 @@ def test_snapshot_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.snapshots_mock.get.assert_called_with(self.snapshot.id) + self.volume_client.find_snapshot.assert_called_with( + self.snapshot.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) -class TestVolumeSnapshotUnset(TestVolumeSnapshot): - snapshot = volume_fakes.create_one_snapshot() - +class TestVolumeSnapshotUnset(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.snapshots_mock.get.return_value = self.snapshot - self.snapshots_mock.delete_metadata.return_value = None - # Get the command object to mock + self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) + self.volume_client.find_snapshot.return_value = self.snapshot + self.volume_client.delete_snapshot_metadata.return_value = None + self.cmd = volume_snapshot.UnsetVolumeSnapshot(self.app, None) def test_snapshot_unset(self): @@ -728,7 +745,7 @@ def test_snapshot_unset(self): self.snapshot.id, ] verifylist = [ - ("property", ["foo"]), + ("properties", ["foo"]), ("snapshot", self.snapshot.id), ] @@ -736,7 +753,7 @@ def test_snapshot_unset(self): result = self.cmd.take_action(parsed_args) - self.snapshots_mock.delete_metadata.assert_called_with( - self.snapshot.id, ["foo"] - ) self.assertIsNone(result) + self.volume_client.delete_snapshot_metadata.assert_called_with( + self.snapshot.id, keys=["foo"] + ) diff --git a/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py b/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py index 2677ddc10a..1b51bfe6af 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_transfer_request.py @@ -12,31 +12,20 @@ # under the License. # -from unittest import mock from unittest.mock import call +from openstack.block_storage.v2 import transfer as _transfer +from openstack.block_storage.v3 import volume as _volume +from openstack import exceptions as sdk_exceptions +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions -from osc_lib import utils from openstackclient.tests.unit import utils as test_utils from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume_transfer_request -class TestTransfer(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - # Get a shortcut to the TransferManager Mock - self.transfer_mock = self.volume_client.transfers - self.transfer_mock.reset_mock() - - # Get a shortcut to the VolumeManager Mock - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - - -class TestTransferAccept(TestTransfer): +class TestTransferAccept(volume_fakes.TestVolume): columns = ( 'id', 'name', @@ -46,17 +35,18 @@ class TestTransferAccept(TestTransfer): def setUp(self): super().setUp() - self.volume_transfer = volume_fakes.create_one_transfer() + self.volume_transfer = sdk_fakes.generate_fake_resource( + _transfer.Transfer + ) self.data = ( self.volume_transfer.id, self.volume_transfer.name, self.volume_transfer.volume_id, ) - self.transfer_mock.get.return_value = self.volume_transfer - self.transfer_mock.accept.return_value = self.volume_transfer + self.volume_client.find_transfer.return_value = self.volume_transfer + self.volume_client.accept_transfer.return_value = self.volume_transfer - # Get the command object to test self.cmd = volume_transfer_request.AcceptTransferRequest( self.app, None ) @@ -75,23 +65,44 @@ def test_transfer_accept(self): columns, data = self.cmd.take_action(parsed_args) - self.transfer_mock.get.assert_called_once_with( - self.volume_transfer.id, + self.volume_client.find_transfer.assert_called_once_with( + self.volume_transfer.id, ignore_missing=False ) - self.transfer_mock.accept.assert_called_once_with( + self.volume_client.accept_transfer.assert_called_once_with( self.volume_transfer.id, 'key_value', ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - def test_transfer_accept_no_option(self): + def test_transfer_accept_non_admin(self): + """Non-admin users get ResourceNotFound on find_transfer; we fall back.""" + self.volume_client.find_transfer.side_effect = ( + sdk_exceptions.ResourceNotFound + ) arglist = [ + '--auth-key', + 'key_value', self.volume_transfer.id, ] verifylist = [ ('transfer_request', self.volume_transfer.id), + ('auth_key', 'key_value'), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.accept_transfer.assert_called_once_with( + self.volume_transfer.id, + 'key_value', + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_transfer_accept_no_option(self): + arglist = [self.volume_transfer.id] + verifylist = [('transfer_request', self.volume_transfer.id)] self.assertRaises( test_utils.ParserException, @@ -102,9 +113,7 @@ def test_transfer_accept_no_option(self): ) -class TestTransferCreate(TestTransfer): - volume = volume_fakes.create_one_volume() - +class TestTransferCreate(volume_fakes.TestVolume): columns = ( 'auth_key', 'created_at', @@ -116,12 +125,9 @@ class TestTransferCreate(TestTransfer): def setUp(self): super().setUp() - self.volume_transfer = volume_fakes.create_one_transfer( - attrs={ - 'volume_id': self.volume.id, - 'auth_key': 'key', - 'created_at': 'time', - }, + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_transfer = sdk_fakes.generate_fake_resource( + _transfer.Transfer, volume_id=self.volume.id ) self.data = ( self.volume_transfer.auth_key, @@ -131,26 +137,24 @@ def setUp(self): self.volume_transfer.volume_id, ) - self.transfer_mock.create.return_value = self.volume_transfer - self.volumes_mock.get.return_value = self.volume + self.volume_client.find_volume.return_value = self.volume + self.volume_client.create_transfer.return_value = self.volume_transfer - # Get the command object to test self.cmd = volume_transfer_request.CreateTransferRequest( self.app, None ) def test_transfer_create_without_name(self): - arglist = [ - self.volume.id, - ] - verifylist = [ - ('volume', self.volume.id), - ] + arglist = [self.volume.id] + verifylist = [('volume', self.volume.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.transfer_mock.create.assert_called_once_with(self.volume.id, None) + self.volume_client.create_transfer.assert_called_once_with( + volume_id=self.volume.id, + name=None, + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -168,26 +172,24 @@ def test_transfer_create_with_name(self): columns, data = self.cmd.take_action(parsed_args) - self.transfer_mock.create.assert_called_once_with( - self.volume.id, - self.volume_transfer.name, + self.volume_client.create_transfer.assert_called_once_with( + volume_id=self.volume.id, + name=self.volume_transfer.name, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) -class TestTransferDelete(TestTransfer): - volume_transfers = volume_fakes.create_transfers(count=2) - +class TestTransferDelete(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.transfer_mock.get = volume_fakes.get_transfers( - self.volume_transfers, - ) - self.transfer_mock.delete.return_value = None + self.volume_transfers = [ + sdk_fakes.generate_fake_resource(_transfer.Transfer), + sdk_fakes.generate_fake_resource(_transfer.Transfer), + ] + self.volume_client.delete_transfer.return_value = None - # Get the command object to mock self.cmd = volume_transfer_request.DeleteTransferRequest( self.app, None ) @@ -199,26 +201,22 @@ def test_transfer_delete(self): result = self.cmd.take_action(parsed_args) - self.transfer_mock.delete.assert_called_with( - self.volume_transfers[0].id + self.volume_client.delete_transfer.assert_called_once_with( + self.volume_transfers[0].id, ignore_missing=False ) self.assertIsNone(result) def test_delete_multiple_transfers(self): - arglist = [] - for v in self.volume_transfers: - arglist.append(v.id) - verifylist = [ - ('transfer_request', arglist), - ] + arglist = [v.id for v in self.volume_transfers] + verifylist = [('transfer_request', arglist)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - calls = [] - for v in self.volume_transfers: - calls.append(call(v.id)) - self.transfer_mock.delete.assert_has_calls(calls) + calls = [ + call(v.id, ignore_missing=False) for v in self.volume_transfers + ] + self.volume_client.delete_transfer.assert_has_calls(calls) self.assertIsNone(result) def test_delete_multiple_transfers_with_exception(self): @@ -226,46 +224,33 @@ def test_delete_multiple_transfers_with_exception(self): self.volume_transfers[0].id, 'unexist_transfer', ] - verifylist = [ - ('transfer_request', arglist), - ] - + verifylist = [('transfer_request', arglist)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.volume_transfers[0], exceptions.CommandError] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - '1 of 2 volume transfer requests failed to delete', - str(e), - ) - - find_mock.assert_any_call( - self.transfer_mock, self.volume_transfers[0].id - ) - find_mock.assert_any_call(self.transfer_mock, 'unexist_transfer') + self.volume_client.delete_transfer.side_effect = [ + None, + exceptions.CommandError, + ] - self.assertEqual(2, find_mock.call_count) - self.transfer_mock.delete.assert_called_once_with( - self.volume_transfers[0].id, + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual( + '1 of 2 volume transfer requests failed to delete', + str(e), ) -class TestTransferList(TestTransfer): - # The Transfers to be listed - volume_transfers = volume_fakes.create_one_transfer() - +class TestTransferList(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.transfer_mock.list.return_value = [self.volume_transfers] + self.volume_transfers = [ + sdk_fakes.generate_fake_resource(_transfer.Transfer), + ] + self.volume_client.transfers.return_value = self.volume_transfers - # Get the command object to test self.cmd = volume_transfer_request.ListTransferRequest(self.app, None) def test_transfer_list_without_argument(self): @@ -273,74 +258,37 @@ def test_transfer_list_without_argument(self): verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - expected_columns = [ - 'ID', - 'Name', - 'Volume', - ] - - # confirming if all expected columns are present in the result. - self.assertEqual(expected_columns, columns) - - datalist = ( + self.volume_client.transfers.assert_called_once_with( + details=True, all_projects=False + ) + self.assertEqual(('ID', 'Name', 'Volume'), columns) + self.assertEqual( ( - self.volume_transfers.id, - self.volume_transfers.name, - self.volume_transfers.volume_id, + ( + self.volume_transfers[0].id, + self.volume_transfers[0].name, + self.volume_transfers[0].volume_id, + ), ), - ) - - # confirming if all expected values are present in the result. - self.assertEqual(datalist, tuple(data)) - - # checking if proper call was made to list volume_transfers - self.transfer_mock.list.assert_called_with( - detailed=True, search_opts={'all_tenants': 0} + tuple(data), ) def test_transfer_list_with_argument(self): arglist = ["--all-projects"] verifylist = [("all_projects", True)] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. - columns, data = self.cmd.take_action(parsed_args) - - expected_columns = [ - 'ID', - 'Name', - 'Volume', - ] - - # confirming if all expected columns are present in the result. - self.assertEqual(expected_columns, columns) - - datalist = ( - ( - self.volume_transfers.id, - self.volume_transfers.name, - self.volume_transfers.volume_id, - ), - ) - - # confirming if all expected values are present in the result. - self.assertEqual(datalist, tuple(data)) + columns, _data = self.cmd.take_action(parsed_args) - # checking if proper call was made to list volume_transfers - self.transfer_mock.list.assert_called_with( - detailed=True, search_opts={'all_tenants': 1} + self.volume_client.transfers.assert_called_once_with( + details=True, all_projects=True ) + self.assertEqual(('ID', 'Name', 'Volume'), columns) -class TestTransferShow(TestTransfer): +class TestTransferShow(volume_fakes.TestVolume): columns = ( 'created_at', 'id', @@ -351,8 +299,8 @@ class TestTransferShow(TestTransfer): def setUp(self): super().setUp() - self.volume_transfer = volume_fakes.create_one_transfer( - attrs={'created_at': 'time'}, + self.volume_transfer = sdk_fakes.generate_fake_resource( + _transfer.Transfer ) self.data = ( self.volume_transfer.created_at, @@ -361,22 +309,19 @@ def setUp(self): self.volume_transfer.volume_id, ) - self.transfer_mock.get.return_value = self.volume_transfer + self.volume_client.find_transfer.return_value = self.volume_transfer - # Get the command object to test self.cmd = volume_transfer_request.ShowTransferRequest(self.app, None) def test_transfer_show(self): - arglist = [ - self.volume_transfer.id, - ] - verifylist = [ - ('transfer_request', self.volume_transfer.id), - ] + arglist = [self.volume_transfer.id] + verifylist = [('transfer_request', self.volume_transfer.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.transfer_mock.get.assert_called_once_with(self.volume_transfer.id) + self.volume_client.find_transfer.assert_called_once_with( + self.volume_transfer.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v2/test_volume_type.py b/openstackclient/tests/unit/volume/v2/test_volume_type.py index 6f50ff2ef6..c288794d87 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_type.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_type.py @@ -10,64 +10,45 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# -from unittest import mock from unittest.mock import call +from openstack.block_storage.v3 import type as _type +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit import utils as tests_utils from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes from openstackclient.volume.v2 import volume_type -class TestType(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.volume_types_mock = self.volume_client.volume_types - self.volume_types_mock.reset_mock() - - self.volume_type_access_mock = self.volume_client.volume_type_access - self.volume_type_access_mock.reset_mock() - - self.volume_encryption_types_mock = ( - self.volume_client.volume_encryption_types - ) - self.volume_encryption_types_mock.reset_mock() - - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() - - -class TestTypeCreate(TestType): +class TestTypeCreate(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.new_volume_type = volume_fakes.create_one_volume_type( - methods={'set_keys': None}, - ) - self.project = identity_fakes.FakeProject.create_one_project() + self.new_volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.project = sdk_fakes.generate_fake_resource(_project.Project) self.columns = ( 'description', 'id', 'is_public', 'name', + 'properties', ) self.data = ( self.new_volume_type.description, self.new_volume_type.id, - True, + self.new_volume_type.is_public, self.new_volume_type.name, + format_columns.DictColumn(self.new_volume_type.extra_specs), ) - self.volume_types_mock.create.return_value = self.new_volume_type - self.projects_mock.get.return_value = self.project - # Get the command object to test + self.volume_client.create_type.return_value = self.new_volume_type + self.identity_sdk_client.find_project.return_value = self.project + self.cmd = volume_type.CreateVolumeType(self.app, None) def test_type_create_public(self): @@ -85,8 +66,8 @@ def test_type_create_public(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.create.assert_called_with( - self.new_volume_type.name, + self.volume_client.create_type.assert_called_with( + name=self.new_volume_type.name, description=self.new_volume_type.description, is_public=True, ) @@ -112,8 +93,8 @@ def test_type_create_private(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.create.assert_called_with( - self.new_volume_type.name, + self.volume_client.create_type.assert_called_with( + name=self.new_volume_type.name, description=self.new_volume_type.description, is_public=False, ) @@ -143,25 +124,41 @@ def test_type_create_with_properties(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.create.assert_called_with( - self.new_volume_type.name, description=None - ) - self.new_volume_type.set_keys.assert_called_once_with( - { + result_type = sdk_fakes.generate_fake_resource( + _type.Type, + extra_specs={ 'myprop': 'myvalue', 'multiattach': ' True', 'cacheable': ' True', 'replication_enabled': ' True', 'RESKEY:availability_zones': 'az1', - } + }, ) + self.volume_client.update_type_extra_specs.return_value = result_type - self.columns += ('properties',) - self.data += (format_columns.DictColumn(None),) + columns, data = self.cmd.take_action(parsed_args) + self.volume_client.create_type.assert_called_with( + name=self.new_volume_type.name, description=None + ) + self.volume_client.update_type_extra_specs.assert_called_once_with( + self.new_volume_type.id, + myprop='myvalue', + multiattach=' True', + cacheable=' True', + replication_enabled=' True', + **{'RESKEY:availability_zones': 'az1'}, + ) + + expected_data = ( + self.new_volume_type.description, + self.new_volume_type.id, + self.new_volume_type.is_public, + self.new_volume_type.name, + format_columns.DictColumn(result_type.extra_specs), + ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.data, data) + self.assertCountEqual(expected_data, data) def test_public_type_create_with_project_public(self): arglist = [ @@ -183,33 +180,40 @@ def test_public_type_create_with_project_public(self): ) def test_type_create_with_encryption(self): - encryption_info = { - 'provider': 'LuksEncryptor', - 'cipher': 'aes-xts-plain64', - 'key_size': '128', - 'control_location': 'front-end', + encryption_type = sdk_fakes.generate_fake_resource( + _type.TypeEncryption, + provider='LuksEncryptor', + cipher='aes-xts-plain64', + key_size='128', + control_location='front-end', + ) + self.new_volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.volume_client.create_type.return_value = self.new_volume_type + self.volume_client.create_type_encryption.return_value = ( + encryption_type + ) + expected_encryption_info = { + 'provider': encryption_type.provider, + 'cipher': encryption_type.cipher, + 'key_size': encryption_type.key_size, + 'control_location': encryption_type.control_location, + 'encryption_id': encryption_type.encryption_id, } - encryption_type = volume_fakes.create_one_encryption_volume_type( - attrs=encryption_info, - ) - self.new_volume_type = volume_fakes.create_one_volume_type( - attrs={'encryption': encryption_info}, - ) - self.volume_types_mock.create.return_value = self.new_volume_type - self.volume_encryption_types_mock.create.return_value = encryption_type encryption_columns = ( 'description', 'encryption', 'id', 'is_public', 'name', + 'properties', ) encryption_data = ( self.new_volume_type.description, - format_columns.DictColumn(encryption_info), + format_columns.DictColumn(expected_encryption_info), self.new_volume_type.id, - True, + self.new_volume_type.is_public, self.new_volume_type.name, + format_columns.DictColumn(self.new_volume_type.extra_specs), ) arglist = [ '--encryption-provider', @@ -232,36 +236,30 @@ def test_type_create_with_encryption(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.create.assert_called_with( - self.new_volume_type.name, + self.volume_client.create_type.assert_called_with( + name=self.new_volume_type.name, description=None, ) - body = { - 'provider': 'LuksEncryptor', - 'cipher': 'aes-xts-plain64', - 'key_size': 128, - 'control_location': 'front-end', - } - self.volume_encryption_types_mock.create.assert_called_with( + self.volume_client.create_type_encryption.assert_called_with( self.new_volume_type, - body, + provider='LuksEncryptor', + cipher='aes-xts-plain64', + key_size=128, + control_location='front-end', ) self.assertEqual(encryption_columns, columns) self.assertCountEqual(encryption_data, data) -class TestTypeDelete(TestType): - volume_types = volume_fakes.create_volume_types(count=2) +class TestTypeDelete(volume_fakes.TestVolume): + volume_types = list(sdk_fakes.generate_fake_resources(_type.Type, count=2)) def setUp(self): super().setUp() - self.volume_types_mock.get = volume_fakes.get_volume_types( - self.volume_types, - ) - self.volume_types_mock.delete.return_value = None + self.volume_client.find_type.side_effect = self.volume_types + self.volume_client.delete_type.return_value = None - # Get the command object to mock self.cmd = volume_type.DeleteVolumeType(self.app, None) def test_type_delete(self): @@ -271,7 +269,10 @@ def test_type_delete(self): result = self.cmd.take_action(parsed_args) - self.volume_types_mock.delete.assert_called_with(self.volume_types[0]) + self.volume_client.find_type.assert_called_with( + self.volume_types[0].id, ignore_missing=False + ) + self.volume_client.delete_type.assert_called_with(self.volume_types[0]) self.assertIsNone(result) def test_delete_multiple_types(self): @@ -288,7 +289,7 @@ def test_delete_multiple_types(self): calls = [] for t in self.volume_types: calls.append(call(t)) - self.volume_types_mock.delete.assert_has_calls(calls) + self.volume_client.delete_type.assert_has_calls(calls) self.assertIsNone(result) def test_delete_multiple_types_with_exception(self): @@ -302,37 +303,37 @@ def test_delete_multiple_types_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.volume_types[0], exceptions.CommandError] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - '1 of 2 volume types failed to delete.', str(e) - ) - find_mock.assert_any_call( - self.volume_types_mock, self.volume_types[0].id - ) - find_mock.assert_any_call(self.volume_types_mock, 'unexist_type') + self.volume_client.find_type.side_effect = [ + self.volume_types[0], + exceptions.CommandError, + ] + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 volume types failed to delete.', str(e)) + self.volume_client.find_type.assert_any_call( + self.volume_types[0].id, ignore_missing=False + ) + self.volume_client.find_type.assert_any_call( + 'unexist_type', ignore_missing=False + ) - self.assertEqual(2, find_mock.call_count) - self.volume_types_mock.delete.assert_called_once_with( - self.volume_types[0] - ) + self.assertEqual(2, self.volume_client.find_type.call_count) + self.volume_client.delete_type.assert_called_once_with( + self.volume_types[0] + ) -class TestTypeList(TestType): - volume_types = volume_fakes.create_volume_types() +class TestTypeList(volume_fakes.TestVolume): + volume_types = list(sdk_fakes.generate_fake_resources(_type.Type, count=2)) columns = [ "ID", "Name", "Is Public", ] - columns_long = columns + ["Description"] + columns_long = [*columns, "Description"] data_with_default_type = [(volume_types[0].id, volume_types[0].name, True)] data = [] for t in volume_types: @@ -357,22 +358,21 @@ class TestTypeList(TestType): def setUp(self): super().setUp() - self.volume_types_mock.list.return_value = self.volume_types - self.volume_types_mock.default.return_value = self.volume_types[0] - # get the command to test + self.volume_client.types.return_value = self.volume_types + self.cmd = volume_type.ListVolumeType(self.app, None) def test_type_list_without_options(self): arglist = [] verifylist = [ ("long", False), - ("is_public", None), + ("is_public", 'none'), ("default", False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.list.assert_called_once_with(is_public=None) + self.volume_client.types.assert_called_once_with(is_public='none') self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) @@ -389,7 +389,7 @@ def test_type_list_with_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.list.assert_called_once_with(is_public=True) + self.volume_client.types.assert_called_once_with(is_public=True) self.assertEqual(self.columns_long, columns) self.assertCountEqual(self.data_long, list(data)) @@ -405,62 +405,62 @@ def test_type_list_with_private_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.list.assert_called_once_with(is_public=False) + self.volume_client.types.assert_called_once_with(is_public=False) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) def test_type_list_with_default_option(self): + self.volume_client.get_type.return_value = self.volume_types[0] + arglist = [ "--default", ] verifylist = [ ("encryption_type", False), ("long", False), - ("is_public", None), + ("is_public", 'none'), ("default", True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.default.assert_called_once_with() + self.volume_client.get_type.assert_called_once_with('default') self.assertEqual(self.columns, columns) self.assertCountEqual(self.data_with_default_type, list(data)) def test_type_list_with_encryption(self): - encryption_type = volume_fakes.create_one_encryption_volume_type( - attrs={'volume_type_id': self.volume_types[0].id}, - ) - encryption_info = { - 'provider': 'LuksEncryptor', - 'cipher': None, - 'key_size': None, - 'control_location': 'front-end', - } - encryption_columns = self.columns + [ - "Encryption", + encryption_types = [ + sdk_fakes.generate_fake_resource( + _type.TypeEncryption, + volume_type_id=vt.id, + ) + for vt in self.volume_types + ] + expected_encryption_info = [ + { + 'provider': encryption_type.provider, + 'cipher': encryption_type.cipher, + 'key_size': encryption_type.key_size, + 'control_location': encryption_type.control_location, + 'encryption_id': encryption_type.encryption_id, + } + for encryption_type in encryption_types ] - encryption_data = [] - encryption_data.append( + encryption_columns = [*self.columns, "Encryption"] + encryption_data = [ ( - self.volume_types[0].id, - self.volume_types[0].name, - self.volume_types[0].is_public, + self.volume_types[x].id, + self.volume_types[x].name, + self.volume_types[x].is_public, volume_type.EncryptionInfoColumn( - self.volume_types[0].id, - {self.volume_types[0].id: encryption_info}, + self.volume_types[x].id, + {self.volume_types[x].id: expected_encryption_info[x]}, ), ) - ) - encryption_data.append( - ( - self.volume_types[1].id, - self.volume_types[1].name, - self.volume_types[1].is_public, - volume_type.EncryptionInfoColumn(self.volume_types[1].id, {}), - ) - ) + for x in (0, 1) + ] - self.volume_encryption_types_mock.list.return_value = [encryption_type] + self.volume_client.get_type_encryption.side_effect = encryption_types arglist = [ "--encryption-type", ] @@ -470,25 +470,25 @@ def test_type_list_with_encryption(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_encryption_types_mock.list.assert_called_once_with() - self.volume_types_mock.list.assert_called_once_with(is_public=None) + self.volume_client.get_type_encryption.assert_has_calls( + [call(self.volume_types[0].id), call(self.volume_types[1].id)], + ) + self.volume_client.types.assert_called_once_with(is_public='none') self.assertEqual(encryption_columns, columns) self.assertCountEqual(encryption_data, list(data)) -class TestTypeSet(TestType): +class TestTypeSet(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.project = identity_fakes.FakeProject.create_one_project() - self.projects_mock.get.return_value = self.project + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project - self.volume_type = volume_fakes.create_one_volume_type( - methods={'set_keys': None}, - ) - self.volume_types_mock.get.return_value = self.volume_type - self.volume_encryption_types_mock.create.return_value = None - self.volume_encryption_types_mock.update.return_value = None + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.volume_client.find_type.return_value = self.volume_type + self.volume_client.create_type_encryption.return_value = None + self.volume_client.update_type_encryption.return_value = None self.cmd = volume_type.SetVolumeType(self.app, None) @@ -516,14 +516,14 @@ def test_type_set(self): 'description': 'new_description', 'is_public': False, } - self.volume_types_mock.update.assert_called_with( + self.volume_client.update_type.assert_called_with( self.volume_type.id, **kwargs ) self.assertIsNone(result) - self.volume_type_access_mock.add_project_access.assert_not_called() - self.volume_encryption_types_mock.update.assert_not_called() - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.add_type_access.assert_not_called() + self.volume_client.update_type_encryption.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() def test_type_set_property(self): arglist = [ @@ -552,18 +552,17 @@ def test_type_set_property(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_type.set_keys.assert_called_once_with( - { - 'myprop': 'myvalue', - 'multiattach': ' True', - 'cacheable': ' True', - 'replication_enabled': ' True', - 'RESKEY:availability_zones': 'az1', - } + self.volume_client.update_type_extra_specs.assert_called_once_with( + self.volume_type.id, + myprop='myvalue', + multiattach=' True', + cacheable=' True', + replication_enabled=' True', + **{'RESKEY:availability_zones': 'az1'}, ) - self.volume_type_access_mock.add_project_access.assert_not_called() - self.volume_encryption_types_mock.update.assert_not_called() - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.add_type_access.assert_not_called() + self.volume_client.update_type_encryption.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() def test_type_set_with_empty_project(self): arglist = [ @@ -581,10 +580,10 @@ def test_type_set_with_empty_project(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_type.set_keys.assert_not_called() - self.volume_type_access_mock.add_project_access.assert_not_called() - self.volume_encryption_types_mock.update.assert_not_called() - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.update_type_extra_specs.assert_not_called() + self.volume_client.add_type_access.assert_not_called() + self.volume_client.update_type_encryption.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() def test_type_set_with_project(self): arglist = [ @@ -601,16 +600,16 @@ def test_type_set_with_project(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_type.set_keys.assert_not_called() - self.volume_type_access_mock.add_project_access.assert_called_with( + self.volume_client.update_type_extra_specs.assert_not_called() + self.volume_client.add_type_access.assert_called_with( self.volume_type.id, self.project.id, ) - self.volume_encryption_types_mock.update.assert_not_called() - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.update_type_encryption.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() def test_type_set_with_new_encryption(self): - self.volume_encryption_types_mock.update.side_effect = ( + self.volume_client.update_type_encryption.side_effect = ( exceptions.NotFound('NotFound') ) arglist = [ @@ -636,24 +635,23 @@ def test_type_set_with_new_encryption(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - body = { - 'provider': 'LuksEncryptor', - 'cipher': 'aes-xts-plain64', - 'key_size': 128, - 'control_location': 'front-end', - } - self.volume_encryption_types_mock.update.assert_called_with( - self.volume_type, - body, + self.volume_client.update_type_encryption.assert_called_with( + encryption=None, + volume_type=self.volume_type, + provider='LuksEncryptor', + cipher='aes-xts-plain64', + key_size=128, + control_location='front-end', ) - self.volume_encryption_types_mock.create.assert_called_with( + self.volume_client.create_type_encryption.assert_called_with( self.volume_type, - body, + provider='LuksEncryptor', + cipher='aes-xts-plain64', + key_size=128, + control_location='front-end', ) - @mock.patch.object(utils, 'find_resource') - def test_type_set_with_existing_encryption(self, mock_find): - mock_find.side_effect = [self.volume_type, "existing_encryption_type"] + def test_type_set_with_existing_encryption(self): arglist = [ '--encryption-provider', 'LuksEncryptor', @@ -674,21 +672,19 @@ def test_type_set_with_existing_encryption(self, mock_find): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_type.set_keys.assert_not_called() - self.volume_type_access_mock.add_project_access.assert_not_called() - body = { - 'provider': 'LuksEncryptor', - 'cipher': 'aes-xts-plain64', - 'control_location': 'front-end', - } - self.volume_encryption_types_mock.update.assert_called_with( - self.volume_type, - body, + self.volume_client.update_type_extra_specs.assert_not_called() + self.volume_client.add_type_access.assert_not_called() + self.volume_client.update_type_encryption.assert_called_with( + encryption=None, + volume_type=self.volume_type, + provider='LuksEncryptor', + cipher='aes-xts-plain64', + control_location='front-end', ) - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() def test_type_set_new_encryption_without_provider(self): - self.volume_encryption_types_mock.update.side_effect = ( + self.volume_client.update_type_encryption.side_effect = ( exceptions.NotFound('NotFound') ) arglist = [ @@ -718,21 +714,19 @@ def test_type_set_new_encryption_without_provider(self): str(exc), ) - self.volume_type.set_keys.assert_not_called() - self.volume_type_access_mock.add_project_access.assert_not_called() - body = { - 'cipher': 'aes-xts-plain64', - 'key_size': 128, - 'control_location': 'front-end', - } - self.volume_encryption_types_mock.update.assert_called_with( - self.volume_type, - body, + self.volume_client.update_type_extra_specs.assert_not_called() + self.volume_client.add_type_access.assert_not_called() + self.volume_client.update_type_encryption.assert_called_with( + encryption=None, + volume_type=self.volume_type, + cipher='aes-xts-plain64', + key_size=128, + control_location='front-end', ) - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() -class TestTypeShow(TestType): +class TestTypeShow(volume_fakes.TestVolume): columns = ( 'access_project_ids', 'description', @@ -745,19 +739,18 @@ class TestTypeShow(TestType): def setUp(self): super().setUp() - self.volume_type = volume_fakes.create_one_volume_type() + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) self.data = ( None, self.volume_type.description, self.volume_type.id, - True, + self.volume_type.is_public, self.volume_type.name, format_columns.DictColumn(self.volume_type.extra_specs), ) - self.volume_types_mock.get.return_value = self.volume_type + self.volume_client.find_type.return_value = self.volume_type - # Get the command object to test self.cmd = volume_type.ShowVolumeType(self.app, None) def test_type_show(self): @@ -769,7 +762,9 @@ def test_type_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.get.assert_called_with(self.volume_type.id) + self.volume_client.find_type.assert_called_with( + self.volume_type.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) @@ -779,31 +774,27 @@ def test_type_show_with_access(self): verifylist = [("volume_type", self.volume_type.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - private_type = volume_fakes.create_one_volume_type( - attrs={'is_public': False}, + private_type = sdk_fakes.generate_fake_resource( + _type.Type, is_public=False + ) + type_access_list = { + 'volume_type_id': private_type.id, + 'project_id': 'project-id-test', + } + self.volume_client.find_type.return_value = private_type + self.volume_client.get_type_access.return_value = [type_access_list] + + columns, data = self.cmd.take_action(parsed_args) + self.volume_client.find_type.assert_called_once_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.get_type_access.assert_called_once_with( + private_type.id ) - type_access_list = volume_fakes.create_one_type_access() - with mock.patch.object( - self.volume_types_mock, - 'get', - return_value=private_type, - ): - with mock.patch.object( - self.volume_type_access_mock, - 'list', - return_value=[type_access_list], - ): - columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.get.assert_called_once_with( - self.volume_type.id - ) - self.volume_type_access_mock.list.assert_called_once_with( - private_type.id - ) self.assertEqual(self.columns, columns) private_type_data = ( - format_columns.ListColumn([type_access_list.project_id]), + format_columns.ListColumn([type_access_list['project_id']]), private_type.description, private_type.id, private_type.is_public, @@ -817,22 +808,19 @@ def test_type_show_with_list_access_exec(self): verifylist = [("volume_type", self.volume_type.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - private_type = volume_fakes.create_one_volume_type( - attrs={'is_public': False}, + private_type = sdk_fakes.generate_fake_resource( + _type.Type, is_public=False + ) + self.volume_client.find_type.return_value = private_type + self.volume_client.get_type_access.side_effect = Exception() + + columns, data = self.cmd.take_action(parsed_args) + self.volume_client.find_type.assert_called_once_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.get_type_access.assert_called_once_with( + private_type.id ) - with mock.patch.object( - self.volume_types_mock, 'get', return_value=private_type - ): - with mock.patch.object( - self.volume_type_access_mock, 'list', side_effect=Exception() - ): - columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.get.assert_called_once_with( - self.volume_type.id - ) - self.volume_type_access_mock.list.assert_called_once_with( - private_type.id - ) self.assertEqual(self.columns, columns) private_type_data = ( @@ -846,18 +834,19 @@ def test_type_show_with_list_access_exec(self): self.assertCountEqual(private_type_data, data) def test_type_show_with_encryption(self): - encryption_type = volume_fakes.create_one_encryption_volume_type() - encryption_info = { - 'provider': 'LuksEncryptor', - 'cipher': None, - 'key_size': None, - 'control_location': 'front-end', + encryption_type = sdk_fakes.generate_fake_resource( + _type.TypeEncryption, + ) + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.volume_client.find_type.return_value = self.volume_type + self.volume_client.get_type_encryption.return_value = encryption_type + expected_encryption_info = { + 'cipher': encryption_type.cipher, + 'control_location': encryption_type.control_location, + 'encryption_id': encryption_type.encryption_id, + 'key_size': encryption_type.key_size, + 'provider': encryption_type.provider, } - self.volume_type = volume_fakes.create_one_volume_type( - attrs={'encryption': encryption_info}, - ) - self.volume_types_mock.get.return_value = self.volume_type - self.volume_encryption_types_mock.get.return_value = encryption_type encryption_columns = ( 'access_project_ids', 'description', @@ -870,9 +859,9 @@ def test_type_show_with_encryption(self): encryption_data = ( None, self.volume_type.description, - format_columns.DictColumn(encryption_info), + format_columns.DictColumn(expected_encryption_info), self.volume_type.id, - True, + self.volume_type.is_public, self.volume_type.name, format_columns.DictColumn(self.volume_type.extra_specs), ) @@ -884,29 +873,27 @@ def test_type_show_with_encryption(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.get.assert_called_with(self.volume_type.id) - self.volume_encryption_types_mock.get.assert_called_with( + self.volume_client.find_type.assert_called_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.get_type_encryption.assert_called_with( self.volume_type.id ) self.assertEqual(encryption_columns, columns) self.assertCountEqual(encryption_data, data) -class TestTypeUnset(TestType): - project = identity_fakes.FakeProject.create_one_project() - volume_type = volume_fakes.create_one_volume_type( - methods={'unset_keys': None}, - ) +class TestTypeUnset(volume_fakes.TestVolume): + volume_type = sdk_fakes.generate_fake_resource(_type.Type) def setUp(self): super().setUp() - self.volume_types_mock.get.return_value = self.volume_type + self.volume_client.find_type.return_value = self.volume_type - # Return a project - self.projects_mock.get.return_value = self.project + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project - # Get the command object to test self.cmd = volume_type.UnsetVolumeType(self.app, None) def test_type_unset(self): @@ -925,8 +912,8 @@ def test_type_unset(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.volume_type.unset_keys.assert_called_once_with( - ['property', 'multi_property'] + self.volume_client.delete_type_extra_specs.assert_called_once_with( + self.volume_type.id, ['property', 'multi_property'] ) self.assertIsNone(result) @@ -945,7 +932,7 @@ def test_type_unset_project_access(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_type_access_mock.remove_project_access.assert_called_with( + self.volume_client.remove_type_access.assert_called_with( self.volume_type.id, self.project.id, ) @@ -966,18 +953,16 @@ def test_type_unset_not_called_without_project_argument(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_encryption_types_mock.delete.assert_not_called() - self.assertFalse( - self.volume_type_access_mock.remove_project_access.called - ) + self.volume_client.delete_type_encryption.assert_not_called() + self.volume_client.remove_type_access.assert_not_called() def test_type_unset_failed_with_missing_volume_type_argument(self): arglist = [ '--project', - 'identity_fakes.project_id', + 'foo', ] verifylist = [ - ('project', 'identity_fakes.project_id'), + ('project', 'foo'), ] self.assertRaises( @@ -1000,15 +985,15 @@ def test_type_unset_encryption_type(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.volume_encryption_types_mock.delete.assert_called_with( - self.volume_type + self.volume_client.delete_type_encryption.assert_called_with( + None, self.volume_type.id ) self.assertIsNone(result) -class TestColumns(TestType): +class TestColumns(volume_fakes.TestVolume): def test_encryption_info_column_with_info(self): - fake_volume_type = volume_fakes.create_one_volume_type() + fake_volume_type = sdk_fakes.generate_fake_resource(_type.Type) type_id = fake_volume_type.id encryption_info = { @@ -1026,7 +1011,7 @@ def test_encryption_info_column_with_info(self): self.assertEqual(encryption_info, col.machine_readable()) def test_encryption_info_column_without_info(self): - fake_volume_type = volume_fakes.create_one_volume_type() + fake_volume_type = sdk_fakes.generate_fake_resource(_type.Type) type_id = fake_volume_type.id col = volume_type.EncryptionInfoColumn(type_id, {}) diff --git a/openstackclient/tests/unit/volume/v3/fakes.py b/openstackclient/tests/unit/volume/v3/fakes.py index eb5c170f63..9f5650cc6d 100644 --- a/openstackclient/tests/unit/volume/v3/fakes.py +++ b/openstackclient/tests/unit/volume/v3/fakes.py @@ -10,93 +10,28 @@ # License for the specific language governing permissions and limitations # under the License. -import copy -import random import re from unittest import mock -import uuid -from cinderclient import api_versions from keystoneauth1 import discover -from openstack.block_storage.v3 import _proxy -from openstack.block_storage.v3 import availability_zone as _availability_zone -from openstack.block_storage.v3 import backup as _backup -from openstack.block_storage.v3 import extension as _extension -from openstack.block_storage.v3 import limits as _limits -from openstack.block_storage.v3 import resource_filter as _filters -from openstack.block_storage.v3 import volume as _volume -from openstack.compute.v2 import _proxy as _compute_proxy -from openstack.image.v2 import _proxy as _image_proxy +from openstack.block_storage import v3 as block_storage_v3 +from openstack.compute import v2 as compute_v2 +from openstack.image import v2 as image_v2 -from openstackclient.tests.unit import fakes from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit import utils -from openstackclient.tests.unit.volume.v2 import fakes as volume_v2_fakes - - -class FakeVolumeClient: - def __init__(self, **kwargs): - self.auth_token = kwargs['token'] - self.management_url = kwargs['endpoint'] - self.api_version = api_versions.APIVersion('3.0') - - self.attachments = mock.Mock() - self.attachments.resource_class = fakes.FakeResource(None, {}) - self.availability_zones = mock.Mock() - self.availability_zones.resource_class = fakes.FakeResource(None, {}) - self.backups = mock.Mock() - self.backups.resource_class = fakes.FakeResource(None, {}) - self.consistencygroups = mock.Mock() - self.consistencygroups.resource_class = fakes.FakeResource(None, {}) - self.clusters = mock.Mock() - self.clusters.resource_class = fakes.FakeResource(None, {}) - self.groups = mock.Mock() - self.groups.resource_class = fakes.FakeResource(None, {}) - self.group_snapshots = mock.Mock() - self.group_snapshots.resource_class = fakes.FakeResource(None, {}) - self.group_types = mock.Mock() - self.group_types.resource_class = fakes.FakeResource(None, {}) - self.messages = mock.Mock() - self.messages.resource_class = fakes.FakeResource(None, {}) - self.resource_filters = mock.Mock() - self.resource_filters.resource_class = fakes.FakeResource(None, {}) - self.restores = mock.Mock() - self.restores.resource_class = fakes.FakeResource(None, {}) - self.transfers = mock.Mock() - self.transfers.resource_class = fakes.FakeResource(None, {}) - self.volume_encryption_types = mock.Mock() - self.volume_encryption_types.resource_class = fakes.FakeResource( - None, {} - ) - self.volume_snapshots = mock.Mock() - self.volume_snapshots.resource_class = fakes.FakeResource(None, {}) - self.volume_type_access = mock.Mock() - self.volume_type_access.resource_class = fakes.FakeResource(None, {}) - self.volume_types = mock.Mock() - self.volume_types.resource_class = fakes.FakeResource(None, {}) - self.volumes = mock.Mock() - self.volumes.resource_class = fakes.FakeResource(None, {}) - self.services = mock.Mock() - self.services.resource_class = fakes.FakeResource(None, {}) - self.workers = mock.Mock() - self.workers.resource_class = fakes.FakeResource(None, {}) class FakeClientMixin: def setUp(self): super().setUp() - self.app.client_manager.volume = FakeVolumeClient( - endpoint=fakes.AUTH_URL, token=fakes.AUTH_TOKEN - ) + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.app.client_manager.volume = mock.Mock(spec=block_storage_v3.Proxy) + self.app.client_manager.volume.api_version = '3' self.volume_client = self.app.client_manager.volume - - # TODO(stephenfin): Rename to 'volume_client' once all commands are - # migrated to SDK - self.app.client_manager.sdk_connection.volume = mock.Mock( - spec=_proxy.Proxy, - ) - self.volume_sdk_client = self.app.client_manager.sdk_connection.volume self.set_volume_api_version() # default to the lowest def set_volume_api_version(self, version: str = '3.0'): @@ -108,10 +43,8 @@ def set_volume_api_version(self, version: str = '3.0'): """ assert re.match(r'3.\d+', version) - self.volume_client.api_version = api_versions.APIVersion(version) - - self.volume_sdk_client.default_microversion = version - self.volume_sdk_client.get_endpoint_data.return_value = ( + self.volume_client.default_microversion = version + self.volume_client.get_endpoint_data.return_value = ( discover.EndpointData( min_microversion='3.0', # cinder has not bumped this yet max_microversion=version, @@ -129,1043 +62,16 @@ def setUp(self): # avoid circular imports by defining this manually rather than using # openstackclient.tests.unit.compute.v2.fakes.FakeClientMixin - self.app.client_manager.compute = mock.Mock(_compute_proxy.Proxy) + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.app.client_manager.compute = mock.Mock(spec=compute_v2.Proxy) self.compute_client = self.app.client_manager.compute # avoid circular imports by defining this manually rather than using # openstackclient.tests.unit.image.v2.fakes.FakeClientMixin - self.app.client_manager.image = mock.Mock(spec=_image_proxy.Proxy) + # TODO(stephenfin): Switch to spec_set once keystoneauth exposes + # instance attributes as class attributes + # https://review.opendev.org/c/openstack/keystoneauth/+/994090 + self.app.client_manager.image = mock.Mock(spec=image_v2.Proxy) self.image_client = self.app.client_manager.image - - -# TODO(stephenfin): Check if the responses are actually the same -create_one_snapshot = volume_v2_fakes.create_one_snapshot -create_one_service = volume_v2_fakes.create_one_service - - -def create_one_availability_zone(attrs=None): - """Create a fake AZ. - - :param dict attrs: A dictionary with all attributes - :return: A fake - openstack.block_storage.v3.availability_zone.AvailabilityZone object - """ - attrs = attrs or {} - - # Set default attributes. - availability_zone_info = { - 'name': uuid.uuid4().hex, - 'state': {'available': True}, - } - - # Overwrite default attributes. - availability_zone_info.update(attrs) - - availability_zone = _availability_zone.AvailabilityZone( - **availability_zone_info - ) - return availability_zone - - -def create_availability_zones(attrs=None, count=2): - """Create multiple fake AZs. - - :param dict attrs: A dictionary with all attributes - :param int count: The number of availability zones to fake - :return: A list of fake - openstack.block_storage.v3.availability_zone.AvailabilityZone objects - """ - availability_zones = [] - for i in range(0, count): - availability_zone = create_one_availability_zone(attrs) - availability_zones.append(availability_zone) - - return availability_zones - - -def create_one_consistency_group(attrs=None): - """Create a fake consistency group. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attributes. - consistency_group_info = { - "id": 'backup-id-' + uuid.uuid4().hex, - "name": 'backup-name-' + uuid.uuid4().hex, - "description": 'description-' + uuid.uuid4().hex, - "status": "error", - "availability_zone": 'zone' + uuid.uuid4().hex, - "created_at": 'time-' + uuid.uuid4().hex, - "volume_types": ['volume-type1'], - } - - # Overwrite default attributes. - consistency_group_info.update(attrs) - - consistency_group = fakes.FakeResource( - info=copy.deepcopy(consistency_group_info), loaded=True - ) - return consistency_group - - -def create_consistency_groups(attrs=None, count=2): - """Create multiple fake consistency groups. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of consistency groups to fake - :return: - A list of FakeResource objects faking the consistency groups - """ - consistency_groups = [] - for i in range(0, count): - consistency_group = create_one_consistency_group(attrs) - consistency_groups.append(consistency_group) - - return consistency_groups - - -def create_one_extension(attrs=None): - """Create a fake extension. - - :param dict attrs: A dictionary with all attributes - :return: A fake - openstack.block_storage.v3.extension.Extension object - """ - attrs = attrs or {} - - # Set default attributes. - extension_info = { - 'alias': 'OS-SCH-HNT', - 'description': 'description-' + uuid.uuid4().hex, - 'links': [ - { - "href": "https://github.com/openstack/block-api", - "type": "text/html", - "rel": "describedby", - } - ], - 'name': 'name-' + uuid.uuid4().hex, - 'updated_at': '2013-04-18T00:00:00+00:00', - } - - # Overwrite default attributes. - extension_info.update(attrs) - - extension = _extension.Extension(**extension_info) - return extension - - -def create_one_backup(attrs=None): - """Create a fake backup. - - :param dict attrs: - A dictionary with all attributes - :return: A fake - openstack.block_storage.v3.backup.Backup object - """ - attrs = attrs or {} - - # Set default attributes. - backup_info = { - "availability_zone": 'zone' + uuid.uuid4().hex, - "container": 'container-' + uuid.uuid4().hex, - "created_at": 'time-' + uuid.uuid4().hex, - "data_timestamp": 'time-' + uuid.uuid4().hex, - "description": 'description-' + uuid.uuid4().hex, - "encryption_key_id": None, - "fail_reason": "Service not found for creating backup.", - "has_dependent_backups": False, - "id": 'backup-id-' + uuid.uuid4().hex, - "is_incremental": False, - "metadata": {}, - "name": 'backup-name-' + uuid.uuid4().hex, - "object_count": None, - "project_id": uuid.uuid4().hex, - "size": random.randint(1, 20), - "snapshot_id": 'snapshot-id' + uuid.uuid4().hex, - "status": "error", - "updated_at": 'time-' + uuid.uuid4().hex, - "user_id": uuid.uuid4().hex, - "volume_id": 'volume-id-' + uuid.uuid4().hex, - } - - # Overwrite default attributes. - backup_info.update(attrs) - - backup = _backup.Backup(**backup_info) - return backup - - -def create_backups(attrs=None, count=2): - """Create multiple fake backups. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of backups to fake - :return: A list of fake - openstack.block_storage.v3.backup.Backup objects - """ - backups = [] - for i in range(0, count): - backup = create_one_backup(attrs) - backups.append(backup) - - return backups - - -def get_backups(backups=None, count=2): - """Get an iterable MagicMock object with a list of faked backups. - - If backups list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List backups: - A list of FakeResource objects faking backups - :param Integer count: - The number of backups to be faked - :return - An iterable Mock object with side_effect set to a list of faked - backups - """ - if backups is None: - backups = create_backups(count) - - return mock.Mock(side_effect=backups) - - -def create_one_cluster(attrs=None): - """Create a fake service cluster. - - :param attrs: A dictionary with all attributes of service cluster - :return: A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - cluster_info = { - 'name': f'cluster-{uuid.uuid4().hex}', - 'binary': f'binary-{uuid.uuid4().hex}', - 'state': random.choice(['up', 'down']), - 'status': random.choice(['enabled', 'disabled']), - 'disabled_reason': None, - 'num_hosts': random.randint(1, 64), - 'num_down_hosts': random.randint(1, 64), - 'last_heartbeat': '2015-09-16T09:28:52.000000', - 'created_at': '2015-09-16T09:28:52.000000', - 'updated_at': '2015-09-16T09:28:52.000000', - 'replication_status': None, - 'frozen': False, - 'active_backend_id': None, - } - - # Overwrite default attributes if there are some attributes set - cluster_info.update(attrs) - - return fakes.FakeResource(None, cluster_info, loaded=True) - - -def create_clusters(attrs=None, count=2): - """Create multiple fake service clusters. - - :param attrs: A dictionary with all attributes of service cluster - :param count: The number of service clusters to be faked - :return: A list of FakeResource objects - """ - clusters = [] - for n in range(0, count): - clusters.append(create_one_cluster(attrs)) - - return clusters - - -def create_one_encryption_volume_type(attrs=None): - """Create a fake encryption volume type. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object with volume_type_id etc. - """ - attrs = attrs or {} - - # Set default attributes. - encryption_info = { - "volume_type_id": 'type-id-' + uuid.uuid4().hex, - 'provider': 'LuksEncryptor', - 'cipher': None, - 'key_size': None, - 'control_location': 'front-end', - } - - # Overwrite default attributes. - encryption_info.update(attrs) - - encryption_type = fakes.FakeResource( - info=copy.deepcopy(encryption_info), loaded=True - ) - return encryption_type - - -def create_limits(attrs=None): - """Create a fake limits object.""" - attrs = attrs or {} - - limits_attrs = { - 'absolute': { - 'totalSnapshotsUsed': 1, - 'maxTotalBackups': 10, - 'maxTotalVolumeGigabytes': 1000, - 'maxTotalSnapshots': 10, - 'maxTotalBackupGigabytes': 1000, - 'totalBackupGigabytesUsed': 0, - 'maxTotalVolumes': 10, - 'totalVolumesUsed': 4, - 'totalBackupsUsed': 0, - 'totalGigabytesUsed': 35, - }, - 'rate': [ - { - "uri": "*", - "limit": [ - { - "value": 10, - "verb": "POST", - "remaining": 2, - "unit": "MINUTE", - "next-available": "2011-12-15T22:42:45Z", - }, - { - "value": 10, - "verb": "PUT", - "remaining": 2, - "unit": "MINUTE", - "next-available": "2011-12-15T22:42:45Z", - }, - { - "value": 100, - "verb": "DELETE", - "remaining": 100, - "unit": "MINUTE", - "next-available": "2011-12-15T22:42:45Z", - }, - ], - } - ], - } - limits_attrs.update(attrs) - - limits = _limits.Limit(**limits_attrs) - return limits - - -def create_one_resource_filter(attrs=None): - """Create a fake resource filter. - - :param attrs: A dictionary with all attributes of resource filter - :return: A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - - resource_filter_info = { - 'filters': [ - 'name', - 'status', - 'image_metadata', - 'bootable', - 'migration_status', - ], - 'resource': 'volume', - } - - # Overwrite default attributes if there are some attributes set - resource_filter_info.update(attrs) - - return _filters.ResourceFilter(**resource_filter_info) - - -def create_resource_filters(attrs=None, count=2): - """Create multiple fake resource filters. - - :param attrs: A dictionary with all attributes of resource filter - :param count: The number of resource filters to be faked - :return: A list of FakeResource objects - """ - resource_filters = [] - for n in range(0, count): - resource_filters.append(create_one_resource_filter(attrs)) - - return resource_filters - - -def create_one_transfer(attrs=None): - """Create a fake transfer. - - :param dict attrs: - A dictionary with all attributes of Transfer Request - :return: - A FakeResource object with volume_id, name, id. - """ - # Set default attribute - transfer_info = { - 'volume_id': 'volume-id-' + uuid.uuid4().hex, - 'name': 'fake_transfer_name', - 'id': 'id-' + uuid.uuid4().hex, - 'links': 'links-' + uuid.uuid4().hex, - } - - # Overwrite default attributes if there are some attributes set - attrs = attrs or {} - - transfer_info.update(attrs) - - transfer = fakes.FakeResource(None, transfer_info, loaded=True) - - return transfer - - -def create_transfers(attrs=None, count=2): - """Create multiple fake transfers. - - :param dict attrs: - A dictionary with all attributes of transfer - :param Integer count: - The number of transfers to be faked - :return: - A list of FakeResource objects - """ - transfers = [] - for n in range(0, count): - transfers.append(create_one_transfer(attrs)) - - return transfers - - -def get_transfers(transfers=None, count=2): - """Get an iterable MagicMock object with a list of faked transfers. - - If transfers list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List transfers: - A list of FakeResource objects faking transfers - :param Integer count: - The number of transfers to be faked - :return - An iterable Mock object with side_effect set to a list of faked - transfers - """ - if transfers is None: - transfers = create_transfers(count) - - return mock.Mock(side_effect=transfers) - - -def create_one_type_access(attrs=None): - """Create a fake volume type access for project. - - :param dict attrs: - A dictionary with all attributes - :return: - A FakeResource object, with Volume_type_ID and Project_ID. - """ - if attrs is None: - attrs = {} - - # Set default attributes. - type_access_attrs = { - 'volume_type_id': 'volume-type-id-' + uuid.uuid4().hex, - 'project_id': 'project-id-' + uuid.uuid4().hex, - } - - # Overwrite default attributes. - type_access_attrs.update(attrs) - - type_access = fakes.FakeResource(None, type_access_attrs, loaded=True) - - return type_access - - -def create_one_volume(attrs=None): - """Create a fake volume. - - :param dict attrs: - A dictionary with all attributes of volume - :return: - A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - volume_info = { - 'id': 'volume-id' + uuid.uuid4().hex, - 'name': 'volume-name' + uuid.uuid4().hex, - 'description': 'description' + uuid.uuid4().hex, - 'status': random.choice(['available', 'in_use']), - 'size': random.randint(1, 20), - 'volume_type': random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), - 'bootable': random.randint(0, 1), - 'metadata': { - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - }, - 'snapshot_id': random.randint(1, 5), - 'availability_zone': 'zone' + uuid.uuid4().hex, - 'attachments': [ - { - 'device': '/dev/' + uuid.uuid4().hex, - 'server_id': uuid.uuid4().hex, - }, - ], - } - - # Overwrite default attributes if there are some attributes set - volume_info.update(attrs) - - volume = fakes.FakeResource(None, volume_info, loaded=True) - return volume - - -def create_volumes(attrs=None, count=2): - """Create multiple fake volumes. - - :param dict attrs: - A dictionary with all attributes of volume - :param Integer count: - The number of volumes to be faked - :return: - A list of FakeResource objects - """ - volumes = [] - for n in range(0, count): - volumes.append(create_one_volume(attrs)) - - return volumes - - -def get_volumes(volumes=None, count=2): - """Get an iterable MagicMock object with a list of faked volumes. - - If volumes list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param List volumes: - A list of FakeResource objects faking volumes - :param Integer count: - The number of volumes to be faked - :return - An iterable Mock object with side_effect set to a list of faked - volumes - """ - if volumes is None: - volumes = create_volumes(count) - - return mock.Mock(side_effect=volumes) - - -def create_one_sdk_volume(attrs=None): - """Create a fake volume. - - :param dict attrs: - A dictionary with all attributes of volume - :return: - A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - volume_info = { - 'id': 'volume-id' + uuid.uuid4().hex, - 'name': 'volume-name' + uuid.uuid4().hex, - 'description': 'description' + uuid.uuid4().hex, - 'status': random.choice(['available', 'in_use']), - 'size': random.randint(1, 20), - 'volume_type': random.choice(['fake_lvmdriver-1', 'fake_lvmdriver-2']), - 'bootable': random.choice(['true', 'false']), - 'metadata': { - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - 'key' + uuid.uuid4().hex: 'val' + uuid.uuid4().hex, - }, - 'snapshot_id': random.randint(1, 5), - 'availability_zone': 'zone' + uuid.uuid4().hex, - 'attachments': [ - { - 'device': '/dev/' + uuid.uuid4().hex, - 'server_id': uuid.uuid4().hex, - }, - ], - } - - # Overwrite default attributes if there are some attributes set - volume_info.update(attrs) - return _volume.Volume(**volume_info) - - -def create_sdk_volumes(attrs=None, count=2): - """Create multiple fake volumes. - - :param dict attrs: - A dictionary with all attributes of volume - :param Integer count: - The number of volumes to be faked - :return: - A list of FakeResource objects - """ - volumes = [] - for n in range(0, count): - volumes.append(create_one_sdk_volume(attrs)) - - return volumes - - -def create_one_volume_group(attrs=None): - """Create a fake group. - - :param attrs: A dictionary with all attributes of group - :return: A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - group_type = attrs.pop('group_type', None) or uuid.uuid4().hex - volume_types = attrs.pop('volume_types', None) or [uuid.uuid4().hex] - - # Set default attribute - group_info = { - 'id': uuid.uuid4().hex, - 'status': random.choice( - [ - 'available', - ] - ), - 'availability_zone': f'az-{uuid.uuid4().hex}', - 'created_at': '2015-09-16T09:28:52.000000', - 'name': 'first_group', - 'description': f'description-{uuid.uuid4().hex}', - 'group_type': group_type, - 'volume_types': volume_types, - 'volumes': [f'volume-{uuid.uuid4().hex}'], - 'group_snapshot_id': None, - 'source_group_id': None, - 'project_id': f'project-{uuid.uuid4().hex}', - } - - # Overwrite default attributes if there are some attributes set - group_info.update(attrs) - - group = fakes.FakeResource(None, group_info, loaded=True) - return group - - -def create_volume_groups(attrs=None, count=2): - """Create multiple fake groups. - - :param attrs: A dictionary with all attributes of group - :param count: The number of groups to be faked - :return: A list of FakeResource objects - """ - groups = [] - for n in range(0, count): - groups.append(create_one_volume_group(attrs)) - - return groups - - -def create_one_volume_group_snapshot(attrs=None, methods=None): - """Create a fake group snapshot. - - :param attrs: A dictionary with all attributes - :param methods: A dictionary with all methods - :return: A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attribute - group_snapshot_info = { - 'id': uuid.uuid4().hex, - 'name': f'group-snapshot-{uuid.uuid4().hex}', - 'description': f'description-{uuid.uuid4().hex}', - 'status': random.choice(['available']), - 'group_id': uuid.uuid4().hex, - 'group_type_id': uuid.uuid4().hex, - 'project_id': uuid.uuid4().hex, - } - - # Overwrite default attributes if there are some attributes set - group_snapshot_info.update(attrs) - - group_snapshot = fakes.FakeResource( - None, group_snapshot_info, methods=methods, loaded=True - ) - return group_snapshot - - -def create_volume_group_snapshots(attrs=None, count=2): - """Create multiple fake group snapshots. - - :param attrs: A dictionary with all attributes of group snapshot - :param count: The number of group snapshots to be faked - :return: A list of FakeResource objects - """ - group_snapshots = [] - for n in range(0, count): - group_snapshots.append(create_one_volume_group_snapshot(attrs)) - - return group_snapshots - - -def create_one_volume_group_type(attrs=None, methods=None): - """Create a fake group type. - - :param attrs: A dictionary with all attributes of group type - :param methods: A dictionary with all methods - :return: A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - - # Set default attribute - group_type_info = { - 'id': uuid.uuid4().hex, - 'name': f'group-type-{uuid.uuid4().hex}', - 'description': f'description-{uuid.uuid4().hex}', - 'is_public': random.choice([True, False]), - 'group_specs': {}, - } - - # Overwrite default attributes if there are some attributes set - group_type_info.update(attrs) - - group_type = fakes.FakeResource( - None, group_type_info, methods=methods, loaded=True - ) - return group_type - - -def create_volume_group_types(attrs=None, count=2): - """Create multiple fake group types. - - :param attrs: A dictionary with all attributes of group type - :param count: The number of group types to be faked - :return: A list of FakeResource objects - """ - group_types = [] - for n in range(0, count): - group_types.append(create_one_volume_group_type(attrs)) - - return group_types - - -def create_one_volume_message(attrs=None): - """Create a fake message. - - :param attrs: A dictionary with all attributes of message - :return: A FakeResource object with id, name, status, etc. - """ - attrs = attrs or {} - - # Set default attribute - message_info = { - 'created_at': '2016-02-11T11:17:37.000000', - 'event_id': f'VOLUME_{random.randint(1, 999999):06d}', - 'guaranteed_until': '2016-02-11T11:17:37.000000', - 'id': uuid.uuid4().hex, - 'message_level': 'ERROR', - 'request_id': f'req-{uuid.uuid4().hex}', - 'resource_type': 'VOLUME', - 'resource_uuid': uuid.uuid4().hex, - 'user_message': f'message-{uuid.uuid4().hex}', - } - - # Overwrite default attributes if there are some attributes set - message_info.update(attrs) - - return fakes.FakeResource(None, message_info, loaded=True) - - -def create_volume_messages(attrs=None, count=2): - """Create multiple fake messages. - - :param attrs: A dictionary with all attributes of message - :param count: The number of messages to be faked - :return: A list of FakeResource objects - """ - messages = [] - for n in range(0, count): - messages.append(create_one_volume_message(attrs)) - - return messages - - -def get_volume_messages(messages=None, count=2): - """Get an iterable MagicMock object with a list of faked messages. - - If messages list is provided, then initialize the Mock object with the - list. Otherwise create one. - - :param messages: A list of FakeResource objects faking messages - :param count: The number of messages to be faked - :return An iterable Mock object with side_effect set to a list of faked - messages - """ - if messages is None: - messages = create_volume_messages(count) - - return mock.Mock(side_effect=messages) - - -def create_one_volume_attachment(attrs=None): - """Create a fake volume attachment. - - :param attrs: A dictionary with all attributes of volume attachment - :return: A FakeResource object with id, status, etc. - """ - attrs = attrs or {} - - attachment_id = uuid.uuid4().hex - volume_id = attrs.pop('volume_id', None) or uuid.uuid4().hex - server_id = attrs.pop('instance', None) or uuid.uuid4().hex - - # Set default attribute - attachment_info = { - 'id': attachment_id, - 'volume_id': volume_id, - 'instance': server_id, - 'status': random.choice( - [ - 'attached', - 'attaching', - 'detached', - 'reserved', - 'error_attaching', - 'error_detaching', - 'deleted', - ] - ), - 'attach_mode': random.choice(['ro', 'rw']), - 'attached_at': '2015-09-16T09:28:52.000000', - 'detached_at': None, - 'connection_info': { - 'access_mode': 'rw', - 'attachment_id': attachment_id, - 'auth_method': 'CHAP', - 'auth_password': 'AcUZ8PpxLHwzypMC', - 'auth_username': '7j3EZQWT3rbE6pcSGKvK', - 'cacheable': False, - 'driver_volume_type': 'iscsi', - 'encrypted': False, - 'qos_specs': None, - 'target_discovered': False, - 'target_iqn': f'iqn.2010-10.org.openstack:volume-{attachment_id}', - 'target_lun': '1', - 'target_portal': '192.168.122.170:3260', - 'volume_id': volume_id, - }, - } - - # Overwrite default attributes if there are some attributes set - attachment_info.update(attrs) - - return fakes.FakeResource(None, attachment_info, loaded=True) - - -def create_volume_attachments(attrs=None, count=2): - """Create multiple fake volume attachments. - - :param attrs: A dictionary with all attributes of volume attachment - :param count: The number of volume attachments to be faked - :return: A list of FakeResource objects - """ - attachments = [] - - for n in range(0, count): - attachments.append(create_one_volume_attachment(attrs)) - - return attachments - - -def get_volume_attachments(attachments=None, count=2): - """Get an iterable MagicMock object with a list of faked volumes. - - If attachments list is provided, then initialize the Mock object with - the list. Otherwise create one. - - :param attachments: A list of FakeResource objects faking volume - attachments - :param count: The number of volume attachments to be faked - :return An iterable Mock object with side_effect set to a list of faked - volume attachments - """ - if attachments is None: - attachments = create_volume_attachments(count) - - return mock.Mock(side_effect=attachments) - - -def create_one_volume_type(attrs=None, methods=None): - """Create a fake volume type. - - :param dict attrs: - A dictionary with all attributes - :param dict methods: - A dictionary with all methods - :return: - A FakeResource object with id, name, description, etc. - """ - attrs = attrs or {} - methods = methods or {} - - # Set default attributes. - volume_type_info = { - "id": 'type-id-' + uuid.uuid4().hex, - "name": 'type-name-' + uuid.uuid4().hex, - "description": 'type-description-' + uuid.uuid4().hex, - "extra_specs": {"foo": "bar"}, - "is_public": True, - } - - # Overwrite default attributes. - volume_type_info.update(attrs) - - volume_type = fakes.FakeResource( - info=copy.deepcopy(volume_type_info), methods=methods, loaded=True - ) - return volume_type - - -def create_volume_types(attrs=None, count=2): - """Create multiple fake volume_types. - - :param dict attrs: - A dictionary with all attributes - :param int count: - The number of types to fake - :return: - A list of FakeResource objects faking the types - """ - volume_types = [] - for i in range(0, count): - volume_type = create_one_volume_type(attrs) - volume_types.append(volume_type) - - return volume_types - - -def get_volume_types(volume_types=None, count=2): - """Get an iterable MagicMock object with a list of faked volume types. - - If volume_types list is provided, then initialize the Mock object with - the list. Otherwise create one. - - :param List volume_types: - A list of FakeResource objects faking volume types - :param Integer count: - The number of volume types to be faked - :return - An iterable Mock object with side_effect set to a list of faked - volume types - """ - if volume_types is None: - volume_types = create_volume_types(count) - - return mock.Mock(side_effect=volume_types) - - -def create_service_log_level_entry(attrs=None): - service_log_level_info = { - 'host': 'host_test', - 'binary': 'cinder-api', - 'prefix': 'cinder.api.common', - 'level': 'DEBUG', - } - # Overwrite default attributes if there are some attributes set - attrs = attrs or {} - - service_log_level_info.update(attrs) - - service_log_level = fakes.FakeResource( - None, service_log_level_info, loaded=True - ) - return service_log_level - - -def create_cleanup_records(): - """Create fake service cleanup records. - - :return: A list of FakeResource objects - """ - cleaning_records = [] - unavailable_records = [] - cleaning_work_info = { - 'id': 1, - 'host': 'devstack@fakedriver-1', - 'binary': 'cinder-volume', - 'cluster_name': 'fake_cluster', - } - unavailable_work_info = { - 'id': 2, - 'host': 'devstack@fakedriver-2', - 'binary': 'cinder-scheduler', - 'cluster_name': 'new_cluster', - } - cleaning_records.append(cleaning_work_info) - unavailable_records.append(unavailable_work_info) - - cleaning = [ - fakes.FakeResource(None, obj, loaded=True) for obj in cleaning_records - ] - unavailable = [ - fakes.FakeResource(None, obj, loaded=True) - for obj in unavailable_records - ] - - return cleaning, unavailable - - -def create_one_manage_record(attrs=None, snapshot=False): - manage_dict = { - 'reference': {'source-name': 'fake-volume'}, - 'size': '1', - 'safe_to_manage': False, - 'reason_not_safe': 'already managed', - 'cinder_id': 'fake-volume', - 'extra_info': None, - } - if snapshot: - manage_dict['source_reference'] = {'source-name': 'fake-source'} - - # Overwrite default attributes if there are some attributes set - attrs = attrs or {} - - manage_dict.update(attrs) - manage_record = fakes.FakeResource(None, manage_dict, loaded=True) - return manage_record - - -def create_volume_manage_list_records(count=2): - volume_manage_list = [] - for i in range(count): - volume_manage_list.append( - create_one_manage_record({'size': str(i + 1)}) - ) - - return volume_manage_list - - -def create_snapshot_manage_list_records(count=2): - snapshot_manage_list = [] - for i in range(count): - snapshot_manage_list.append( - create_one_manage_record({'size': str(i + 1)}, snapshot=True) - ) - - return snapshot_manage_list diff --git a/openstackclient/tests/unit/volume/v3/test_backup_record.py b/openstackclient/tests/unit/volume/v3/test_backup_record.py new file mode 100644 index 0000000000..9e01a5af1f --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_backup_record.py @@ -0,0 +1,109 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack.block_storage.v3 import backup as _backup +from openstack.test import fakes as sdk_fakes + +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import backup_record + + +class TestBackupRecordExport(volume_fakes.TestVolume): + fake_backup = sdk_fakes.generate_fake_resource( + _backup.Backup, + volume_id='a54708a2-0388-4476-a909-09579f885c25', + ) + fake_record = { + 'backup-record': { + 'backup_service': 'cinder.backup.drivers.swift.SwiftBackupDriver', + 'backup_url': 'eyJzdGF0dXMiOiAiYXZh', + }, + } + + def setUp(self): + super().setUp() + + self.volume_client.find_backup.return_value = self.fake_backup + self.volume_client.export_backup.return_value = self.fake_record + + self.cmd = backup_record.ExportBackupRecord(self.app, None) + + def test_backup_export_table(self): + arglist = [self.fake_backup.name] + verifylist = [("backup", self.fake_backup.name)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + parsed_args.formatter = 'table' + columns, __ = self.cmd.take_action(parsed_args) + + self.volume_client.find_backup.assert_called_once_with( + self.fake_backup.name, ignore_missing=False + ) + self.volume_client.export_backup.assert_called_once_with( + self.fake_backup + ) + self.assertEqual(('Backup Service', 'Metadata'), columns) + + def test_backup_export_json(self): + arglist = [self.fake_backup.name] + verifylist = [("backup", self.fake_backup.name)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + parsed_args.formatter = 'json' + columns, __ = self.cmd.take_action(parsed_args) + + self.volume_client.find_backup.assert_called_once_with( + self.fake_backup.name, ignore_missing=False + ) + self.volume_client.export_backup.assert_called_once_with( + self.fake_backup + ) + self.assertEqual(('backup_service', 'backup_url'), columns) + + +class TestBackupRecordImport(volume_fakes.TestVolume): + fake_import = { + 'backup': { + 'id': 'backup.id', + 'name': 'backup.name', + }, + } + + def setUp(self): + super().setUp() + + self.volume_client.import_backup.return_value = self.fake_import + + self.cmd = backup_record.ImportBackupRecord(self.app, None) + + def test_backup_import(self): + arglist = [ + "cinder.backup.drivers.swift.SwiftBackupDriver", + "fake_backup_record_data", + ] + verifylist = [ + ( + "backup_service", + "cinder.backup.drivers.swift.SwiftBackupDriver", + ), + ("backup_metadata", "fake_backup_record_data"), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, __ = self.cmd.take_action(parsed_args) + + self.volume_client.import_backup.assert_called_once_with( + "cinder.backup.drivers.swift.SwiftBackupDriver", + "fake_backup_record_data", + ) + self.assertEqual(('backup',), columns) diff --git a/openstackclient/tests/unit/volume/v3/test_block_storage_cleanup.py b/openstackclient/tests/unit/volume/v3/test_block_storage_cleanup.py index 425b3875c9..7024fd6716 100644 --- a/openstackclient/tests/unit/volume/v3/test_block_storage_cleanup.py +++ b/openstackclient/tests/unit/volume/v3/test_block_storage_cleanup.py @@ -17,25 +17,36 @@ from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes from openstackclient.volume.v3 import block_storage_cleanup - -class TestBlockStorage(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - # Get a shortcut to the BlockStorageWorkerManager Mock - self.worker_mock = self.volume_client.workers - self.worker_mock.reset_mock() - - -class TestBlockStorageCleanup(TestBlockStorage): - cleaning, unavailable = volume_fakes.create_cleanup_records() - +FAKE_CLEANING = [ + { + 'id': 1, + 'host': 'devstack@fakedriver-1', + 'binary': 'cinder-volume', + 'cluster_name': 'fake_cluster', + } +] +FAKE_UNAVAILABLE = [ + { + 'id': 2, + 'host': 'devstack@fakedriver-2', + 'binary': 'cinder-scheduler', + 'cluster_name': 'new_cluster', + } +] +FAKE_CLEANUP_RESPONSE = { + 'cleaning': FAKE_CLEANING, + 'unavailable': FAKE_UNAVAILABLE, +} + + +class TestBlockStorageCleanup(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.worker_mock.clean.return_value = (self.cleaning, self.unavailable) + self.volume_client.cleanup_service_workers.return_value = ( + FAKE_CLEANUP_RESPONSE + ) - # Get the command object to test self.cmd = block_storage_cleanup.BlockStorageCleanup(self.app, None) def test_cleanup(self): @@ -55,24 +66,28 @@ def test_cleanup(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) expected_columns = ('ID', 'Cluster Name', 'Host', 'Binary', 'Status') - cleaning_data = tuple( - (obj.id, obj.cluster_name, obj.host, obj.binary, 'Cleaning') - for obj in self.cleaning + expected_data = ( + ( + 1, + 'fake_cluster', + 'devstack@fakedriver-1', + 'cinder-volume', + 'Cleaning', + ), + ( + 2, + 'new_cluster', + 'devstack@fakedriver-2', + 'cinder-scheduler', + 'Unavailable', + ), ) - unavailable_data = tuple( - (obj.id, obj.cluster_name, obj.host, obj.binary, 'Unavailable') - for obj in self.unavailable - ) - expected_data = cleaning_data + unavailable_data columns, data = self.cmd.take_action(parsed_args) self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, tuple(data)) - # checking if proper call was made to cleanup resources - # Since we ignore all parameters with None value, we don't - # have any arguments passed to the API - self.worker_mock.clean.assert_called_once_with() + self.volume_client.cleanup_service_workers.assert_called_once_with() def test_block_storage_cleanup_pre_324(self): arglist = [] @@ -132,22 +147,28 @@ def test_cleanup_with_args(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) expected_columns = ('ID', 'Cluster Name', 'Host', 'Binary', 'Status') - cleaning_data = tuple( - (obj.id, obj.cluster_name, obj.host, obj.binary, 'Cleaning') - for obj in self.cleaning - ) - unavailable_data = tuple( - (obj.id, obj.cluster_name, obj.host, obj.binary, 'Unavailable') - for obj in self.unavailable + expected_data = ( + ( + 1, + 'fake_cluster', + 'devstack@fakedriver-1', + 'cinder-volume', + 'Cleaning', + ), + ( + 2, + 'new_cluster', + 'devstack@fakedriver-2', + 'cinder-scheduler', + 'Unavailable', + ), ) - expected_data = cleaning_data + unavailable_data columns, data = self.cmd.take_action(parsed_args) self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, tuple(data)) - # checking if proper call was made to cleanup resources - self.worker_mock.clean.assert_called_once_with( + self.volume_client.cleanup_service_workers.assert_called_once_with( cluster_name=fake_cluster, host=fake_host, binary=fake_binary, diff --git a/openstackclient/tests/unit/volume/v3/test_block_storage_cluster.py b/openstackclient/tests/unit/volume/v3/test_block_storage_cluster.py index 758105c569..d04abf8af4 100644 --- a/openstackclient/tests/unit/volume/v3/test_block_storage_cluster.py +++ b/openstackclient/tests/unit/volume/v3/test_block_storage_cluster.py @@ -10,31 +10,24 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.block_storage.v3 import cluster as _cluster +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes from openstackclient.volume.v3 import block_storage_cluster -class TestBlockStorageCluster(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - # Get a shortcut to the BlockStorageClusterManager Mock - self.cluster_mock = self.volume_client.clusters - self.cluster_mock.reset_mock() - - -class TestBlockStorageClusterList(TestBlockStorageCluster): - # The cluster to be listed - fake_clusters = volume_fakes.create_clusters() +class TestBlockStorageClusterList(volume_fakes.TestVolume): + fake_clusters = list( + sdk_fakes.generate_fake_resources(_cluster.Cluster, count=2) + ) def setUp(self): super().setUp() - self.cluster_mock.list.return_value = self.fake_clusters + self.volume_client.clusters.return_value = self.fake_clusters - # Get the command object to test self.cmd = block_storage_cluster.ListBlockStorageCluster( self.app, None ) @@ -69,15 +62,14 @@ def test_cluster_list(self): self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, tuple(data)) - # checking if proper call was made to list clusters - self.cluster_mock.list.assert_called_with( + self.volume_client.clusters.assert_called_once_with( name=None, binary=None, is_up=None, disabled=None, num_hosts=None, num_down_hosts=None, - detailed=False, + details=False, ) def test_cluster_list_with_full_options(self): @@ -139,15 +131,14 @@ def test_cluster_list_with_full_options(self): self.assertEqual(expected_columns, columns) self.assertEqual(expected_data, tuple(data)) - # checking if proper call was made to list clusters - self.cluster_mock.list.assert_called_with( + self.volume_client.clusters.assert_called_once_with( name='foo', binary='bar', is_up=True, disabled=True, num_hosts=5, num_down_hosts=0, - detailed=True, + details=True, ) def test_cluster_list_pre_v37(self): @@ -173,8 +164,8 @@ def test_cluster_list_pre_v37(self): ) -class TestBlockStorageClusterSet(TestBlockStorageCluster): - cluster = volume_fakes.create_one_cluster() +class TestBlockStorageClusterSet(volume_fakes.TestVolume): + cluster = sdk_fakes.generate_fake_resource(_cluster.Cluster) columns = ( 'Name', 'Binary', @@ -209,11 +200,12 @@ class TestBlockStorageClusterSet(TestBlockStorageCluster): def setUp(self): super().setUp() - self.cluster_mock.update.return_value = self.cluster + self.volume_client.enable_cluster.return_value = self.cluster + self.volume_client.disable_cluster.return_value = self.cluster self.cmd = block_storage_cluster.SetBlockStorageCluster(self.app, None) - def test_cluster_set(self): + def test_cluster_set_enable(self): self.set_volume_api_version('3.7') arglist = [ @@ -233,12 +225,10 @@ def test_cluster_set(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) - self.cluster_mock.update.assert_called_once_with( - self.cluster.name, - 'cinder-volume', - disabled=False, - disabled_reason=None, + self.volume_client.enable_cluster.assert_called_once_with( + _cluster.Cluster(name=self.cluster.name, binary='cinder-volume') ) + self.volume_client.disable_cluster.assert_not_called() def test_cluster_set_disable_with_reason(self): self.set_volume_api_version('3.7') @@ -263,12 +253,13 @@ def test_cluster_set_disable_with_reason(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) - self.cluster_mock.update.assert_called_once_with( - self.cluster.name, - self.cluster.binary, - disabled=True, - disabled_reason='foo', + self.volume_client.disable_cluster.assert_called_once_with( + _cluster.Cluster( + name=self.cluster.name, binary=self.cluster.binary + ), + reason='foo', ) + self.volume_client.enable_cluster.assert_not_called() def test_cluster_set_only_with_disable_reason(self): self.set_volume_api_version('3.7') @@ -341,8 +332,8 @@ def test_cluster_set_pre_v37(self): ) -class TestBlockStorageClusterShow(TestBlockStorageCluster): - cluster = volume_fakes.create_one_cluster() +class TestBlockStorageClusterShow(volume_fakes.TestVolume): + cluster = sdk_fakes.generate_fake_resource(_cluster.Cluster) columns = ( 'Name', 'Binary', @@ -377,7 +368,7 @@ class TestBlockStorageClusterShow(TestBlockStorageCluster): def setUp(self): super().setUp() - self.cluster_mock.show.return_value = self.cluster + self.volume_client.get_cluster.return_value = self.cluster self.cmd = block_storage_cluster.ShowBlockStorageCluster( self.app, None @@ -387,13 +378,11 @@ def test_cluster_show(self): self.set_volume_api_version('3.7') arglist = [ - '--binary', - self.cluster.binary, self.cluster.name, ] verifylist = [ ('cluster', self.cluster.name), - ('binary', self.cluster.binary), + ('binary', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -402,22 +391,19 @@ def test_cluster_show(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, tuple(data)) - self.cluster_mock.show.assert_called_once_with( - self.cluster.name, - binary=self.cluster.binary, + self.volume_client.get_cluster.assert_called_once_with( + self.cluster.name ) def test_cluster_show_pre_v37(self): self.set_volume_api_version('3.6') arglist = [ - '--binary', - self.cluster.binary, self.cluster.name, ] verifylist = [ ('cluster', self.cluster.name), - ('binary', self.cluster.binary), + ('binary', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) diff --git a/openstackclient/tests/unit/volume/v3/test_block_storage_log_level.py b/openstackclient/tests/unit/volume/v3/test_block_storage_log_level.py index 58394e18b9..f0c050876d 100644 --- a/openstackclient/tests/unit/volume/v3/test_block_storage_log_level.py +++ b/openstackclient/tests/unit/volume/v3/test_block_storage_log_level.py @@ -10,9 +10,10 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# import ddt +from openstack.block_storage.v3 import service as _service +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.tests.unit import utils as tests_utils @@ -20,24 +21,17 @@ from openstackclient.volume.v3 import block_storage_log_level as service -class TestService(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - # Get a shortcut to the ServiceManager Mock - self.service_mock = self.volume_client.services - self.service_mock.reset_mock() - - -class TestBlockStorageLogLevelList(TestService): - service_log = volume_fakes.create_service_log_level_entry() - +class TestBlockStorageLogLevelList(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.service_mock.get_log_levels.return_value = [self.service_log] + self.log_level = sdk_fakes.generate_fake_resource( + _service.LogLevel, binary='cinder-scheduler' + ) + self.volume_client.get_service_log_levels.return_value = [ + self.log_level + ] - # Get the command object to test self.cmd = service.BlockStorageLogLevelList(self.app, None) def test_block_storage_log_level_list(self): @@ -45,16 +39,16 @@ def test_block_storage_log_level_list(self): arglist = [ '--host', - self.service_log.host, + self.log_level.host, '--service', - self.service_log.binary, + self.log_level.binary, '--log-prefix', - self.service_log.prefix, + 'cinder.', ] verifylist = [ - ('host', self.service_log.host), - ('service', self.service_log.binary), - ('log_prefix', self.service_log.prefix), + ('host', self.log_level.host), + ('service', self.log_level.binary), + ('log_prefix', 'cinder.'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -66,40 +60,35 @@ def test_block_storage_log_level_list(self): 'Prefix', 'Level', ] - - # confirming if all expected columns are present in the result. - self.assertEqual(expected_columns, columns) - - datalist = ( + datalist = tuple( ( - self.service_log.binary, - self.service_log.host, - self.service_log.prefix, - self.service_log.level, - ), + self.log_level.binary, + self.log_level.host, + prefix, + level, + ) + for prefix, level in self.log_level.levels.values() ) - - # confirming if all expected values are present in the result. + self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - # checking if proper call was made to get log level of services - self.service_mock.get_log_levels.assert_called_with( - server=self.service_log.host, - binary=self.service_log.binary, - prefix=self.service_log.prefix, + self.volume_client.get_service_log_levels.assert_called_with( + server=self.log_level.host, + binary=self.log_level.binary, + prefix='cinder.', ) def test_block_storage_log_level_list_pre_332(self): arglist = [ '--host', - self.service_log.host, + self.log_level.host, '--service', 'cinder-api', '--log-prefix', 'cinder_test.api.common', ] verifylist = [ - ('host', self.service_log.host), + ('host', self.log_level.host), ('service', 'cinder-api'), ('log_prefix', 'cinder_test.api.common'), ] @@ -117,14 +106,14 @@ def test_block_storage_log_level_list_invalid_service_name(self): arglist = [ '--host', - self.service_log.host, + self.log_level.host, '--service', 'nova-api', '--log-prefix', 'cinder_test.api.common', ] verifylist = [ - ('host', self.service_log.host), + ('host', self.log_level.host), ('service', 'nova-api'), ('log_prefix', 'cinder_test.api.common'), ] @@ -139,13 +128,15 @@ def test_block_storage_log_level_list_invalid_service_name(self): @ddt.ddt -class TestBlockStorageLogLevelSet(TestService): - service_log = volume_fakes.create_service_log_level_entry() - +class TestBlockStorageLogLevelSet(volume_fakes.TestVolume): def setUp(self): super().setUp() - # Get the command object to test + self.log_level = sdk_fakes.generate_fake_resource( + _service.LogLevel, binary='cinder-api' + ) + self.volume_client.set_service_log_levels.return_value = None + self.cmd = service.BlockStorageLogLevelSet(self.app, None) def test_block_storage_log_level_set(self): @@ -154,45 +145,45 @@ def test_block_storage_log_level_set(self): arglist = [ 'ERROR', '--host', - self.service_log.host, + self.log_level.host, '--service', - self.service_log.binary, + self.log_level.binary, '--log-prefix', - self.service_log.prefix, + 'cinder.api.common', ] verifylist = [ ('level', 'ERROR'), - ('host', self.service_log.host), - ('service', self.service_log.binary), - ('log_prefix', self.service_log.prefix), + ('host', self.log_level.host), + ('service', self.log_level.binary), + ('log_prefix', 'cinder.api.common'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) + ret = self.cmd.take_action(parsed_args) - # checking if proper call was made to set log level of services - self.service_mock.set_log_levels.assert_called_with( + self.assertIsNone(ret) + self.volume_client.set_service_log_levels.assert_called_with( level='ERROR', - server=self.service_log.host, - binary=self.service_log.binary, - prefix=self.service_log.prefix, + server=self.log_level.host, + binary=self.log_level.binary, + prefix='cinder.api.common', ) def test_block_storage_log_level_set_pre_332(self): arglist = [ 'ERROR', '--host', - self.service_log.host, + self.log_level.host, '--service', 'cinder-api', '--log-prefix', - 'cinder_test.api.common', + 'cinder.api.common', ] verifylist = [ ('level', 'ERROR'), - ('host', self.service_log.host), + ('host', self.log_level.host), ('service', 'cinder-api'), - ('log_prefix', 'cinder_test.api.common'), + ('log_prefix', 'cinder.api.common'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -209,7 +200,7 @@ def test_block_storage_log_level_set_invalid_service_name(self): arglist = [ 'ERROR', '--host', - self.service_log.host, + self.log_level.host, '--service', 'nova-api', '--log-prefix', @@ -217,7 +208,7 @@ def test_block_storage_log_level_set_invalid_service_name(self): ] verifylist = [ ('level', 'ERROR'), - ('host', self.service_log.host), + ('host', self.log_level.host), ('service', 'nova-api'), ('log_prefix', 'cinder.api.common'), ] @@ -237,7 +228,7 @@ def test_block_storage_log_level_set_log_level(self, log_level): arglist = [ log_level, '--host', - self.service_log.host, + self.log_level.host, '--service', 'cinder-api', '--log-prefix', @@ -245,7 +236,7 @@ def test_block_storage_log_level_set_log_level(self, log_level): ] verifylist = [ ('level', log_level.upper()), - ('host', self.service_log.host), + ('host', self.log_level.host), ('service', 'cinder-api'), ('log_prefix', 'cinder.api.common'), ] @@ -263,10 +254,9 @@ def test_block_storage_log_level_set_log_level(self, log_level): self.cmd.take_action(parsed_args) - # checking if proper call was made to set log level of services - self.service_mock.set_log_levels.assert_called_with( + self.volume_client.set_service_log_levels.assert_called_with( level=log_level.upper(), - server=self.service_log.host, - binary=self.service_log.binary, - prefix=self.service_log.prefix, + server=self.log_level.host, + binary=self.log_level.binary, + prefix='cinder.api.common', ) diff --git a/openstackclient/tests/unit/volume/v3/test_block_storage_manage.py b/openstackclient/tests/unit/volume/v3/test_block_storage_manage.py index 55d7baf030..4caf8edb83 100644 --- a/openstackclient/tests/unit/volume/v3/test_block_storage_manage.py +++ b/openstackclient/tests/unit/volume/v3/test_block_storage_manage.py @@ -12,34 +12,65 @@ from unittest import mock +from openstack.block_storage.v3 import ( + manageable_snapshot as _manageable_snapshot, +) +from openstack.block_storage.v3 import manageable_volume as _manageable_volume from osc_lib import exceptions from openstackclient.tests.unit import utils as tests_utils from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes from openstackclient.volume.v3 import block_storage_manage - -class TestBlockStorageManage(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - self.snapshots_mock = self.volume_client.volume_snapshots - self.snapshots_mock.reset_mock() - - -class TestBlockStorageVolumeManage(TestBlockStorageManage): - volume_manage_list = volume_fakes.create_volume_manage_list_records() - +FAKE_VOLUME_MANAGE_LIST = [ + _manageable_volume.ManageableVolume( + reference={'source-name': 'fake-volume'}, + size='1', + safe_to_manage=False, + reason_not_safe='already managed', + cinder_id='fake-volume', + extra_info=None, + ), + _manageable_volume.ManageableVolume( + reference={'source-name': 'fake-volume'}, + size='2', + safe_to_manage=False, + reason_not_safe='already managed', + cinder_id='fake-volume', + extra_info=None, + ), +] + +FAKE_SNAPSHOT_MANAGE_LIST = [ + _manageable_snapshot.ManageableSnapshot( + reference={'source-name': 'fake-snapshot'}, + source_reference={'source-name': 'fake-source'}, + size='1', + safe_to_manage=False, + reason_not_safe='already managed', + cinder_id='fake-snapshot', + extra_info=None, + ), + _manageable_snapshot.ManageableSnapshot( + reference={'source-name': 'fake-snapshot'}, + source_reference={'source-name': 'fake-source'}, + size='2', + safe_to_manage=False, + reason_not_safe='already managed', + cinder_id='fake-snapshot', + extra_info=None, + ), +] + + +class TestBlockStorageVolumeManage(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volumes_mock.list_manageable.return_value = ( - self.volume_manage_list + self.volume_client.manageable_volumes.return_value = iter( + FAKE_VOLUME_MANAGE_LIST ) - # Get the command object to test self.cmd = block_storage_manage.BlockStorageManageVolumes( self.app, None ) @@ -47,54 +78,28 @@ def setUp(self): def test_block_storage_volume_manage_list(self): self.set_volume_api_version('3.8') - arglist = [ - 'fake_host', - ] - verifylist = [ - ('host', 'fake_host'), - ] + arglist = ['fake_host'] + verifylist = [('host', 'fake_host')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - expected_columns = [ - 'reference', - 'size', - 'safe_to_manage', - ] - datalist = [] - for volume_record in self.volume_manage_list: - manage_details = ( - volume_record.reference, - volume_record.size, - volume_record.safe_to_manage, - ) - datalist.append(manage_details) - datalist = tuple(datalist) + expected_columns = ['reference', 'size', 'safe_to_manage'] + expected_data = tuple( + (v.reference, v.size, v.safe_to_manage) + for v in FAKE_VOLUME_MANAGE_LIST + ) self.assertEqual(expected_columns, columns) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(expected_data, tuple(data)) - # checking if proper call was made to get volume manageable list - self.volumes_mock.list_manageable.assert_called_with( - host='fake_host', - detailed=False, - marker=None, - limit=None, - offset=None, - sort=None, - cluster=None, + self.volume_client.manageable_volumes.assert_called_once_with( + details=False, host='fake_host' ) def test_block_storage_volume_manage_list__pre_v38(self): - self.set_volume_api_version('3.7') - - arglist = [ - 'fake_host', - ] - verifylist = [ - ('host', 'fake_host'), - ] + arglist = ['fake_host'] + verifylist = [('host', 'fake_host')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) exc = self.assertRaises( @@ -107,13 +112,8 @@ def test_block_storage_volume_manage_list__pre_v38(self): def test_block_storage_volume_manage_list__pre_v317(self): self.set_volume_api_version('3.16') - arglist = [ - '--cluster', - 'fake_cluster', - ] - verifylist = [ - ('cluster', 'fake_cluster'), - ] + arglist = ['--cluster', 'fake_cluster'] + verifylist = [('cluster', 'fake_cluster')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) exc = self.assertRaises( @@ -127,15 +127,8 @@ def test_block_storage_volume_manage_list__pre_v317(self): def test_block_storage_volume_manage_list__host_and_cluster(self): self.set_volume_api_version('3.17') - arglist = [ - 'fake_host', - '--cluster', - 'fake_cluster', - ] - verifylist = [ - ('host', 'fake_host'), - ('cluster', 'fake_cluster'), - ] + arglist = ['fake_host', '--cluster', 'fake_cluster'] + verifylist = [('host', 'fake_host'), ('cluster', 'fake_cluster')] exc = self.assertRaises( tests_utils.ParserException, self.check_parser, @@ -151,11 +144,7 @@ def test_block_storage_volume_manage_list__detailed(self): """This option is deprecated.""" self.set_volume_api_version('3.8') - arglist = [ - '--detailed', - 'True', - 'fake_host', - ] + arglist = ['--detailed', 'True', 'fake_host'] verifylist = [ ('host', 'fake_host'), ('detailed', 'True'), @@ -177,31 +166,23 @@ def test_block_storage_volume_manage_list__detailed(self): 'cinder_id', 'extra_info', ] - datalist = [] - for volume_record in self.volume_manage_list: - manage_details = ( - volume_record.reference, - volume_record.size, - volume_record.safe_to_manage, - volume_record.reason_not_safe, - volume_record.cinder_id, - volume_record.extra_info, + expected_data = tuple( + ( + v.reference, + v.size, + v.safe_to_manage, + v.reason_not_safe, + v.cinder_id, + v.extra_info, ) - datalist.append(manage_details) - datalist = tuple(datalist) + for v in FAKE_VOLUME_MANAGE_LIST + ) self.assertEqual(expected_columns, columns) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(expected_data, tuple(data)) - # checking if proper call was made to get volume manageable list - self.volumes_mock.list_manageable.assert_called_with( - host='fake_host', - detailed=True, - marker=None, - limit=None, - offset=None, - sort=None, - cluster=None, + self.volume_client.manageable_volumes.assert_called_once_with( + details=True, host='fake_host' ) mock_warning.assert_called_once() self.assertIn( @@ -245,45 +226,39 @@ def test_block_storage_volume_manage_list__all_args(self): 'cinder_id', 'extra_info', ] - datalist = [] - for volume_record in self.volume_manage_list: - manage_details = ( - volume_record.reference, - volume_record.size, - volume_record.safe_to_manage, - volume_record.reason_not_safe, - volume_record.cinder_id, - volume_record.extra_info, + expected_data = tuple( + ( + v.reference, + v.size, + v.safe_to_manage, + v.reason_not_safe, + v.cinder_id, + v.extra_info, ) - datalist.append(manage_details) - datalist = tuple(datalist) + for v in FAKE_VOLUME_MANAGE_LIST + ) self.assertEqual(expected_columns, columns) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(expected_data, tuple(data)) - # checking if proper call was made to get volume manageable list - self.volumes_mock.list_manageable.assert_called_with( + self.volume_client.manageable_volumes.assert_called_once_with( + details=True, host='fake_host', - detailed=True, marker='fake_marker', limit='5', offset='3', sort='size:asc', - cluster=None, ) -class TestBlockStorageSnapshotManage(TestBlockStorageManage): - snapshot_manage_list = volume_fakes.create_snapshot_manage_list_records() - +class TestBlockStorageSnapshotManage(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.snapshots_mock.list_manageable.return_value = ( - self.snapshot_manage_list + self.volume_client.manageable_snapshots.return_value = iter( + FAKE_SNAPSHOT_MANAGE_LIST ) - # Get the command object to test self.cmd = block_storage_manage.BlockStorageManageSnapshots( self.app, None ) @@ -291,12 +266,8 @@ def setUp(self): def test_block_storage_snapshot_manage_list(self): self.set_volume_api_version('3.8') - arglist = [ - 'fake_host', - ] - verifylist = [ - ('host', 'fake_host'), - ] + arglist = ['fake_host'] + verifylist = [('host', 'fake_host')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) @@ -307,40 +278,21 @@ def test_block_storage_snapshot_manage_list(self): 'safe_to_manage', 'source_reference', ] - datalist = [] - for snapshot_record in self.snapshot_manage_list: - manage_details = ( - snapshot_record.reference, - snapshot_record.size, - snapshot_record.safe_to_manage, - snapshot_record.source_reference, - ) - datalist.append(manage_details) - datalist = tuple(datalist) + expected_data = tuple( + (s.reference, s.size, s.safe_to_manage, s.source_reference) + for s in FAKE_SNAPSHOT_MANAGE_LIST + ) self.assertEqual(expected_columns, columns) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(expected_data, tuple(data)) - # checking if proper call was made to get snapshot manageable list - self.snapshots_mock.list_manageable.assert_called_with( - host='fake_host', - detailed=False, - marker=None, - limit=None, - offset=None, - sort=None, - cluster=None, + self.volume_client.manageable_snapshots.assert_called_once_with( + details=False, host='fake_host' ) def test_block_storage_snapshot_manage_list__pre_v38(self): - self.set_volume_api_version('3.7') - - arglist = [ - 'fake_host', - ] - verifylist = [ - ('host', 'fake_host'), - ] + arglist = ['fake_host'] + verifylist = [('host', 'fake_host')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) exc = self.assertRaises( @@ -353,13 +305,8 @@ def test_block_storage_snapshot_manage_list__pre_v38(self): def test_block_storage_snapshot_manage_list__pre_v317(self): self.set_volume_api_version('3.16') - arglist = [ - '--cluster', - 'fake_cluster', - ] - verifylist = [ - ('cluster', 'fake_cluster'), - ] + arglist = ['--cluster', 'fake_cluster'] + verifylist = [('cluster', 'fake_cluster')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) exc = self.assertRaises( @@ -373,15 +320,8 @@ def test_block_storage_snapshot_manage_list__pre_v317(self): def test_block_storage_snapshot_manage_list__host_and_cluster(self): self.set_volume_api_version('3.17') - arglist = [ - 'fake_host', - '--cluster', - 'fake_cluster', - ] - verifylist = [ - ('host', 'fake_host'), - ('cluster', 'fake_cluster'), - ] + arglist = ['fake_host', '--cluster', 'fake_cluster'] + verifylist = [('host', 'fake_host'), ('cluster', 'fake_cluster')] exc = self.assertRaises( tests_utils.ParserException, self.check_parser, @@ -396,11 +336,7 @@ def test_block_storage_snapshot_manage_list__host_and_cluster(self): def test_block_storage_snapshot_manage_list__detailed(self): self.set_volume_api_version('3.8') - arglist = [ - '--detailed', - 'True', - 'fake_host', - ] + arglist = ['--detailed', 'True', 'fake_host'] verifylist = [ ('host', 'fake_host'), ('detailed', 'True'), @@ -423,32 +359,24 @@ def test_block_storage_snapshot_manage_list__detailed(self): 'cinder_id', 'extra_info', ] - datalist = [] - for snapshot_record in self.snapshot_manage_list: - manage_details = ( - snapshot_record.reference, - snapshot_record.size, - snapshot_record.safe_to_manage, - snapshot_record.source_reference, - snapshot_record.reason_not_safe, - snapshot_record.cinder_id, - snapshot_record.extra_info, + expected_data = tuple( + ( + s.reference, + s.size, + s.safe_to_manage, + s.source_reference, + s.reason_not_safe, + s.cinder_id, + s.extra_info, ) - datalist.append(manage_details) - datalist = tuple(datalist) + for s in FAKE_SNAPSHOT_MANAGE_LIST + ) self.assertEqual(expected_columns, columns) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(expected_data, tuple(data)) - # checking if proper call was made to get snapshot manageable list - self.snapshots_mock.list_manageable.assert_called_with( - host='fake_host', - detailed=True, - marker=None, - limit=None, - offset=None, - sort=None, - cluster=None, + self.volume_client.manageable_snapshots.assert_called_once_with( + details=True, host='fake_host' ) mock_warning.assert_called_once() self.assertIn( @@ -493,30 +421,27 @@ def test_block_storage_snapshot_manage_list__all_args(self): 'cinder_id', 'extra_info', ] - datalist = [] - for snapshot_record in self.snapshot_manage_list: - manage_details = ( - snapshot_record.reference, - snapshot_record.size, - snapshot_record.safe_to_manage, - snapshot_record.source_reference, - snapshot_record.reason_not_safe, - snapshot_record.cinder_id, - snapshot_record.extra_info, + expected_data = tuple( + ( + s.reference, + s.size, + s.safe_to_manage, + s.source_reference, + s.reason_not_safe, + s.cinder_id, + s.extra_info, ) - datalist.append(manage_details) - datalist = tuple(datalist) + for s in FAKE_SNAPSHOT_MANAGE_LIST + ) self.assertEqual(expected_columns, columns) - self.assertEqual(datalist, tuple(data)) + self.assertEqual(expected_data, tuple(data)) - # checking if proper call was made to get snapshot manageable list - self.snapshots_mock.list_manageable.assert_called_with( + self.volume_client.manageable_snapshots.assert_called_once_with( + details=True, host='fake_host', - detailed=True, marker='fake_marker', limit='5', offset='3', sort='size:asc', - cluster=None, ) diff --git a/openstackclient/tests/unit/volume/v3/test_block_storage_resource_filter.py b/openstackclient/tests/unit/volume/v3/test_block_storage_resource_filter.py index 609458f748..80602302a3 100644 --- a/openstackclient/tests/unit/volume/v3/test_block_storage_resource_filter.py +++ b/openstackclient/tests/unit/volume/v3/test_block_storage_resource_filter.py @@ -10,6 +10,7 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.block_storage.v3 import resource_filter as _filters from osc_lib.cli import format_columns from osc_lib import exceptions @@ -17,14 +18,29 @@ from openstackclient.volume.v3 import block_storage_resource_filter -class TestBlockStorageResourceFilterList(volume_fakes.TestVolume): - # The resource filters to be listed - fake_resource_filters = volume_fakes.create_resource_filters() +def _create_fake_resource_filter(): + return _filters.ResourceFilter( + filters=[ + 'name', + 'status', + 'image_metadata', + 'bootable', + 'migration_status', + ], + resource='volume', + ) + +class TestBlockStorageResourceFilterList(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volume_sdk_client.resource_filters.return_value = ( + self.fake_resource_filters = [ + _create_fake_resource_filter(), + _create_fake_resource_filter(), + _create_fake_resource_filter(), + ] + self.volume_client.resource_filters.return_value = ( self.fake_resource_filters ) @@ -56,7 +72,7 @@ def test_resource_filter_list(self): self.assertEqual(expected_data, tuple(data)) # checking if proper call was made to list clusters - self.volume_sdk_client.resource_filters.assert_called_with() + self.volume_client.resource_filters.assert_called_with() def test_resource_filter_list_pre_v333(self): self.set_volume_api_version('3.32') @@ -74,13 +90,11 @@ def test_resource_filter_list_pre_v333(self): class TestBlockStorageResourceFilterShow(volume_fakes.TestVolume): - # The resource filters to be listed - fake_resource_filter = volume_fakes.create_one_resource_filter() - def setUp(self): super().setUp() - self.volume_sdk_client.resource_filters.return_value = iter( + self.fake_resource_filter = _create_fake_resource_filter() + self.volume_client.resource_filters.return_value = iter( [self.fake_resource_filter] ) @@ -113,7 +127,7 @@ def test_resource_filter_show(self): self.assertEqual(expected_data, data) # checking if proper call was made to list clusters - self.volume_sdk_client.resource_filters.assert_called_with( + self.volume_client.resource_filters.assert_called_with( resource='volume' ) diff --git a/openstackclient/tests/unit/volume/v3/test_consistency_group.py b/openstackclient/tests/unit/volume/v3/test_consistency_group.py new file mode 100644 index 0000000000..6703f51cd7 --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_consistency_group.py @@ -0,0 +1,742 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest import mock + +from openstack.block_storage.v3 import ( + consistency_group as _consistency_group, +) +from openstack.block_storage.v3 import ( + consistency_group_snapshot as _consistency_group_snapshot, +) +from openstack.block_storage.v3 import type as _type +from openstack.test import fakes as sdk_fakes +from osc_lib.cli import format_columns +from osc_lib import exceptions + +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import consistency_group + + +class TestConsistencyGroupAddVolume(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ) + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) + + self.cmd = consistency_group.AddVolumeToConsistencyGroup( + self.app, None + ) + + def test_add_one_volume_to_consistency_group(self): + volume = sdk_fakes.generate_fake_resource( + _type.Type, + ) + self.volume_client.find_volume.return_value = volume + arglist = [ + self.consistency_group.id, + volume.id, + ] + verifylist = [ + ('consistency_group', self.consistency_group.id), + ('volumes', [volume.id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.find_volume.assert_called_once_with( + volume.id, ignore_missing=False + ) + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, add_volumes=volume.id + ) + self.assertIsNone(result) + + def test_add_multiple_volumes_to_consistency_group(self): + volumes = [ + sdk_fakes.generate_fake_resource(_type.Type), + sdk_fakes.generate_fake_resource(_type.Type), + ] + self.volume_client.find_volume.side_effect = volumes + arglist = [ + self.consistency_group.id, + volumes[0].id, + volumes[1].id, + ] + verifylist = [ + ('consistency_group', self.consistency_group.id), + ('volumes', [volumes[0].id, volumes[1].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, + add_volumes=volumes[0].id + ',' + volumes[1].id, + ) + self.assertIsNone(result) + + @mock.patch.object(consistency_group.LOG, 'error') + def test_add_multiple_volumes_to_consistency_group_with_exception( + self, + mock_error, + ): + volume = sdk_fakes.generate_fake_resource(_type.Type) + self.volume_client.find_volume.side_effect = [ + volume, + exceptions.CommandError, + ] + arglist = [ + self.consistency_group.id, + volume.id, + 'unexist_volume', + ] + verifylist = [ + ('consistency_group', self.consistency_group.id), + ('volumes', [volume.id, 'unexist_volume']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + mock_error.assert_called_with( + '%(result)s of %(total)s volumes failed to add.', + {'result': 1, 'total': 2}, + ) + self.assertIsNone(result) + self.volume_client.find_volume.assert_any_call( + volume.id, ignore_missing=False + ) + self.volume_client.find_volume.assert_any_call( + 'unexist_volume', ignore_missing=False + ) + self.assertEqual(2, self.volume_client.find_volume.call_count) + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, add_volumes=volume.id + ) + + +class TestConsistencyGroupCreate(volume_fakes.TestVolume): + columns = ( + 'availability_zone', + 'created_at', + 'description', + 'id', + 'name', + 'status', + 'volume_types', + ) + + def setUp(self): + super().setUp() + + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.new_consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ) + self.consistency_group_snapshot = sdk_fakes.generate_fake_resource( + _consistency_group_snapshot.ConsistencyGroupSnapshot + ) + self.volume_client.create_consistency_group.return_value = ( + self.new_consistency_group + ) + self.volume_client.create_consistency_group_from_source.return_value = self.new_consistency_group + self.volume_client.find_type.return_value = self.volume_type + self.volume_client.find_consistency_group_snapshot.return_value = ( + self.consistency_group_snapshot + ) + self.volume_client.find_consistency_group.return_value = ( + self.new_consistency_group + ) + + self.data = ( + self.new_consistency_group.availability_zone, + self.new_consistency_group.created_at, + self.new_consistency_group.description, + self.new_consistency_group.id, + self.new_consistency_group.name, + self.new_consistency_group.status, + self.new_consistency_group.volume_types, + ) + + self.cmd = consistency_group.CreateConsistencyGroup(self.app, None) + + def test_consistency_group_create(self): + arglist = [ + '--volume-type', + self.volume_type.id, + '--description', + self.new_consistency_group.description, + '--availability-zone', + self.new_consistency_group.availability_zone, + self.new_consistency_group.name, + ] + verifylist = [ + ('volume_type', self.volume_type.id), + ('description', self.new_consistency_group.description), + ( + 'availability_zone', + self.new_consistency_group.availability_zone, + ), + ('name', self.new_consistency_group.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.find_type.assert_called_once_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.find_consistency_group.assert_not_called() + self.volume_client.create_consistency_group.assert_called_once_with( + volume_types=self.volume_type.id, + name=self.new_consistency_group.name, + description=self.new_consistency_group.description, + availability_zone=self.new_consistency_group.availability_zone, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_consistency_group_create_without_name(self): + arglist = [ + '--volume-type', + self.volume_type.id, + '--description', + self.new_consistency_group.description, + '--availability-zone', + self.new_consistency_group.availability_zone, + ] + verifylist = [ + ('volume_type', self.volume_type.id), + ('description', self.new_consistency_group.description), + ( + 'availability_zone', + self.new_consistency_group.availability_zone, + ), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.find_type.assert_called_once_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.find_consistency_group.assert_not_called() + self.volume_client.create_consistency_group.assert_called_once_with( + volume_types=self.volume_type.id, + name=None, + description=self.new_consistency_group.description, + availability_zone=self.new_consistency_group.availability_zone, + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_consistency_group_create_from_source(self): + arglist = [ + '--consistency-group-source', + self.new_consistency_group.id, + '--description', + self.new_consistency_group.description, + self.new_consistency_group.name, + ] + verifylist = [ + ('source', self.new_consistency_group.id), + ('description', self.new_consistency_group.description), + ('name', self.new_consistency_group.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.find_type.assert_not_called() + self.volume_client.find_consistency_group.assert_called_once_with( + self.new_consistency_group.id, ignore_missing=False + ) + self.volume_client.create_consistency_group_from_source.assert_called_once_with( + consistency_group_snapshot=None, + consistency_group=self.new_consistency_group.id, + name=self.new_consistency_group.name, + description=self.new_consistency_group.description, + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_consistency_group_create_from_snapshot(self): + arglist = [ + '--consistency-group-snapshot', + self.consistency_group_snapshot.id, + '--description', + self.new_consistency_group.description, + self.new_consistency_group.name, + ] + verifylist = [ + ('snapshot', self.consistency_group_snapshot.id), + ('description', self.new_consistency_group.description), + ('name', self.new_consistency_group.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.find_type.assert_not_called() + self.volume_client.find_consistency_group_snapshot.assert_called_once_with( + self.consistency_group_snapshot.id, ignore_missing=False + ) + self.volume_client.create_consistency_group_from_source.assert_called_once_with( + consistency_group_snapshot=self.consistency_group_snapshot.id, + consistency_group=None, + name=self.new_consistency_group.name, + description=self.new_consistency_group.description, + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + +class TestConsistencyGroupDelete(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.consistency_groups = [ + sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ), + sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ), + ] + + self.volume_client.find_consistency_group.side_effect = ( + self.consistency_groups + ) + self.volume_client.delete_consistency_group.return_value = None + + self.cmd = consistency_group.DeleteConsistencyGroup(self.app, None) + + def test_consistency_group_delete(self): + arglist = [self.consistency_groups[0].id] + verifylist = [("consistency_groups", [self.consistency_groups[0].id])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_groups[0].id, ignore_missing=False + ) + self.volume_client.delete_consistency_group.assert_called_once_with( + self.consistency_groups[0], force=False + ) + self.assertIsNone(result) + + def test_consistency_group_delete_with_force(self): + arglist = [ + '--force', + self.consistency_groups[0].id, + ] + verifylist = [ + ('force', True), + ("consistency_groups", [self.consistency_groups[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.delete_consistency_group.assert_called_once_with( + self.consistency_groups[0], force=True + ) + self.assertIsNone(result) + + def test_delete_multiple_consistency_groups(self): + self.volume_client.find_consistency_group.side_effect = ( + self.consistency_groups + ) + arglist = [cg.id for cg in self.consistency_groups] + verifylist = [ + ('consistency_groups', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.volume_client.delete_consistency_group.assert_any_call( + self.consistency_groups[0], force=False + ) + self.volume_client.delete_consistency_group.assert_any_call( + self.consistency_groups[1], force=False + ) + self.assertIsNone(result) + + def test_delete_multiple_consistency_groups_with_exception(self): + self.volume_client.find_consistency_group.side_effect = [ + self.consistency_groups[0], + exceptions.CommandError, + ] + arglist = [ + self.consistency_groups[0].id, + 'unexist_consistency_group', + ] + verifylist = [ + ('consistency_groups', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual( + '1 of 2 consistency groups failed to delete.', str(e) + ) + + self.volume_client.find_consistency_group.assert_any_call( + self.consistency_groups[0].id, ignore_missing=False + ) + self.volume_client.find_consistency_group.assert_any_call( + 'unexist_consistency_group', ignore_missing=False + ) + self.assertEqual( + 2, self.volume_client.find_consistency_group.call_count + ) + self.volume_client.delete_consistency_group.assert_called_once_with( + self.consistency_groups[0], force=False + ) + + +class TestConsistencyGroupList(volume_fakes.TestVolume): + column_headers = [ + 'ID', + 'Status', + 'Name', + ] + column_headers_long = [ + 'ID', + 'Status', + 'Availability Zone', + 'Name', + 'Description', + 'Volume Types', + ] + + def setUp(self): + super().setUp() + + self.consistency_groups = [ + sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ), + sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ), + ] + self.volume_client.consistency_groups.return_value = ( + self.consistency_groups + ) + + self.data = [] + for cg in self.consistency_groups: + self.data.append( + ( + cg.id, + cg.status, + cg.name, + ) + ) + self.data_long = [] + for cg in self.consistency_groups: + self.data_long.append( + ( + cg.id, + cg.status, + cg.availability_zone, + cg.name, + cg.description, + format_columns.ListColumn(cg.volume_types), + ) + ) + + self.cmd = consistency_group.ListConsistencyGroup(self.app, None) + + def test_consistency_group_list_without_options(self): + arglist = [] + verifylist = [ + ("all_projects", False), + ("long", False), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.consistency_groups.assert_called_once_with( + all_tenants=False, + ) + self.assertEqual(self.column_headers, columns) + self.assertCountEqual(self.data, list(data)) + + def test_consistency_group_list_with_all_project(self): + arglist = ["--all-projects"] + verifylist = [ + ("all_projects", True), + ("long", False), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.consistency_groups.assert_called_once_with( + all_tenants=True, + ) + self.assertEqual(self.column_headers, columns) + self.assertCountEqual(self.data, list(data)) + + def test_consistency_group_list_with_long(self): + arglist = [ + "--long", + ] + verifylist = [ + ("all_projects", False), + ("long", True), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.consistency_groups.assert_called_once_with( + all_tenants=False, + ) + self.assertEqual(self.column_headers_long, columns) + self.assertCountEqual(self.data_long, list(data)) + + +class TestConsistencyGroupRemoveVolume(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ) + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) + + self.cmd = consistency_group.RemoveVolumeFromConsistencyGroup( + self.app, None + ) + + def test_remove_one_volume_from_consistency_group(self): + volume = sdk_fakes.generate_fake_resource(_type.Type) + self.volume_client.find_volume.return_value = volume + arglist = [ + self.consistency_group.id, + volume.id, + ] + verifylist = [ + ('consistency_group', self.consistency_group.id), + ('volumes', [volume.id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, remove_volumes=volume.id + ) + self.assertIsNone(result) + + def test_remove_multi_volumes_from_consistency_group(self): + volumes = [ + sdk_fakes.generate_fake_resource(_type.Type), + sdk_fakes.generate_fake_resource(_type.Type), + ] + self.volume_client.find_volume.side_effect = volumes + arglist = [ + self.consistency_group.id, + volumes[0].id, + volumes[1].id, + ] + verifylist = [ + ('consistency_group', self.consistency_group.id), + ('volumes', [volumes[0].id, volumes[1].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, + remove_volumes=volumes[0].id + ',' + volumes[1].id, + ) + self.assertIsNone(result) + + @mock.patch.object(consistency_group.LOG, 'error') + def test_remove_multiple_volumes_from_consistency_group_with_exception( + self, + mock_error, + ): + volume = sdk_fakes.generate_fake_resource(_type.Type) + self.volume_client.find_volume.side_effect = [ + volume, + exceptions.CommandError, + ] + arglist = [ + self.consistency_group.id, + volume.id, + 'unexist_volume', + ] + verifylist = [ + ('consistency_group', self.consistency_group.id), + ('volumes', [volume.id, 'unexist_volume']), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + mock_error.assert_called_with( + '%(result)s of %(total)s volumes failed to remove.', + {'result': 1, 'total': 2}, + ) + self.assertIsNone(result) + self.volume_client.find_volume.assert_any_call( + volume.id, ignore_missing=False + ) + self.volume_client.find_volume.assert_any_call( + 'unexist_volume', ignore_missing=False + ) + self.assertEqual(2, self.volume_client.find_volume.call_count) + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, remove_volumes=volume.id + ) + + +class TestConsistencyGroupSet(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ) + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) + + self.cmd = consistency_group.SetConsistencyGroup(self.app, None) + + def test_consistency_group_set_name(self): + new_name = 'new_name' + arglist = [ + '--name', + new_name, + self.consistency_group.id, + ] + verifylist = [ + ('name', new_name), + ('description', None), + ('consistency_group', self.consistency_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, name=new_name + ) + self.assertIsNone(result) + + def test_consistency_group_set_description(self): + new_description = 'new_description' + arglist = [ + '--description', + new_description, + self.consistency_group.id, + ] + verifylist = [ + ('name', None), + ('description', new_description), + ('consistency_group', self.consistency_group.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.update_consistency_group.assert_called_once_with( + self.consistency_group, description=new_description + ) + self.assertIsNone(result) + + +class TestConsistencyGroupShow(volume_fakes.TestVolume): + columns = ( + 'availability_zone', + 'created_at', + 'description', + 'id', + 'name', + 'status', + 'volume_types', + ) + + def setUp(self): + super().setUp() + + self.consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ) + self.data = ( + self.consistency_group.availability_zone, + self.consistency_group.created_at, + self.consistency_group.description, + self.consistency_group.id, + self.consistency_group.name, + self.consistency_group.status, + self.consistency_group.volume_types, + ) + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) + self.cmd = consistency_group.ShowConsistencyGroup(self.app, None) + + def test_consistency_group_show(self): + arglist = [self.consistency_group.id] + verifylist = [("consistency_group", self.consistency_group.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v3/test_consistency_group_snapshot.py b/openstackclient/tests/unit/volume/v3/test_consistency_group_snapshot.py new file mode 100644 index 0000000000..d94a606d9e --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_consistency_group_snapshot.py @@ -0,0 +1,382 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES 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.mock import call + +from openstack.block_storage.v3 import consistency_group as _consistency_group +from openstack.block_storage.v3 import ( + consistency_group_snapshot as _cg_snapshot, +) +from openstack.test import fakes as sdk_fakes +from osc_lib import exceptions + +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import consistency_group_snapshot + + +class TestConsistencyGroupSnapshotCreate(volume_fakes.TestVolume): + _consistency_group_snapshot = sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot + ) + consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ) + + columns = ( + 'consistencygroup_id', + 'created_at', + 'description', + 'id', + 'name', + 'status', + ) + data = ( + _consistency_group_snapshot.consistencygroup_id, + _consistency_group_snapshot.created_at, + _consistency_group_snapshot.description, + _consistency_group_snapshot.id, + _consistency_group_snapshot.name, + _consistency_group_snapshot.status, + ) + + def setUp(self): + super().setUp() + self.volume_client.create_consistency_group_snapshot.return_value = ( + self._consistency_group_snapshot + ) + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) + + # Get the command object to test + self.cmd = consistency_group_snapshot.CreateConsistencyGroupSnapshot( + self.app, None + ) + + def test_consistency_group_snapshot_create(self): + arglist = [ + '--consistency-group', + self.consistency_group.id, + '--description', + self._consistency_group_snapshot.description, + self._consistency_group_snapshot.name, + ] + verifylist = [ + ('consistency_group', self.consistency_group.id), + ('description', self._consistency_group_snapshot.description), + ('snapshot_name', self._consistency_group_snapshot.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.create_consistency_group_snapshot.assert_called_once_with( + consistencygroup_id=self.consistency_group.id, + name=self._consistency_group_snapshot.name, + description=self._consistency_group_snapshot.description, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_consistency_group_snapshot_create_no_consistency_group(self): + arglist = [ + '--description', + self._consistency_group_snapshot.description, + self._consistency_group_snapshot.name, + ] + verifylist = [ + ('description', self._consistency_group_snapshot.description), + ('snapshot_name', self._consistency_group_snapshot.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.find_consistency_group.assert_called_once_with( + self._consistency_group_snapshot.name, ignore_missing=False + ) + self.volume_client.create_consistency_group_snapshot.assert_called_once_with( + consistencygroup_id=self.consistency_group.id, + name=self._consistency_group_snapshot.name, + description=self._consistency_group_snapshot.description, + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + +class TestConsistencyGroupSnapshotDelete(volume_fakes.TestVolume): + consistency_group_snapshots = [ + sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot + ), + sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot + ), + ] + + def setUp(self): + super().setUp() + + self.volume_client.find_consistency_group_snapshot.side_effect = ( + self.consistency_group_snapshots + ) + self.volume_client.delete_consistency_group_snapshot.return_value = ( + None + ) + + # Get the command object to mock + self.cmd = consistency_group_snapshot.DeleteConsistencyGroupSnapshot( + self.app, None + ) + + def test_consistency_group_snapshot_delete(self): + arglist = [self.consistency_group_snapshots[0].id] + verifylist = [ + ( + "consistency_group_snapshot", + [self.consistency_group_snapshots[0].id], + ) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.find_consistency_group_snapshot.assert_called_once_with( + self.consistency_group_snapshots[0].id, ignore_missing=False + ) + self.volume_client.delete_consistency_group_snapshot.assert_called_once_with( + self.consistency_group_snapshots[0] + ) + self.assertIsNone(result) + + def test_multiple_consistency_group_snapshots_delete(self): + arglist = [] + for c in self.consistency_group_snapshots: + arglist.append(c.id) + verifylist = [ + ('consistency_group_snapshot', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + find_calls = [] + delete_calls = [] + for c in self.consistency_group_snapshots: + find_calls.append(call(c.id, ignore_missing=False)) + delete_calls.append(call(c)) + self.volume_client.find_consistency_group_snapshot.assert_has_calls( + find_calls + ) + self.volume_client.delete_consistency_group_snapshot.assert_has_calls( + delete_calls + ) + self.assertIsNone(result) + + def test_delete_with_exception(self): + arglist = ['missing-snapshot'] + verifylist = [('consistency_group_snapshot', ['missing-snapshot'])] + + self.volume_client.find_consistency_group_snapshot.side_effect = ( + exceptions.CommandError + ) + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestConsistencyGroupSnapshotList(volume_fakes.TestVolume): + consistency_group_snapshots = [ + sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot, status='available' + ), + sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot, status='available' + ), + ] + consistency_group = sdk_fakes.generate_fake_resource( + _consistency_group.ConsistencyGroup + ) + + column_headers = [ + 'ID', + 'Status', + 'Name', + ] + column_headers_long = [ + 'ID', + 'Status', + 'ConsistencyGroup ID', + 'Name', + 'Description', + 'Created At', + ] + data = [] + for c in consistency_group_snapshots: + data.append( + ( + c.id, + c.status, + c.name, + ) + ) + data_long = [] + for c in consistency_group_snapshots: + data_long.append( + ( + c.id, + c.status, + c.consistencygroup_id, + c.name, + c.description, + c.created_at, + ) + ) + + def setUp(self): + super().setUp() + + self.volume_client.consistency_group_snapshots.return_value = ( + self.consistency_group_snapshots + ) + self.volume_client.find_consistency_group.return_value = ( + self.consistency_group + ) + # Get the command to test + self.cmd = consistency_group_snapshot.ListConsistencyGroupSnapshot( + self.app, None + ) + + def test_consistency_group_snapshot_list_without_options(self): + arglist = [] + verifylist = [ + ("all_projects", False), + ("long", False), + ("status", None), + ("consistency_group", None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.consistency_group_snapshots.assert_called_once_with( + all_tenants=False, + status=None, + consistencygroup_id=None, + ) + self.assertEqual(self.column_headers, columns) + self.assertEqual(self.data, list(data)) + + def test_consistency_group_snapshot_list_with_long(self): + arglist = [ + "--long", + ] + verifylist = [ + ("all_projects", False), + ("long", True), + ("status", None), + ("consistency_group", None), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.consistency_group_snapshots.assert_called_once_with( + all_tenants=False, + status=None, + consistencygroup_id=None, + ) + self.assertEqual(self.column_headers_long, columns) + self.assertEqual(self.data_long, list(data)) + + def test_consistency_group_snapshot_list_with_options(self): + arglist = [ + "--all-project", + "--status", + self.consistency_group_snapshots[0].status, + "--consistency-group", + self.consistency_group.id, + ] + verifylist = [ + ("all_projects", True), + ("long", False), + ("status", self.consistency_group_snapshots[0].status), + ("consistency_group", self.consistency_group.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.find_consistency_group.assert_called_once_with( + self.consistency_group.id, ignore_missing=False + ) + self.volume_client.consistency_group_snapshots.assert_called_once_with( + all_tenants=True, + status=self.consistency_group_snapshots[0].status, + consistencygroup_id=self.consistency_group.id, + ) + self.assertEqual(self.column_headers, columns) + self.assertEqual(self.data, list(data)) + + +class TestConsistencyGroupSnapshotShow(volume_fakes.TestVolume): + _consistency_group_snapshot = sdk_fakes.generate_fake_resource( + _cg_snapshot.ConsistencyGroupSnapshot + ) + + columns = ( + 'consistencygroup_id', + 'created_at', + 'description', + 'id', + 'name', + 'status', + ) + data = ( + _consistency_group_snapshot.consistencygroup_id, + _consistency_group_snapshot.created_at, + _consistency_group_snapshot.description, + _consistency_group_snapshot.id, + _consistency_group_snapshot.name, + _consistency_group_snapshot.status, + ) + + def setUp(self): + super().setUp() + + self.volume_client.find_consistency_group_snapshot.return_value = ( + self._consistency_group_snapshot + ) + self.cmd = consistency_group_snapshot.ShowConsistencyGroupSnapshot( + self.app, None + ) + + def test_consistency_group_snapshot_show(self): + arglist = [self._consistency_group_snapshot.id] + verifylist = [ + ("consistency_group_snapshot", self._consistency_group_snapshot.id) + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + columns, data = self.cmd.take_action(parsed_args) + self.volume_client.find_consistency_group_snapshot.assert_called_once_with( + self._consistency_group_snapshot.id, ignore_missing=False + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v3/test_qos_specs.py b/openstackclient/tests/unit/volume/v3/test_qos_specs.py new file mode 100644 index 0000000000..5e4717fa15 --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_qos_specs.py @@ -0,0 +1,457 @@ +# Copyright 2015 iWeb Technologies Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from unittest.mock import call + +from openstack.block_storage.v3 import qos_spec as _qos_spec +from openstack.block_storage.v3 import type as _type +from openstack.test import fakes as sdk_fakes +from osc_lib.cli import format_columns +from osc_lib import exceptions + +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import qos_specs + + +class TestQosAssociate(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ) + + self.volume_client.find_qos_spec.return_value = self.qos_spec + self.volume_client.find_type.return_value = self.volume_type + self.cmd = qos_specs.AssociateQos(self.app, None) + + def test_qos_associate(self): + arglist = [self.qos_spec.id, self.volume_type.id] + verifylist = [ + ('qos_spec', self.qos_spec.id), + ('volume_type', self.volume_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.find_qos_spec.assert_called_once_with( + self.qos_spec.id, ignore_missing=False + ) + self.volume_client.find_type.assert_called_once_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.associate_qos_spec.assert_called_once_with( + self.qos_spec.id, self.volume_type.id + ) + self.assertIsNone(result) + + +class TestQosCreate(volume_fakes.TestVolume): + columns = ('consumer', 'id', 'name', 'properties') + + def setUp(self): + super().setUp() + + self.new_qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ) + self.volume_client.create_qos_spec.return_value = self.new_qos_spec + + self.data = ( + self.new_qos_spec.consumer, + self.new_qos_spec.id, + self.new_qos_spec.name, + format_columns.DictColumn(self.new_qos_spec.specs), + ) + + self.cmd = qos_specs.CreateQos(self.app, None) + + def test_qos_create_without_properties(self): + arglist = [ + self.new_qos_spec.name, + ] + verifylist = [ + ('name', self.new_qos_spec.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.create_qos_spec.assert_called_once_with( + name=self.new_qos_spec.name, consumer='both' + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_qos_create_with_consumer(self): + arglist = [ + '--consumer', + self.new_qos_spec.consumer, + self.new_qos_spec.name, + ] + verifylist = [ + ('consumer', self.new_qos_spec.consumer), + ('name', self.new_qos_spec.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.create_qos_spec.assert_called_once_with( + name=self.new_qos_spec.name, + consumer=self.new_qos_spec.consumer, + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_qos_create_with_properties(self): + arglist = [ + '--consumer', + self.new_qos_spec.consumer, + '--property', + 'foo=bar', + '--property', + 'iops=9001', + self.new_qos_spec.name, + ] + verifylist = [ + ('consumer', self.new_qos_spec.consumer), + ('properties', self.new_qos_spec.specs), + ('name', self.new_qos_spec.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.create_qos_spec.assert_called_once_with( + name=self.new_qos_spec.name, + consumer=self.new_qos_spec.consumer, + foo='bar', + iops='9001', + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + +class TestQosDelete(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.qos_specs = [ + sdk_fakes.generate_fake_resource(_qos_spec.QoSSpec, specs={}), + sdk_fakes.generate_fake_resource(_qos_spec.QoSSpec, specs={}), + ] + qos_by_id = {q.id: q for q in self.qos_specs} + self.volume_client.find_qos_spec.side_effect = lambda x, **kwargs: ( + qos_by_id[x] + ) + self.cmd = qos_specs.DeleteQos(self.app, None) + + def test_qos_delete(self): + arglist = [self.qos_specs[0].id] + verifylist = [('qos_specs', [self.qos_specs[0].id])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.delete_qos_spec.assert_called_once_with( + self.qos_specs[0].id, ignore_missing=False, force=False + ) + self.assertIsNone(result) + + def test_qos_delete_with_force(self): + arglist = ['--force', self.qos_specs[0].id] + verifylist = [('force', True), ('qos_specs', [self.qos_specs[0].id])] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.delete_qos_spec.assert_called_once_with( + self.qos_specs[0].id, ignore_missing=False, force=True + ) + self.assertIsNone(result) + + def test_delete_multiple_qoses(self): + arglist = [] + for q in self.qos_specs: + arglist.append(q.id) + verifylist = [ + ('qos_specs', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + calls = [] + for q in self.qos_specs: + calls.append(call(q.id, ignore_missing=False, force=False)) + self.volume_client.delete_qos_spec.assert_has_calls(calls) + self.assertIsNone(result) + + def test_delete_multiple_qoses_with_exception(self): + arglist = [ + self.qos_specs[0].id, + 'unexist_qos', + ] + verifylist = [ + ('qos_specs', arglist), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual( + '1 of 2 QoS specifications failed to delete.', str(e) + ) + + self.volume_client.delete_qos_spec.assert_called_once_with( + self.qos_specs[0].id, ignore_missing=False, force=False + ) + + +class TestQosDisassociate(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, consumer='front-end', specs={} + ) + + self.volume_client.find_qos_spec.return_value = self.qos_spec + self.volume_client.find_type.return_value = self.volume_type + self.cmd = qos_specs.DisassociateQos(self.app, None) + + def test_qos_disassociate_with_volume_type(self): + arglist = [ + '--volume-type', + self.volume_type.id, + self.qos_spec.id, + ] + verifylist = [ + ('volume_type', self.volume_type.id), + ('qos_spec', self.qos_spec.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.disassociate_qos_spec.assert_called_once_with( + self.qos_spec.id, self.volume_type.id + ) + self.assertIsNone(result) + + def test_qos_disassociate_with_all_volume_types(self): + arglist = [ + '--all', + self.qos_spec.id, + ] + verifylist = [('qos_spec', self.qos_spec.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.disassociate_all_qos_spec.assert_called_once_with( + self.qos_spec.id + ) + self.assertIsNone(result) + + +class TestQosList(volume_fakes.TestVolume): + columns = ( + 'ID', + 'Name', + 'Consumer', + 'Associations', + 'Properties', + ) + + def setUp(self): + super().setUp() + + self.qos_specs = [ + sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ), + sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ), + ] + self.qos_association = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpecAssociation + ) + self.volume_client.qos_specs.return_value = self.qos_specs + self.volume_client.qos_spec_associations.return_value = [ + self.qos_association + ] + + self.data = [] + for q in self.qos_specs: + self.data.append( + ( + q.id, + q.name, + q.consumer, + format_columns.ListColumn([self.qos_association.name]), + format_columns.DictColumn(q.specs), + ) + ) + + self.cmd = qos_specs.ListQos(self.app, None) + + def test_qos_list(self): + arglist = [] + verifylist = [] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.volume_client.qos_specs.assert_called_once_with() + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, list(data)) + + +class TestQosSet(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ) + + self.volume_client.find_qos_spec.return_value = self.qos_spec + self.cmd = qos_specs.SetQos(self.app, None) + + def test_qos_set_with_properties_with_id(self): + arglist = [ + '--no-property', + '--property', + 'a=b', + '--property', + 'c=d', + self.qos_spec.id, + ] + new_property = {"a": "b", "c": "d"} + verifylist = [ + ('no_property', True), + ('properties', new_property), + ('qos_spec', self.qos_spec.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.delete_qos_spec_metadata.assert_called_once_with( + self.qos_spec.id, + list(self.qos_spec.specs.keys()), + ) + self.volume_client.update_qos_spec.assert_called_once_with( + self.qos_spec.id, a='b', c='d' + ) + self.assertIsNone(result) + + +class TestQosShow(volume_fakes.TestVolume): + columns = ('associations', 'consumer', 'id', 'name', 'properties') + + def setUp(self): + super().setUp() + + self.qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, + consumer='front-end', + specs={'foo': 'bar', 'iops': '9001'}, + ) + self.qos_association = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpecAssociation + ) + self.volume_client.find_qos_spec.return_value = self.qos_spec + self.volume_client.qos_spec_associations.return_value = [ + self.qos_association + ] + + self.data = ( + format_columns.ListColumn([self.qos_association.name]), + self.qos_spec.consumer, + self.qos_spec.id, + self.qos_spec.name, + format_columns.DictColumn(self.qos_spec.specs), + ) + + self.cmd = qos_specs.ShowQos(self.app, None) + + def test_qos_show(self): + arglist = [self.qos_spec.id] + verifylist = [('qos_spec', self.qos_spec.id)] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.volume_client.find_qos_spec.assert_called_once_with( + self.qos_spec.id, ignore_missing=False + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, tuple(data)) + + +class TestQosUnset(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.qos_spec = sdk_fakes.generate_fake_resource( + _qos_spec.QoSSpec, consumer='front-end', specs={} + ) + self.volume_client.find_qos_spec.return_value = self.qos_spec + self.cmd = qos_specs.UnsetQos(self.app, None) + + def test_qos_unset_with_properties(self): + arglist = [ + '--property', + 'iops', + '--property', + 'foo', + self.qos_spec.id, + ] + verifylist = [ + ('properties', ['iops', 'foo']), + ('qos_spec', self.qos_spec.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.delete_qos_spec_metadata.assert_called_once_with( + self.qos_spec.id, ['iops', 'foo'] + ) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v3/test_service.py b/openstackclient/tests/unit/volume/v3/test_service.py index 3333595513..3cac2eece7 100644 --- a/openstackclient/tests/unit/volume/v3/test_service.py +++ b/openstackclient/tests/unit/volume/v3/test_service.py @@ -12,108 +12,83 @@ # under the License. # -from cinderclient import api_versions +from unittest import mock + +from openstack.block_storage.v3 import service as _service +from openstack.test import fakes as sdk_fakes +from osc_lib import exceptions from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes from openstackclient.volume.v3 import service -class TestService(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - # Get a shortcut to the ServiceManager Mock - self.service_mock = self.volume_client.services - self.service_mock.reset_mock() - - -class TestServiceList(TestService): - # The service to be listed - services = volume_fakes.create_one_service() - +class TestServiceList(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.service_mock.list.return_value = [self.services] + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.volume_client.services.return_value = [self.service] - # Get the command object to test self.cmd = service.ListService(self.app, None) def test_service_list(self): arglist = [ '--host', - self.services.host, + self.service.host, '--service', - self.services.binary, + self.service.binary, ] verifylist = [ - ('host', self.services.host), - ('service', self.services.binary), + ('host', self.service.host), + ('service', self.service.binary), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - expected_columns = [ + expected_columns = ( 'Binary', 'Host', 'Zone', 'Status', 'State', 'Updated At', - ] - - # confirming if all expected columns are present in the result. - self.assertEqual(expected_columns, columns) - + ) datalist = ( ( - self.services.binary, - self.services.host, - self.services.zone, - self.services.status, - self.services.state, - self.services.updated_at, + self.service.binary, + self.service.host, + self.service.availability_zone, + self.service.status, + self.service.state, + self.service.updated_at, ), ) - - # confirming if all expected values are present in the result. + self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - - # checking if proper call was made to list services - self.service_mock.list.assert_called_with( - self.services.host, - self.services.binary, + self.volume_client.services.assert_called_with( + host=self.service.host, + binary=self.service.binary, ) - # checking if prohibited columns are present in output - self.assertNotIn("Disabled Reason", columns) - self.assertNotIn(self.services.disabled_reason, tuple(data)) - def test_service_list_with_long_option(self): arglist = [ '--host', - self.services.host, + self.service.host, '--service', - self.services.binary, + self.service.binary, '--long', ] verifylist = [ - ('host', self.services.host), - ('service', self.services.binary), + ('host', self.service.host), + ('service', self.service.binary), ('long', True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - expected_columns = [ + expected_columns = ( 'Binary', 'Host', 'Zone', @@ -121,55 +96,43 @@ def test_service_list_with_long_option(self): 'State', 'Updated At', 'Disabled Reason', - ] - - # confirming if all expected columns are present in the result. - self.assertEqual(expected_columns, columns) - + ) datalist = ( ( - self.services.binary, - self.services.host, - self.services.zone, - self.services.status, - self.services.state, - self.services.updated_at, - self.services.disabled_reason, + self.service.binary, + self.service.host, + self.service.availability_zone, + self.service.status, + self.service.state, + self.service.updated_at, + self.service.disabled_reason, ), ) - - # confirming if all expected values are present in the result. + self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - - self.service_mock.list.assert_called_with( - self.services.host, - self.services.binary, + self.volume_client.services.assert_called_with( + host=self.service.host, + binary=self.service.binary, ) def test_service_list_with_cluster(self): - self.volume_client.api_version = api_versions.APIVersion('3.7') - cluster = {'cluster': 'fake-cluster'} - cluster_service = volume_fakes.create_one_service(attrs=cluster) - self.service_mock.list.return_value = [cluster_service] + self.set_volume_api_version('3.7') arglist = [ '--host', - cluster_service.host, + self.service.host, '--service', - cluster_service.binary, + self.service.binary, ] verifylist = [ - ('host', cluster_service.host), - ('service', cluster_service.binary), + ('host', self.service.host), + ('service', self.service.binary), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - expected_columns = [ + expected_columns = ( 'Binary', 'Host', 'Zone', @@ -177,60 +140,43 @@ def test_service_list_with_cluster(self): 'State', 'Updated At', 'Cluster', - ] - - # confirming if all expected columns are present in the result. - self.assertEqual(expected_columns, columns) - + ) datalist = ( ( - cluster_service.binary, - cluster_service.host, - cluster_service.zone, - cluster_service.status, - cluster_service.state, - cluster_service.updated_at, - cluster_service.cluster, + self.service.binary, + self.service.host, + self.service.availability_zone, + self.service.status, + self.service.state, + self.service.updated_at, + self.service.cluster, ), ) - - # confirming if all expected values are present in the result. + self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - - # checking if proper call was made to list services - self.service_mock.list.assert_called_with( - cluster_service.host, - cluster_service.binary, + self.volume_client.services.assert_called_with( + host=self.service.host, + binary=self.service.binary, ) - # checking if prohibited columns are present in output - self.assertNotIn("Disabled Reason", columns) - self.assertNotIn(cluster_service.disabled_reason, tuple(data)) - def test_service_list_with_backend_state(self): - self.volume_client.api_version = api_versions.APIVersion('3.49') - backend_state = {'cluster': 'fake-cluster', 'backend_state': 'up'} - backend_service = volume_fakes.create_one_service(attrs=backend_state) - self.service_mock.list.return_value = [backend_service] + self.set_volume_api_version('3.49') arglist = [ '--host', - backend_service.host, + self.service.host, '--service', - backend_service.binary, + self.service.binary, ] verifylist = [ - ('host', backend_service.host), - ('service', backend_service.binary), + ('host', self.service.host), + ('service', self.service.binary), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - expected_columns = [ + expected_columns = ( 'Binary', 'Host', 'Zone', @@ -239,33 +185,167 @@ def test_service_list_with_backend_state(self): 'Updated At', 'Cluster', 'Backend State', - ] - - # confirming if all expected columns are present in the result. - self.assertEqual(expected_columns, columns) - + ) datalist = ( ( - backend_service.binary, - backend_service.host, - backend_service.zone, - backend_service.status, - backend_service.state, - backend_service.updated_at, - backend_service.cluster, - backend_service.backend_state, + self.service.binary, + self.service.host, + self.service.availability_zone, + self.service.status, + self.service.state, + self.service.updated_at, + self.service.cluster, + self.service.backend_state, ), ) - - # confirming if all expected values are present in the result. + self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) + self.volume_client.services.assert_called_with( + host=self.service.host, + binary=self.service.binary, + ) + + +class TestServiceSet(volume_fakes.TestVolume): + def setUp(self): + super().setUp() - # checking if proper call was made to list services - self.service_mock.list.assert_called_with( - backend_service.host, - backend_service.binary, + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.service.enable = mock.Mock(autospec=True) + self.service.disable = mock.Mock(autospec=True) + self.volume_client.find_service.return_value = self.service + + self.cmd = service.SetService(self.app, None) + + def test_service_set_nothing(self): + arglist = [ + self.service.host, + self.service.binary, + ] + verifylist = [ + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + + self.service.enable.assert_not_called() + self.service.disable.assert_not_called() + self.assertIsNone(result) + + def test_service_set_enable(self): + arglist = [ + '--enable', + self.service.host, + self.service.binary, + ] + verifylist = [ + ('enable', True), + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.service.enable.assert_called_with(self.volume_client) + self.service.disable.assert_not_called() + self.assertIsNone(result) + + def test_service_set_disable(self): + arglist = [ + '--disable', + self.service.host, + self.service.binary, + ] + verifylist = [ + ('disable', True), + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.service.enable.assert_not_called() + self.service.disable.assert_called_with( + self.volume_client, reason=None ) + self.assertIsNone(result) + + def test_service_set_disable_with_reason(self): + reason = 'earthquake' + arglist = [ + '--disable', + '--disable-reason', + reason, + self.service.host, + self.service.binary, + ] + verifylist = [ + ('disable', True), + ('disable_reason', reason), + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.service.enable.assert_not_called() + self.service.disable.assert_called_with( + self.volume_client, reason=reason + ) + self.assertIsNone(result) + + def test_service_set_only_with_disable_reason(self): + reason = 'earthquake' + arglist = [ + '--disable-reason', + reason, + self.service.host, + self.service.binary, + ] + verifylist = [ + ('disable_reason', reason), + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + try: + self.cmd.take_action(parsed_args) + self.fail("CommandError should be raised.") + except exceptions.CommandError as e: + self.assertEqual( + "Cannot specify option --disable-reason without " + "--disable specified.", + str(e), + ) + + def test_service_set_enable_with_disable_reason(self): + reason = 'earthquake' + arglist = [ + '--enable', + '--disable-reason', + reason, + self.service.host, + self.service.binary, + ] + verifylist = [ + ('enable', True), + ('disable_reason', reason), + ('host', self.service.host), + ('service', self.service.binary), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # checking if prohibited columns are present in output - self.assertNotIn("Disabled Reason", columns) - self.assertNotIn(backend_service.disabled_reason, tuple(data)) + try: + self.cmd.take_action(parsed_args) + self.fail("CommandError should be raised.") + except exceptions.CommandError as e: + self.assertEqual( + "Cannot specify option --disable-reason without " + "--disable specified.", + str(e), + ) diff --git a/openstackclient/tests/unit/volume/v3/test_volume.py b/openstackclient/tests/unit/volume/v3/test_volume.py index 6bf43d73f4..135075466c 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume.py +++ b/openstackclient/tests/unit/volume/v3/test_volume.py @@ -13,71 +13,105 @@ import copy from unittest import mock +import uuid +from openstack.block_storage.v3 import backup as _backup from openstack.block_storage.v3 import block_storage_summary as _summary from openstack.block_storage.v3 import snapshot as _snapshot +from openstack.block_storage.v3 import type as _type from openstack.block_storage.v3 import volume as _volume +from openstack import exceptions as sdk_exceptions +from openstack.identity.v3 import project as _project +from openstack.identity.v3 import user as _user from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes +from openstackclient.api import volume_v3 from openstackclient.tests.unit.image.v2 import fakes as image_fakes from openstackclient.tests.unit import utils as test_utils from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes from openstackclient.volume.v3 import volume -# TODO(stephenfin): Combine these two test classes -class TestVolumeCreateLegacy(volume_fakes.TestVolume): - project = identity_fakes.FakeProject.create_one_project() - user = identity_fakes.FakeUser.create_one_user() - +class TestVolumeCreate(volume_fakes.TestVolume): columns = ( 'attachments', 'availability_zone', + 'backup_id', 'bootable', + 'cluster_name', + 'consistencygroup_id', + 'consumes_quota', + 'created_at', 'description', + 'encrypted', + 'encryption_key_id', + 'group_id', 'id', + 'multiattach', 'name', + 'os-vol-host-attr:host', + 'os-vol-mig-status-attr:migstat', + 'os-vol-mig-status-attr:name_id', + 'os-vol-tenant-attr:tenant_id', 'properties', + 'provider_id', + 'replication_status', + 'service_uuid', + 'shared_targets', 'size', 'snapshot_id', + 'source_volid', 'status', 'type', + 'updated_at', + 'user_id', + 'volume_image_metadata', + 'volume_type_id', ) def setUp(self): super().setUp() - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - - self.consistencygroups_mock = self.volume_client.consistencygroups - self.consistencygroups_mock.reset_mock() - - self.snapshots_mock = self.volume_client.volume_snapshots - self.snapshots_mock.reset_mock() - - self.backups_mock = self.volume_client.backups - self.backups_mock.reset_mock() - - self.new_volume = volume_fakes.create_one_volume() - self.volumes_mock.create.return_value = self.new_volume + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.create_volume.return_value = self.volume self.datalist = ( - self.new_volume.attachments, - self.new_volume.availability_zone, - self.new_volume.bootable, - self.new_volume.description, - self.new_volume.id, - self.new_volume.name, - format_columns.DictColumn(self.new_volume.metadata), - self.new_volume.size, - self.new_volume.snapshot_id, - self.new_volume.status, - self.new_volume.volume_type, + self.volume.attachments, + self.volume.availability_zone, + self.volume.backup_id, + self.volume.is_bootable, + self.volume.cluster_name, + self.volume.consistency_group_id, + self.volume.consumes_quota, + self.volume.created_at, + self.volume.description, + self.volume.is_encrypted, + self.volume.encryption_key_id, + self.volume.group_id, + self.volume.id, + self.volume.is_multiattach, + self.volume.name, + self.volume.host, + self.volume.migration_status, + self.volume.migration_id, + self.volume.project_id, + format_columns.DictColumn(self.volume.metadata), + self.volume.provider_id, + self.volume.replication_status, + self.volume.service_uuid, + self.volume.shared_targets, + self.volume.size, + self.volume.snapshot_id, + self.volume.source_volume_id, + self.volume.status, + self.volume.volume_type, + self.volume.updated_at, + self.volume.user_id, + self.volume.volume_image_metadata, + self.volume.volume_type_id, ) # Get the command object to test @@ -86,87 +120,88 @@ def setUp(self): def test_volume_create_min_options(self): arglist = [ '--size', - str(self.new_volume.size), + str(self.volume.size), ] verifylist = [ - ('size', self.new_volume.size), + ('size', self.volume.size), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, name=None, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, backup_id=None, ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_options(self): - consistency_group = volume_fakes.create_one_consistency_group() - self.consistencygroups_mock.get.return_value = consistency_group + consistency_group_id = 'cg123' arglist = [ '--size', - str(self.new_volume.size), + str(self.volume.size), '--description', - self.new_volume.description, + self.volume.description, '--type', - self.new_volume.volume_type, + self.volume.volume_type, '--availability-zone', - self.new_volume.availability_zone, + self.volume.availability_zone, '--consistency-group', - consistency_group.id, + consistency_group_id, '--hint', 'k=v', - self.new_volume.name, + self.volume.name, ] verifylist = [ - ('size', self.new_volume.size), - ('description', self.new_volume.description), - ('type', self.new_volume.volume_type), - ('availability_zone', self.new_volume.availability_zone), - ('consistency_group', consistency_group.id), + ('size', self.volume.size), + ('description', self.volume.description), + ('type', self.volume.volume_type), + ('availability_zone', self.volume.availability_zone), + ('consistency_group', consistency_group_id), ('hint', {'k': 'v'}), - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. - columns, data = self.cmd.take_action(parsed_args) - - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + with mock.patch.object( + volume_v3, + 'find_consistency_group', + return_value={'id': consistency_group_id}, + ) as mock_find_cg: + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, - description=self.new_volume.description, - volume_type=self.new_volume.volume_type, - availability_zone=self.new_volume.availability_zone, + name=self.volume.name, + description=self.volume.description, + volume_type=self.volume.volume_type, + availability_zone=self.volume.availability_zone, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=consistency_group.id, + image_id=None, + source_volume_id=None, + consistency_group_id=consistency_group_id, scheduler_hints={'k': 'v'}, backup_id=None, ) + mock_find_cg.assert_called_once_with( + self.volume_client, consistency_group_id + ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_properties(self): arglist = [ @@ -175,38 +210,35 @@ def test_volume_create_properties(self): '--property', 'Beta=b', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('properties', {'Alpha': 'a', 'Beta': 'b'}), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata={'Alpha': 'a', 'Beta': 'b'}, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, backup_id=None, ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_image_id(self): image = image_fakes.create_one_image() @@ -216,38 +248,35 @@ def test_volume_create_image_id(self): '--image', image.id, '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('image', image.id), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=image.id, - source_volid=None, - consistencygroup_id=None, + image_id=image.id, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, backup_id=None, ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_image_name(self): image = image_fakes.create_one_image() @@ -257,177 +286,173 @@ def test_volume_create_image_name(self): '--image', image.name, '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('image', image.name), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=image.id, - source_volid=None, - consistencygroup_id=None, + image_id=image.id, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, backup_id=None, ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_with_snapshot(self): - snapshot = volume_fakes.create_one_snapshot() - self.new_volume.snapshot_id = snapshot.id + snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) + self.volume_client.find_snapshot.return_value = snapshot + arglist = [ '--snapshot', - self.new_volume.snapshot_id, - self.new_volume.name, + snapshot.id, + self.volume.name, ] verifylist = [ - ('snapshot', self.new_volume.snapshot_id), - ('name', self.new_volume.name), + ('snapshot', snapshot.id), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.snapshots_mock.get.return_value = snapshot - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_once_with( + self.volume_client.create_volume.assert_called_with( size=snapshot.size, snapshot_id=snapshot.id, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, backup_id=None, ) + self.volume_client.find_snapshot.assert_called_once_with( + snapshot.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_with_backup(self): self.set_volume_api_version('3.47') - backup = volume_fakes.create_one_backup() - self.new_volume.backup_id = backup.id + backup = sdk_fakes.generate_fake_resource(_backup.Backup) + self.volume_client.find_backup.return_value = backup + arglist = [ '--backup', - self.new_volume.backup_id, - self.new_volume.name, + backup.id, + self.volume.name, ] verifylist = [ - ('backup', self.new_volume.backup_id), - ('name', self.new_volume.name), + ('backup', backup.id), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.backups_mock.get.return_value = backup - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_once_with( + self.volume_client.create_volume.assert_called_with( size=backup.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, backup_id=backup.id, ) + self.volume_client.find_backup.assert_called_once_with( + backup.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_with_backup_pre_v347(self): - backup = volume_fakes.create_one_backup() - self.new_volume.backup_id = backup.id + backup = sdk_fakes.generate_fake_resource(_backup.Backup) + self.volume_client.find_backup.return_value = backup + arglist = [ '--backup', - self.new_volume.backup_id, - self.new_volume.name, + backup.id, + self.volume.name, ] verifylist = [ - ('backup', self.new_volume.backup_id), - ('name', self.new_volume.name), + ('backup', backup.id), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.backups_mock.get.return_value = backup - exc = self.assertRaises( exceptions.CommandError, self.cmd.take_action, parsed_args ) self.assertIn("--os-volume-api-version 3.47 or greater", str(exc)) + self.volume_client.create_volume.assert_not_called() + def test_volume_create_with_source_volume(self): - source_vol = "source_vol" + source_volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = source_volume + arglist = [ '--source', - self.new_volume.id, - source_vol, + source_volume.id, + self.volume.name, ] verifylist = [ - ('source', self.new_volume.id), - ('name', source_vol), + ('source', source_volume.id), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.volumes_mock.get.return_value = self.new_volume - - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_once_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=source_volume.size, snapshot_id=None, - name=source_vol, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=self.new_volume.id, - consistencygroup_id=None, + image_id=None, + source_volume_id=source_volume.id, + consistency_group_id=None, scheduler_hints=None, backup_id=None, ) + self.volume_client.find_volume.assert_called_once_with( + source_volume.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) @mock.patch.object(utils, 'wait_for_status', return_value=True) def test_volume_create_with_bootable_and_readonly(self, mock_wait): @@ -435,192 +460,191 @@ def test_volume_create_with_bootable_and_readonly(self, mock_wait): '--bootable', '--read-only', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('bootable', True), ('read_only', True), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, backup_id=None, ) - - self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) - self.volumes_mock.set_bootable.assert_called_with( - self.new_volume.id, True + self.volume_client.set_volume_bootable_status.assert_called_once_with( + self.volume, True ) - self.volumes_mock.update_readonly_flag.assert_called_with( - self.new_volume.id, True + self.volume_client.set_volume_readonly.assert_called_once_with( + self.volume, True ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + @mock.patch.object(utils, 'wait_for_status', return_value=True) def test_volume_create_with_nonbootable_and_readwrite(self, mock_wait): arglist = [ '--non-bootable', '--read-write', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('bootable', False), ('read_only', False), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, backup_id=None, ) - - self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) - self.volumes_mock.set_bootable.assert_called_with( - self.new_volume.id, False + self.volume_client.set_volume_bootable_status.assert_called_once_with( + self.volume, False ) - self.volumes_mock.update_readonly_flag.assert_called_with( - self.new_volume.id, False + self.volume_client.set_volume_readonly.assert_called_once_with( + self.volume, False ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist, data) + @mock.patch.object(volume.LOG, 'error') @mock.patch.object(utils, 'wait_for_status', return_value=True) def test_volume_create_with_bootable_and_readonly_fail( self, mock_wait, mock_error ): - self.volumes_mock.set_bootable.side_effect = exceptions.CommandError() - - self.volumes_mock.update_readonly_flag.side_effect = ( - exceptions.CommandError() + self.volume_client.set_volume_bootable_status.side_effect = ( + sdk_exceptions.NotFoundException('foo') + ) + self.volume_client.set_volume_readonly.side_effect = ( + sdk_exceptions.NotFoundException('foo') ) arglist = [ '--bootable', '--read-only', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('bootable', True), ('read_only', True), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, backup_id=None, ) + self.volume_client.set_volume_bootable_status.assert_called_once_with( + self.volume, True + ) + self.volume_client.set_volume_readonly.assert_called_once_with( + self.volume, True + ) self.assertEqual(2, mock_error.call_count) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) - self.volumes_mock.set_bootable.assert_called_with( - self.new_volume.id, True - ) - self.volumes_mock.update_readonly_flag.assert_called_with( - self.new_volume.id, True - ) + self.assertEqual(self.datalist, data) @mock.patch.object(volume.LOG, 'error') @mock.patch.object(utils, 'wait_for_status', return_value=False) def test_volume_create_non_available_with_readonly( - self, - mock_wait, - mock_error, + self, mock_wait, mock_error ): arglist = [ '--non-bootable', '--read-only', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('bootable', False), ('read_only', True), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints=None, backup_id=None, ) self.assertEqual(2, mock_error.call_count) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_without_size(self): arglist = [ - self.new_volume.name, + self.volume.name, ] verifylist = [ - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -637,15 +661,15 @@ def test_volume_create_with_multi_source(self): '--snapshot', 'source_snapshot', '--size', - str(self.new_volume.size), - self.new_volume.name, + str(self.volume.size), + self.volume.name, ] verifylist = [ ('image', 'source_image'), ('source', 'source_volume'), ('snapshot', 'source_snapshot'), - ('size', self.new_volume.size), - ('name', self.new_volume.name), + ('size', self.volume.size), + ('name', self.volume.name), ] self.assertRaises( @@ -664,7 +688,7 @@ def test_volume_create_hints(self): """ arglist = [ '--size', - str(self.new_volume.size), + str(self.volume.size), '--hint', 'k=v', '--hint', @@ -679,10 +703,10 @@ def test_volume_create_hints(self): 'local_to_instance=v6', '--hint', 'different_host=v7', - self.new_volume.name, + self.volume.name, ] verifylist = [ - ('size', self.new_volume.size), + ('size', self.volume.size), ( 'hint', { @@ -692,26 +716,23 @@ def test_volume_create_hints(self): 'different_host': ['v5', 'v7'], }, ), - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns a two-part tuple with a tuple of column names and a tuple of - # data to be shown. columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.create.assert_called_with( - size=self.new_volume.size, + self.volume_client.create_volume.assert_called_with( + size=self.volume.size, snapshot_id=None, - name=self.new_volume.name, + name=self.volume.name, description=None, volume_type=None, availability_zone=None, metadata=None, - imageRef=None, - source_volid=None, - consistencygroup_id=None, + image_id=None, + source_volume_id=None, + consistency_group_id=None, scheduler_hints={ 'k': 'v2', 'same_host': ['v3', 'v4'], @@ -722,108 +743,28 @@ def test_volume_create_hints(self): ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) - - -class TestVolumeCreate(volume_fakes.TestVolume): - columns = ( - 'attachments', - 'availability_zone', - 'consistency_group_id', - 'created_at', - 'description', - 'extended_replication_status', - 'group_id', - 'host', - 'id', - 'image_id', - 'is_bootable', - 'is_encrypted', - 'is_multiattach', - 'location', - 'metadata', - 'migration_id', - 'migration_status', - 'name', - 'project_id', - 'provider_id', - 'replication_driver_data', - 'replication_status', - 'scheduler_hints', - 'size', - 'snapshot_id', - 'source_volume_id', - 'status', - 'updated_at', - 'user_id', - 'volume_image_metadata', - 'volume_type', - ) - - def setUp(self): - super().setUp() - - self.new_volume = sdk_fakes.generate_fake_resource( - _volume.Volume, **{'size': 1} - ) - - self.datalist = ( - self.new_volume.attachments, - self.new_volume.availability_zone, - self.new_volume.consistency_group_id, - self.new_volume.created_at, - self.new_volume.description, - self.new_volume.extended_replication_status, - self.new_volume.group_id, - self.new_volume.host, - self.new_volume.id, - self.new_volume.image_id, - self.new_volume.is_bootable, - self.new_volume.is_encrypted, - self.new_volume.is_multiattach, - self.new_volume.location, - self.new_volume.metadata, - self.new_volume.migration_id, - self.new_volume.migration_status, - self.new_volume.name, - self.new_volume.project_id, - self.new_volume.provider_id, - self.new_volume.replication_driver_data, - self.new_volume.replication_status, - self.new_volume.scheduler_hints, - self.new_volume.size, - self.new_volume.snapshot_id, - self.new_volume.source_volume_id, - self.new_volume.status, - self.new_volume.updated_at, - self.new_volume.user_id, - self.new_volume.volume_image_metadata, - self.new_volume.volume_type, - ) - - # Get the command object to test - self.cmd = volume.CreateVolume(self.app, None) + self.assertEqual(self.datalist, data) def test_volume_create_remote_source(self): - self.volume_sdk_client.manage_volume.return_value = self.new_volume + self.volume_client.manage_volume.return_value = self.volume arglist = [ '--remote-source', 'key=val', '--host', 'fake_host', - self.new_volume.name, + self.volume.name, ] verifylist = [ ('remote_source', {'key': 'val'}), ('host', 'fake_host'), - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.manage_volume.assert_called_with( + self.volume_client.manage_volume.assert_called_with( host='fake_host', ref={'key': 'val'}, name=parsed_args.name, @@ -836,7 +777,7 @@ def test_volume_create_remote_source(self): ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.datalist, data) + self.assertEqual(self.datalist, data) def test_volume_create_remote_source_pre_v316(self): self.set_volume_api_version('3.15') @@ -845,12 +786,12 @@ def test_volume_create_remote_source_pre_v316(self): 'key=val', '--cluster', 'fake_cluster', - self.new_volume.name, + self.volume.name, ] verifylist = [ ('remote_source', {'key': 'val'}), ('cluster', 'fake_cluster'), - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -870,13 +811,13 @@ def test_volume_create_remote_source_host_and_cluster(self): 'fake_host', '--cluster', 'fake_cluster', - self.new_volume.name, + self.volume.name, ] verifylist = [ ('remote_source', {'key': 'val'}), ('host', 'fake_host'), ('cluster', 'fake_cluster'), - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -891,11 +832,11 @@ def test_volume_create_remote_source_no_host_or_cluster(self): arglist = [ '--remote-source', 'key=val', - self.new_volume.name, + self.volume.name, ] verifylist = [ ('remote_source', {'key': 'val'}), - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -909,15 +850,15 @@ def test_volume_create_remote_source_no_host_or_cluster(self): def test_volume_create_remote_source_size(self): arglist = [ '--size', - str(self.new_volume.size), + str(self.volume.size), '--remote-source', 'key=val', - self.new_volume.name, + self.volume.name, ] verifylist = [ - ('size', self.new_volume.size), + ('size', self.volume.size), ('remote_source', {'key': 'val'}), - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -933,15 +874,15 @@ def test_volume_create_remote_source_size(self): def test_volume_create_host_no_remote_source(self): arglist = [ '--size', - str(self.new_volume.size), + str(self.volume.size), '--host', 'fake_host', - self.new_volume.name, + self.volume.name, ] verifylist = [ - ('size', self.new_volume.size), + ('size', self.volume.size), ('host', 'fake_host'), - ('name', self.new_volume.name), + ('name', self.volume.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -954,16 +895,14 @@ def test_volume_create_host_no_remote_source(self): ) -class TestVolumeDeleteLegacy(volume_fakes.TestVolume): +class TestVolumeDelete(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - - self.volumes_mock.delete.return_value = None - self.volumes = volume_fakes.create_volumes(count=1) - self.volumes_mock.get = volume_fakes.get_volumes(self.volumes, 0) + self.volumes = list(sdk_fakes.generate_fake_resources(_volume.Volume)) + self.volume_client.find_volume.side_effect = self.volumes + self.volume_client.delete_volume.return_value = None + self.volume_client.unmanage_volume.return_value = None # Get the command object to mock self.cmd = volume.DeleteVolume(self.app, None) @@ -972,62 +911,75 @@ def test_volume_delete_one_volume(self): arglist = [self.volumes[0].id] verifylist = [ ("force", False), - ("purge", False), + ("cascade", False), ("volumes", [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.volumes_mock.delete.assert_called_once_with( - self.volumes[0].id, cascade=False + self.volume_client.find_volume.assert_called_once_with( + self.volumes[0].id, ignore_missing=False + ) + self.volume_client.delete_volume.assert_called_once_with( + self.volumes[0].id, cascade=False, force=False ) - self.assertIsNone(result) def test_volume_delete_multi_volumes(self): arglist = [v.id for v in self.volumes] verifylist = [ ('force', False), - ('purge', False), + ('cascade', False), ('volumes', arglist), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - - calls = [mock.call(v.id, cascade=False) for v in self.volumes] - self.volumes_mock.delete.assert_has_calls(calls) self.assertIsNone(result) + self.volume_client.find_volume.assert_has_calls( + [mock.call(v.id, ignore_missing=False) for v in self.volumes] + ) + self.volume_client.delete_volume.assert_has_calls( + [mock.call(v.id, cascade=False, force=False) for v in self.volumes] + ) + def test_volume_delete_multi_volumes_with_exception(self): + self.volume_client.find_volume.side_effect = [ + self.volumes[0], + sdk_exceptions.NotFoundException(), + ] + arglist = [ self.volumes[0].id, 'unexist_volume', ] verifylist = [ ('force', False), - ('purge', False), - ('volumes', arglist), + ('cascade', False), + ('volumes', [self.volumes[0].id, 'unexist_volume']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.volumes[0], exceptions.CommandError] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('1 of 2 volumes failed to delete.', str(e)) - - find_mock.assert_any_call(self.volumes_mock, self.volumes[0].id) - find_mock.assert_any_call(self.volumes_mock, 'unexist_volume') - - self.assertEqual(2, find_mock.call_count) - self.volumes_mock.delete.assert_called_once_with( - self.volumes[0].id, cascade=False - ) + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertEqual('1 of 2 volumes failed to delete.', str(exc)) + + self.volume_client.find_volume.assert_has_calls( + [ + mock.call(self.volumes[0].id, ignore_missing=False), + mock.call('unexist_volume', ignore_missing=False), + ] + ) + self.volume_client.delete_volume.assert_has_calls( + [ + mock.call(self.volumes[0].id, cascade=False, force=False), + ] + ) def test_volume_delete_with_purge(self): arglist = [ @@ -1036,18 +988,43 @@ def test_volume_delete_with_purge(self): ] verifylist = [ ('force', False), - ('purge', True), + ('cascade', True), ('volumes', [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.volumes_mock.delete.assert_called_once_with( - self.volumes[0].id, cascade=True + self.volume_client.find_volume.assert_called_once_with( + self.volumes[0].id, ignore_missing=False + ) + self.volume_client.delete_volume.assert_called_once_with( + self.volumes[0].id, cascade=True, force=False ) + + def test_volume_delete_with_cascade(self): + arglist = [ + '--cascade', + self.volumes[0].id, + ] + verifylist = [ + ('force', False), + ('cascade', True), + ('volumes', [self.volumes[0].id]), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_once_with( + self.volumes[0].id, ignore_missing=False + ) + self.volume_client.delete_volume.assert_called_once_with( + self.volumes[0].id, cascade=True, force=False + ) + def test_volume_delete_with_force(self): arglist = [ '--force', @@ -1055,82 +1032,73 @@ def test_volume_delete_with_force(self): ] verifylist = [ ('force', True), - ('purge', False), + ('cascade', False), ('volumes', [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - - self.volumes_mock.force_delete.assert_called_once_with( - self.volumes[0].id - ) self.assertIsNone(result) - -class TestVolumeDelete(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - self.volume_sdk_client.unmanage_volume.return_value = None - - # Get the command object to mock - self.cmd = volume.DeleteVolume(self.app, None) + self.volume_client.find_volume.assert_called_once_with( + self.volumes[0].id, ignore_missing=False + ) + self.volume_client.delete_volume.assert_called_once_with( + self.volumes[0].id, cascade=False, force=True + ) def test_volume_delete_remote(self): - vol = sdk_fakes.generate_fake_resource(_volume.Volume, **{'size': 1}) - self.volumes_mock.get.return_value = vol - - arglist = ['--remote', vol.id] + arglist = ['--remote', self.volumes[0].id] verifylist = [ ("remote", True), ("force", False), - ("purge", False), - ("volumes", [vol.id]), + ("cascade", False), + ("volumes", [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - - self.volume_sdk_client.unmanage_volume.assert_called_once_with(vol.id) self.assertIsNone(result) - def test_volume_delete_multi_volumes_remote(self): - volumes = sdk_fakes.generate_fake_resources( - _volume.Volume, count=3, attrs={'size': 1} + self.volume_client.find_volume.assert_called_once_with( + self.volumes[0].id, ignore_missing=False + ) + self.volume_client.delete_volume.assert_not_called() + self.volume_client.unmanage_volume.assert_called_once_with( + self.volumes[0].id ) - arglist = ['--remote'] - arglist += [v.id for v in volumes] + def test_volume_delete_multi_volumes_remote(self): + arglist = ['--remote'] + [v.id for v in self.volumes] verifylist = [ ('remote', True), ('force', False), - ('purge', False), + ('cascade', False), ('volumes', arglist[1:]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - - calls = [mock.call(v.id) for v in volumes] - self.volume_sdk_client.unmanage_volume.assert_has_calls(calls) self.assertIsNone(result) - def test_volume_delete_remote_with_purge(self): - vol = sdk_fakes.generate_fake_resource(_volume.Volume, **{'size': 1}) + self.volume_client.find_volume.assert_has_calls( + [mock.call(v.id, ignore_missing=False) for v in self.volumes] + ) + self.volume_client.delete_volume.assert_not_called() + self.volume_client.unmanage_volume.assert_has_calls( + [mock.call(v.id) for v in self.volumes] + ) + def test_volume_delete_remote_with_purge(self): arglist = [ '--remote', '--purge', - vol.id, + self.volumes[0].id, ] verifylist = [ ('remote', True), ('force', False), - ('purge', True), - ('volumes', [vol.id]), + ('volumes', [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1138,24 +1106,26 @@ def test_volume_delete_remote_with_purge(self): exceptions.CommandError, self.cmd.take_action, parsed_args ) self.assertIn( - "The --force and --purge options are not supported with the " + "The --force and --cascade options are not supported with the " "--remote parameter.", str(exc), ) - def test_volume_delete_remote_with_force(self): - vol = sdk_fakes.generate_fake_resource(_volume.Volume, **{'size': 1}) + self.volume_client.find_volume.assert_not_called() + self.volume_client.delete_volume.assert_not_called() + self.volume_client.unmanage_volume.assert_not_called() + def test_volume_delete_remote_with_force(self): arglist = [ '--remote', '--force', - vol.id, + self.volumes[0].id, ] verifylist = [ ('remote', True), ('force', True), - ('purge', False), - ('volumes', [vol.id]), + ('cascade', False), + ('volumes', [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1163,44 +1133,37 @@ def test_volume_delete_remote_with_force(self): exceptions.CommandError, self.cmd.take_action, parsed_args ) self.assertIn( - "The --force and --purge options are not supported with the " + "The --force and --cascade options are not supported with the " "--remote parameter.", str(exc), ) + self.volume_client.find_volume.assert_not_called() + self.volume_client.delete_volume.assert_not_called() + self.volume_client.unmanage_volume.assert_not_called() + class TestVolumeList(volume_fakes.TestVolume): - project = identity_fakes.FakeProject.create_one_project() - user = identity_fakes.FakeUser.create_one_user() + project = sdk_fakes.generate_fake_resource(_project.Project) + user = sdk_fakes.generate_fake_resource(_user.User) - columns = [ + columns = ( 'ID', 'Name', 'Status', 'Size', 'Attached to', - ] + ) def setUp(self): super().setUp() - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() - - self.users_mock = self.identity_client.users - self.users_mock.reset_mock() - - self.mock_volume = volume_fakes.create_one_volume() - self.volumes_mock.list.return_value = [self.mock_volume] - - self.users_mock.get.return_value = self.user + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.volumes.return_value = [self.volume] - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = self.project + self.identity_sdk_client.find_user.return_value = self.user - # Get the command object to test self.cmd = volume.ListVolume(self.app, None) def test_volume_list_no_options(self): @@ -1217,28 +1180,15 @@ def test_volume_list_no_options(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, - ) - + self.volume_client.volumes.assert_called_once_with(all_projects=False) self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1260,28 +1210,17 @@ def test_volume_list_project(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': True, - 'project_id': self.project.id, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + project_id=self.project.id, all_projects=True ) - self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1306,28 +1245,17 @@ def test_volume_list_project_domain(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': True, - 'project_id': self.project.id, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + project_id=self.project.id, all_projects=True ) - self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1349,27 +1277,17 @@ def test_volume_list_user(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': self.user.id, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + user_id=self.user.id, all_projects=False ) self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1394,28 +1312,17 @@ def test_volume_list_user_domain(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': self.user.id, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + user_id=self.user.id, all_projects=False ) - self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1423,12 +1330,12 @@ def test_volume_list_user_domain(self): def test_volume_list_name(self): arglist = [ '--name', - self.mock_volume.name, + self.volume.name, ] verifylist = [ ('long', False), ('all_projects', False), - ('name', self.mock_volume.name), + ('name', self.volume.name), ('status', None), ('marker', None), ('limit', None), @@ -1437,28 +1344,17 @@ def test_volume_list_name(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': None, - 'name': self.mock_volume.name, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + name=self.volume.name, all_projects=False ) - self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1466,13 +1362,13 @@ def test_volume_list_name(self): def test_volume_list_status(self): arglist = [ '--status', - self.mock_volume.status, + self.volume.status, ] verifylist = [ ('long', False), ('all_projects', False), ('name', None), - ('status', self.mock_volume.status), + ('status', self.volume.status), ('marker', None), ('limit', None), ] @@ -1480,28 +1376,17 @@ def test_volume_list_status(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': None, - 'name': None, - 'status': self.mock_volume.status, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, + self.volume_client.volumes.assert_called_once_with( + status=self.volume.status, all_projects=False ) - self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1522,28 +1407,15 @@ def test_volume_list_all_projects(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': True, - 'project_id': None, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, - ) - + self.volume_client.volumes.assert_called_once_with(all_projects=True) self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) self.assertCountEqual(datalist, tuple(data)) @@ -1565,20 +1437,8 @@ def test_volume_list_long(self): columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, - ) - - collist = [ + self.volume_client.volumes.assert_called_once_with(all_projects=False) + columns_long = ( 'ID', 'Name', 'Status', @@ -1587,27 +1447,27 @@ def test_volume_list_long(self): 'Bootable', 'Attached to', 'Properties', - ] - self.assertEqual(collist, columns) - - datalist = ( + ) + data_long = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - self.mock_volume.volume_type, - self.mock_volume.bootable, - volume.AttachmentsColumn(self.mock_volume.attachments), - format_columns.DictColumn(self.mock_volume.metadata), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + self.volume.volume_type, + self.volume.is_bootable, + volume.AttachmentsColumn(self.volume.attachments), + format_columns.DictColumn(self.volume.metadata), ), ) - self.assertCountEqual(datalist, tuple(data)) + print(self.volume.is_bootable) + self.assertEqual(columns_long, columns) + self.assertCountEqual(data_long, tuple(data)) def test_volume_list_with_marker_and_limit(self): arglist = [ "--marker", - self.mock_volume.id, + self.volume.id, "--limit", "2", ] @@ -1616,36 +1476,26 @@ def test_volume_list_with_marker_and_limit(self): ('all_projects', False), ('name', None), ('status', None), - ('marker', self.mock_volume.id), + ('marker', self.volume.id), ('limit', 2), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) + self.volume_client.volumes.assert_called_once_with( + limit=2, marker=self.volume.id, all_projects=False + ) self.assertEqual(self.columns, columns) - datalist = ( ( - self.mock_volume.id, - self.mock_volume.name, - self.mock_volume.status, - self.mock_volume.size, - volume.AttachmentsColumn(self.mock_volume.attachments), + self.volume.id, + self.volume.name, + self.volume.status, + self.volume.size, + volume.AttachmentsColumn(self.volume.attachments), ), ) - - self.volumes_mock.list.assert_called_once_with( - marker=self.mock_volume.id, - limit=2, - search_opts={ - 'status': None, - 'project_id': None, - 'user_id': None, - 'name': None, - 'all_tenants': False, - }, - ) self.assertCountEqual(datalist, tuple(data)) def test_volume_list_negative_limit(self): @@ -1664,110 +1514,139 @@ def test_volume_list_negative_limit(self): verifylist, ) - def test_volume_list_backward_compatibility(self): - arglist = [ - '-c', - 'Display Name', - ] - verifylist = [ - ('columns', ['Display Name']), - ('long', False), - ('all_projects', False), - ('name', None), - ('status', None), - ('marker', None), - ('limit', None), - ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - search_opts = { - 'all_tenants': False, - 'project_id': None, - 'user_id': None, - 'name': None, - 'status': None, - } - self.volumes_mock.list.assert_called_once_with( - search_opts=search_opts, - marker=None, - limit=None, - ) - - self.assertIn('Display Name', columns) - self.assertNotIn('Name', columns) - - for each_volume in data: - self.assertIn(self.mock_volume.name, each_volume) - class TestVolumeMigrate(volume_fakes.TestVolume): - _volume = volume_fakes.create_one_volume() - def setUp(self): super().setUp() - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.volume_client.migrate_volume.return_value = None - self.volumes_mock.get.return_value = self._volume - self.volumes_mock.migrate_volume.return_value = None - # Get the command object to test self.cmd = volume.MigrateVolume(self.app, None) def test_volume_migrate(self): arglist = [ "--host", "host@backend-name#pool", - self._volume.id, + self.volume.id, ] verifylist = [ ("force_host_copy", False), ("lock_volume", False), ("host", "host@backend-name#pool"), - ("volume", self._volume.id), + ("volume", self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.volumes_mock.get.assert_called_once_with(self._volume.id) - self.volumes_mock.migrate_volume.assert_called_once_with( - self._volume.id, "host@backend-name#pool", False, False - ) self.assertIsNone(result) - def test_volume_migrate_with_option(self): + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.migrate_volume.assert_called_once_with( + self.volume.id, + host="host@backend-name#pool", + force_host_copy=False, + lock_volume=False, + cluster=None, + ) + + def test_volume_migrate_with_host(self): arglist = [ "--force-host-copy", "--lock-volume", "--host", "host@backend-name#pool", - self._volume.id, + self.volume.id, ] verifylist = [ ("force_host_copy", True), ("lock_volume", True), ("host", "host@backend-name#pool"), - ("volume", self._volume.id), + ("volume", self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.volumes_mock.get.assert_called_once_with(self._volume.id) - self.volumes_mock.migrate_volume.assert_called_once_with( - self._volume.id, "host@backend-name#pool", True, True + self.assertIsNone(result) + + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.migrate_volume.assert_called_once_with( + self.volume.id, + host="host@backend-name#pool", + force_host_copy=True, + lock_volume=True, + cluster=None, ) + + def test_volume_migrate_with_cluster(self): + self.set_volume_api_version('3.16') + arglist = [ + "--cluster", + "cluster@backend-name#pool", + self.volume.id, + ] + verifylist = [ + ( + "cluster", + "cluster@backend-name#pool", + ), + ("volume", self.volume.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - def test_volume_migrate_without_host(self): + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.migrate_volume.assert_called_once_with( + self.volume.id, + host=None, + force_host_copy=False, + lock_volume=False, + cluster="cluster@backend-name#pool", + ) + + def test_volume_migrate_with_cluster_pre_v316(self): + self.set_volume_api_version('3.15') arglist = [ - self._volume.id, + "--cluster", + "cluster@backend-name#pool", + self.volume.id, + ] + verifylist = [ + ( + "cluster", + "cluster@backend-name#pool", + ), + ("volume", self.volume.id), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + self.volume_client.migrate_volume.assert_not_called() + + def test_volume_migrate_without_host_and_cluster(self): + arglist = [ + self.volume.id, ] verifylist = [ ("force_host_copy", False), ("lock_volume", False), - ("volume", self._volume.id), + ("volume", self.volume.id), ] self.assertRaises( @@ -1777,23 +1656,18 @@ def test_volume_migrate_without_host(self): arglist, verifylist, ) + self.volume_client.find_volume.assert_not_called() + self.volume_client.migrate_volume.assert_not_called() class TestVolumeSet(volume_fakes.TestVolume): - volume_type = volume_fakes.create_one_volume_type() - def setUp(self): super().setUp() - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - - self.types_mock = self.volume_client.volume_types - self.types_mock.reset_mock() - - self.new_volume = volume_fakes.create_one_volume() - self.volumes_mock.get.return_value = self.new_volume - self.types_mock.get.return_value = self.volume_type + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.volume_client.find_type.return_value = self.volume_type # Get the command object to test self.cmd = volume.SetVolume(self.app, None) @@ -1804,19 +1678,24 @@ def test_volume_set_property(self): 'a=b', '--property', 'c=d', - self.new_volume.id, + self.volume.id, ] verifylist = [ ('properties', {'a': 'b', 'c': 'd'}), - ('read_only', None), + ('volume', self.volume.id), ('bootable', None), - ('volume', self.new_volume.id), + ('read_only', None), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.cmd.take_action(parsed_args) - self.volumes_mock.set_metadata.assert_called_with( - self.new_volume.id, parsed_args.properties + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.set_volume_metadata.assert_called_once_with( + self.volume, **parsed_args.properties ) def test_volume_set_image_property(self): @@ -1825,247 +1704,342 @@ def test_volume_set_image_property(self): 'Alpha=a', '--image-property', 'Beta=b', - self.new_volume.id, + self.volume.id, ] verifylist = [ ('image_properties', {'Alpha': 'a', 'Beta': 'b'}), - ('read_only', None), + ('volume', self.volume.id), ('bootable', None), - ('volume', self.new_volume.id), + ('read_only', None), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - # In base command class ShowOne in cliff, abstract method take_action() - # returns nothing - self.cmd.take_action(parsed_args) - self.volumes_mock.set_image_metadata.assert_called_with( - self.new_volume.id, parsed_args.image_properties + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.set_volume_image_metadata.assert_called_once_with( + self.volume, **parsed_args.image_properties ) def test_volume_set_state(self): - arglist = ['--state', 'error', self.new_volume.id] + arglist = ['--state', 'error', self.volume.id] verifylist = [ - ('state', 'error'), ('read_only', None), - ('bootable', None), - ('volume', self.new_volume.id), + ('state', 'error'), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.reset_state.assert_called_with( - self.new_volume.id, 'error' - ) - self.volumes_mock.update_readonly_flag.assert_not_called() self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.reset_volume_status.assert_called_with( + self.volume, status='error' + ) + def test_volume_set_state_failed(self): - self.volumes_mock.reset_state.side_effect = exceptions.CommandError() - arglist = ['--state', 'error', self.new_volume.id] - verifylist = [('state', 'error'), ('volume', self.new_volume.id)] + self.volume_client.reset_volume_status.side_effect = ( + sdk_exceptions.NotFoundException('foo') + ) + + arglist = ['--state', 'error', self.volume.id] + verifylist = [('state', 'error'), ('volume', self.volume.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - 'One or more of the set operations failed', str(e) - ) - self.volumes_mock.reset_state.assert_called_with( - self.new_volume.id, 'error' + exc = self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + self.assertEqual('One or more of the set operations failed', str(exc)) + + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.reset_volume_status.assert_called_with( + self.volume, status='error' ) def test_volume_set_attached(self): - arglist = ['--attached', self.new_volume.id] + arglist = ['--attached', self.volume.id] verifylist = [ ('attached', True), ('detached', False), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.reset_state.assert_called_with( - self.new_volume.id, attach_status='attached', state=None - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.reset_volume_status.assert_called_with( + self.volume, attach_status='attached' + ) + def test_volume_set_detached(self): - arglist = ['--detached', self.new_volume.id] + arglist = ['--detached', self.volume.id] verifylist = [ ('attached', False), ('detached', True), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.reset_state.assert_called_with( - self.new_volume.id, attach_status='detached', state=None - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.reset_volume_status.assert_called_with( + self.volume, attach_status='detached' + ) + def test_volume_set_bootable(self): arglist = [ '--bootable', - self.new_volume.id, + self.volume.id, ] verifylist = [ ('bootable', True), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.cmd.take_action(parsed_args) - self.volumes_mock.set_bootable.assert_called_with( - self.new_volume.id, verifylist[0][1] + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False ) + self.volume_client.set_volume_bootable_status(self.volume, True) def test_volume_set_non_bootable(self): arglist = [ '--non-bootable', - self.new_volume.id, + self.volume.id, ] verifylist = [ ('bootable', False), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.cmd.take_action(parsed_args) - self.volumes_mock.set_bootable.assert_called_with( - self.new_volume.id, verifylist[0][1] + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False ) + self.volume_client.set_volume_bootable_status(self.volume, False) - def test_volume_set_readonly(self): - arglist = ['--read-only', self.new_volume.id] + def test_volume_set_read_only(self): + arglist = ['--read-only', self.volume.id] verifylist = [ ('read_only', True), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.update_readonly_flag.assert_called_once_with( - self.new_volume.id, True - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.set_volume_readonly.assert_called_with( + self.volume, True + ) + def test_volume_set_read_write(self): - arglist = ['--read-write', self.new_volume.id] + arglist = ['--read-write', self.volume.id] verifylist = [ ('read_only', False), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.update_readonly_flag.assert_called_once_with( - self.new_volume.id, False - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.set_volume_readonly.assert_called_with( + self.volume, False + ) + def test_volume_set_type(self): - arglist = ['--type', self.volume_type.id, self.new_volume.id] + arglist = ['--type', self.volume_type.id, self.volume.id] verifylist = [ ('retype_policy', None), ('type', self.volume_type.id), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.retype.assert_called_once_with( - self.new_volume.id, self.volume_type.id, 'never' - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.find_type.assert_called_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.retype_volume.assert_called_once_with( + self.volume, self.volume_type, 'never' + ) + def test_volume_set_type_with_policy(self): arglist = [ '--retype-policy', 'on-demand', '--type', self.volume_type.id, - self.new_volume.id, + self.volume.id, ] verifylist = [ ('retype_policy', 'on-demand'), ('type', self.volume_type.id), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.retype.assert_called_once_with( - self.new_volume.id, self.volume_type.id, 'on-demand' - ) self.assertIsNone(result) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.find_type.assert_called_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.retype_volume.assert_called_once_with( + self.volume, self.volume_type, 'on-demand' + ) + @mock.patch.object(volume.LOG, 'warning') def test_volume_set_with_only_retype_policy(self, mock_warning): - arglist = ['--retype-policy', 'on-demand', self.new_volume.id] + arglist = ['--retype-policy', 'on-demand', self.volume.id] verifylist = [ ('retype_policy', 'on-demand'), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - self.volumes_mock.retype.assert_not_called() + self.assertIsNone(result) + + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.find_type.assert_not_called() + self.volume_client.retype_volume.assert_not_called() mock_warning.assert_called_with( - "'--retype-policy' option will not work without '--type' option" + "'%s' option will not work without '--type' option", + '--retype-policy', ) - self.assertIsNone(result) class TestVolumeShow(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + + self.columns = ( + 'attachments', + 'availability_zone', + 'backup_id', + 'bootable', + 'cluster_name', + 'consistencygroup_id', + 'consumes_quota', + 'created_at', + 'description', + 'encrypted', + 'encryption_key_id', + 'group_id', + 'id', + 'multiattach', + 'name', + 'os-vol-host-attr:host', + 'os-vol-mig-status-attr:migstat', + 'os-vol-mig-status-attr:name_id', + 'os-vol-tenant-attr:tenant_id', + 'properties', + 'provider_id', + 'replication_status', + 'service_uuid', + 'shared_targets', + 'size', + 'snapshot_id', + 'source_volid', + 'status', + 'type', + 'updated_at', + 'user_id', + 'volume_image_metadata', + 'volume_type_id', + ) + self.data = ( + self.volume.attachments, + self.volume.availability_zone, + self.volume.backup_id, + self.volume.is_bootable, + self.volume.cluster_name, + self.volume.consistency_group_id, + self.volume.consumes_quota, + self.volume.created_at, + self.volume.description, + self.volume.is_encrypted, + self.volume.encryption_key_id, + self.volume.group_id, + self.volume.id, + self.volume.is_multiattach, + self.volume.name, + self.volume.host, + self.volume.migration_status, + self.volume.migration_id, + self.volume.project_id, + format_columns.DictColumn(self.volume.metadata), + self.volume.provider_id, + self.volume.replication_status, + self.volume.service_uuid, + self.volume.shared_targets, + self.volume.size, + self.volume.snapshot_id, + self.volume.source_volume_id, + self.volume.status, + self.volume.volume_type, + self.volume.updated_at, + self.volume.user_id, + self.volume.volume_image_metadata, + self.volume.volume_type_id, + ) - self._volume = volume_fakes.create_one_volume() - self.volumes_mock.get.return_value = self._volume - # Get the command object to test self.cmd = volume.ShowVolume(self.app, None) def test_volume_show(self): - arglist = [self._volume.id] - verifylist = [("volume", self._volume.id)] + arglist = [self.volume.id] + verifylist = [("volume", self.volume.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volumes_mock.get.assert_called_with(self._volume.id) - self.assertEqual( - tuple(sorted(self._volume.keys())), - columns, - ) - self.assertTupleEqual( - ( - self._volume.attachments, - self._volume.availability_zone, - self._volume.bootable, - self._volume.description, - self._volume.id, - self._volume.name, - format_columns.DictColumn(self._volume.metadata), - self._volume.size, - self._volume.snapshot_id, - self._volume.status, - self._volume.volume_type, - ), - data, + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False ) @@ -2073,61 +2047,61 @@ class TestVolumeUnset(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - - self.new_volume = volume_fakes.create_one_volume() - self.volumes_mock.get.return_value = self.new_volume - - # Get the command object to set property - self.cmd_set = volume.SetVolume(self.app, None) + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.volume_client.delete_volume_metadata.return_value = None + self.volume_client.delete_volume_image_metadata.return_value = None - # Get the command object to unset property - self.cmd_unset = volume.UnsetVolume(self.app, None) + self.cmd = volume.UnsetVolume(self.app, None) - def test_volume_unset_image_property(self): - # Arguments for setting image properties + def test_volume_unset_property(self): arglist = [ - '--image-property', - 'Alpha=a', - '--image-property', - 'Beta=b', - self.new_volume.id, + '--property', + 'a', + '--property', + 'c', + self.volume.id, ] verifylist = [ - ('image_properties', {'Alpha': 'a', 'Beta': 'b'}), - ('volume', self.new_volume.id), + ('properties', ['a', 'c']), + ('volume', self.volume.id), ] - parsed_args = self.check_parser(self.cmd_set, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns nothing - self.cmd_set.take_action(parsed_args) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - # Arguments for unsetting image properties - arglist_unset = [ + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.delete_volume_metadata.assert_called_once_with( + self.volume.id, keys=parsed_args.properties + ) + + def test_volume_unset_image_property(self): + arglist = [ '--image-property', 'Alpha', - self.new_volume.id, + self.volume.id, ] - verifylist_unset = [ + verifylist = [ ('image_properties', ['Alpha']), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] - parsed_args_unset = self.check_parser( - self.cmd_unset, arglist_unset, verifylist_unset - ) - # In base command class ShowOne in cliff, abstract method take_action() - # returns nothing - self.cmd_unset.take_action(parsed_args_unset) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.volumes_mock.delete_image_metadata.assert_called_with( - self.new_volume.id, parsed_args_unset.image_properties + self.volume_client.find_volume.assert_called_with( + self.volume.id, ignore_missing=False + ) + self.volume_client.delete_volume_image_metadata.assert_called_once_with( + self.volume.id, keys=parsed_args.image_properties ) def test_volume_unset_image_property_fail(self): - self.volumes_mock.delete_image_metadata.side_effect = ( + self.volume_client.delete_volume_image_metadata.side_effect = ( exceptions.CommandError() ) arglist = [ @@ -2135,27 +2109,28 @@ def test_volume_unset_image_property_fail(self): 'Alpha', '--property', 'Beta', - self.new_volume.id, + self.volume.id, ] verifylist = [ ('image_properties', ['Alpha']), ('properties', ['Beta']), - ('volume', self.new_volume.id), + ('volume', self.volume.id), ] - parsed_args = self.check_parser(self.cmd_unset, arglist, verifylist) + parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd_unset.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - 'One or more of the unset operations failed', str(e) - ) - self.volumes_mock.delete_image_metadata.assert_called_with( - self.new_volume.id, parsed_args.image_properties + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertEqual( + 'One or more of the unset operations failed', str(exc) + ) + self.volume_client.delete_volume_metadata.assert_called_once_with( + self.volume.id, keys=parsed_args.properties ) - self.volumes_mock.delete_metadata.assert_called_with( - self.new_volume.id, parsed_args.properties + self.volume_client.delete_volume_image_metadata.assert_called_once_with( + self.volume.id, keys=parsed_args.image_properties ) @@ -2175,7 +2150,7 @@ def setUp(self): total_count=2, total_size=self.volume_a.size + self.volume_b.size, ) - self.volume_sdk_client.summary.return_value = self.summary + self.volume_client.summary.return_value = self.summary # Get the command object to test self.cmd = volume.VolumeSummary(self.app, None) @@ -2192,7 +2167,7 @@ def test_volume_summary(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.summary.assert_called_once_with(True) + self.volume_client.summary.assert_called_once_with(True) self.assertEqual(self.columns, columns) @@ -2225,7 +2200,7 @@ def test_volume_summary_with_metadata(self): total_size=self.volume_a.size + self.volume_b.size, metadata=metadata, ) - self.volume_sdk_client.summary.return_value = self.summary + self.volume_client.summary.return_value = self.summary new_cols = copy.deepcopy(self.columns) new_cols.extend(['Metadata']) @@ -2240,7 +2215,7 @@ def test_volume_summary_with_metadata(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.summary.assert_called_once_with(True) + self.volume_client.summary.assert_called_once_with(True) self.assertEqual(new_cols, columns) @@ -2261,8 +2236,8 @@ def setUp(self): _snapshot.Snapshot, volume_id=self.volume.id, ) - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.find_snapshot.return_value = self.snapshot + self.volume_client.find_volume.return_value = self.volume + self.volume_client.find_snapshot.return_value = self.snapshot # Get the command object to test self.cmd = volume.VolumeRevertToSnapshot(self.app, None) @@ -2295,15 +2270,15 @@ def test_volume_revert_to_snapshot(self): self.cmd.take_action(parsed_args) - self.volume_sdk_client.revert_volume_to_snapshot.assert_called_once_with( + self.volume_client.revert_volume_to_snapshot.assert_called_once_with( self.volume, self.snapshot, ) - self.volume_sdk_client.find_volume.assert_called_with( + self.volume_client.find_volume.assert_called_with( self.volume.id, ignore_missing=False, ) - self.volume_sdk_client.find_snapshot.assert_called_with( + self.volume_client.find_snapshot.assert_called_with( self.snapshot.id, ignore_missing=False, ) @@ -2311,29 +2286,45 @@ def test_volume_revert_to_snapshot(self): class TestColumns(volume_fakes.TestVolume): def test_attachments_column_without_server_cache(self): - _volume = volume_fakes.create_one_volume() - server_id = _volume.attachments[0]['server_id'] - device = _volume.attachments[0]['device'] + vol = sdk_fakes.generate_fake_resource( + _volume.Volume, + attachments=[ + { + 'device': '/dev/' + uuid.uuid4().hex, + 'server_id': uuid.uuid4().hex, + }, + ], + ) + server_id = vol.attachments[0]['server_id'] + device = vol.attachments[0]['device'] - col = volume.AttachmentsColumn(_volume.attachments, {}) + col = volume.AttachmentsColumn(vol.attachments, {}) self.assertEqual( f'Attached to {server_id} on {device} ', col.human_readable(), ) - self.assertEqual(_volume.attachments, col.machine_readable()) + self.assertEqual(vol.attachments, col.machine_readable()) def test_attachments_column_with_server_cache(self): - _volume = volume_fakes.create_one_volume() + vol = sdk_fakes.generate_fake_resource( + _volume.Volume, + attachments=[ + { + 'device': '/dev/' + uuid.uuid4().hex, + 'server_id': uuid.uuid4().hex, + }, + ], + ) - server_id = _volume.attachments[0]['server_id'] - device = _volume.attachments[0]['device'] + server_id = vol.attachments[0]['server_id'] + device = vol.attachments[0]['device'] fake_server = mock.Mock() fake_server.name = 'fake-server-name' server_cache = {server_id: fake_server} - col = volume.AttachmentsColumn(_volume.attachments, server_cache) + col = volume.AttachmentsColumn(vol.attachments, server_cache) self.assertEqual( 'Attached to {} on {} '.format('fake-server-name', device), col.human_readable(), ) - self.assertEqual(_volume.attachments, col.machine_readable()) + self.assertEqual(vol.attachments, col.machine_readable()) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_attachment.py b/openstackclient/tests/unit/volume/v3/test_volume_attachment.py index b7838e034a..7fda8f3be5 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_attachment.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_attachment.py @@ -10,27 +10,26 @@ # License for the specific language governing permissions and limitations # under the License. +from openstack.block_storage.v3 import attachment as _attachment +from openstack.block_storage.v3 import volume as _volume +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from openstackclient.tests.unit.compute.v2 import fakes as compute_fakes -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes from openstackclient.volume.v3 import volume_attachment -class TestVolumeAttachment(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.projects_mock = self.app.client_manager.identity.projects - - -class TestVolumeAttachmentCreate(TestVolumeAttachment): - volume = volume_fakes.create_one_volume() +class TestVolumeAttachmentCreate(volume_fakes.TestVolume): + volume = sdk_fakes.generate_fake_resource(_volume.Volume) server = compute_fakes.create_one_server() - volume_attachment = volume_fakes.create_one_volume_attachment( - attrs={'instance': server.id, 'volume_id': volume.id}, + volume_attachment = sdk_fakes.generate_fake_resource( + _attachment.Attachment, + instance=server.id, + volume_id=volume.id, + connection_info={}, ) columns = ( @@ -57,9 +56,9 @@ class TestVolumeAttachmentCreate(TestVolumeAttachment): def setUp(self): super().setUp() - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.create_attachment.return_value = ( - self.volume_attachment.to_dict() + self.volume_client.find_volume.return_value = self.volume + self.volume_client.create_attachment.return_value = ( + self.volume_attachment ) self.compute_client.find_server.return_value = self.server @@ -89,13 +88,13 @@ def test_volume_attachment_create(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.volume.id, ignore_missing=False ) self.compute_client.find_server.assert_called_once_with( self.server.id, ignore_missing=False ) - self.volume_sdk_client.create_attachment.assert_called_once_with( + self.volume_client.create_attachment.assert_called_once_with( self.volume.id, connector={}, instance=self.server.id, @@ -156,13 +155,13 @@ def test_volume_attachment_create_with_connect(self): ] ) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.volume.id, ignore_missing=False ) self.compute_client.find_server.assert_called_once_with( self.server.id, ignore_missing=False ) - self.volume_sdk_client.create_attachment.assert_called_once_with( + self.volume_client.create_attachment.assert_called_once_with( self.volume.id, connector=connect_info, instance=self.server.id, @@ -239,13 +238,15 @@ def test_volume_attachment_create_with_connect_missing_arg(self): ) -class TestVolumeAttachmentDelete(TestVolumeAttachment): - volume_attachment = volume_fakes.create_one_volume_attachment() +class TestVolumeAttachmentDelete(volume_fakes.TestVolume): + volume_attachment = sdk_fakes.generate_fake_resource( + _attachment.Attachment + ) def setUp(self): super().setUp() - self.volume_sdk_client.delete_attachment.return_value = None + self.volume_client.delete_attachment.return_value = None self.cmd = volume_attachment.DeleteVolumeAttachment(self.app, None) @@ -262,7 +263,7 @@ def test_volume_attachment_delete(self): result = self.cmd.take_action(parsed_args) - self.volume_sdk_client.delete_attachment.assert_called_once_with( + self.volume_client.delete_attachment.assert_called_once_with( self.volume_attachment.id, ) self.assertIsNone(result) @@ -286,8 +287,10 @@ def test_volume_attachment_delete_pre_v327(self): ) -class TestVolumeAttachmentSet(TestVolumeAttachment): - volume_attachment = volume_fakes.create_one_volume_attachment() +class TestVolumeAttachmentSet(volume_fakes.TestVolume): + volume_attachment = sdk_fakes.generate_fake_resource( + _attachment.Attachment, connection_info={} + ) columns = ( 'ID', @@ -313,7 +316,7 @@ class TestVolumeAttachmentSet(TestVolumeAttachment): def setUp(self): super().setUp() - self.volume_sdk_client.update_attachment.return_value = ( + self.volume_client.update_attachment.return_value = ( self.volume_attachment ) @@ -364,7 +367,7 @@ def test_volume_attachment_set(self): ] ) - self.volume_sdk_client.update_attachment.assert_called_once_with( + self.volume_client.update_attachment.assert_called_once_with( self.volume_attachment.id, connector=connect_info, ) @@ -393,13 +396,15 @@ def test_volume_attachment_set_pre_v327(self): ) -class TestVolumeAttachmentComplete(TestVolumeAttachment): - volume_attachment = volume_fakes.create_one_volume_attachment() +class TestVolumeAttachmentComplete(volume_fakes.TestVolume): + volume_attachment = sdk_fakes.generate_fake_resource( + _attachment.Attachment + ) def setUp(self): super().setUp() - self.volume_sdk_client.complete_attachment.return_value = None + self.volume_client.complete_attachment.return_value = None self.cmd = volume_attachment.CompleteVolumeAttachment(self.app, None) @@ -416,7 +421,7 @@ def test_volume_attachment_complete(self): result = self.cmd.take_action(parsed_args) - self.volume_sdk_client.complete_attachment.assert_called_once_with( + self.volume_client.complete_attachment.assert_called_once_with( self.volume_attachment.id, ) self.assertIsNone(result) @@ -440,9 +445,12 @@ def test_volume_attachment_complete_pre_v344(self): ) -class TestVolumeAttachmentList(TestVolumeAttachment): - project = identity_fakes.FakeProject.create_one_project() - volume_attachments = volume_fakes.create_volume_attachments() +class TestVolumeAttachmentList(volume_fakes.TestVolume): + project = sdk_fakes.generate_fake_resource(_project.Project) + volume_attachments = [ + sdk_fakes.generate_fake_resource(_attachment.Attachment), + sdk_fakes.generate_fake_resource(_attachment.Attachment), + ] columns = ( 'ID', @@ -463,10 +471,8 @@ class TestVolumeAttachmentList(TestVolumeAttachment): def setUp(self): super().setUp() - self.projects_mock.get.return_value = self.project - self.volume_sdk_client.attachments.return_value = ( - self.volume_attachments - ) + self.identity_sdk_client.find_project.return_value = self.project + self.volume_client.attachments.return_value = self.volume_attachments self.cmd = volume_attachment.ListVolumeAttachment(self.app, None) @@ -486,7 +492,7 @@ def test_volume_attachment_list(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.attachments.assert_called_once_with( + self.volume_client.attachments.assert_called_once_with( search_opts={ 'all_tenants': False, 'project_id': None, @@ -495,6 +501,7 @@ def test_volume_attachment_list(self): }, marker=None, limit=None, + max_items=None, ) self.assertEqual(self.columns, columns) self.assertCountEqual(tuple(self.data), data) @@ -526,7 +533,7 @@ def test_volume_attachment_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.attachments.assert_called_once_with( + self.volume_client.attachments.assert_called_once_with( search_opts={ 'all_tenants': True, 'project_id': self.project.id, @@ -535,6 +542,7 @@ def test_volume_attachment_list_with_options(self): }, marker='volume-attachment-id', limit=2, + max_items=None, ) self.assertEqual(self.columns, columns) self.assertCountEqual(tuple(self.data), data) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_backend.py b/openstackclient/tests/unit/volume/v3/test_volume_backend.py new file mode 100644 index 0000000000..9c1e15a052 --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_volume_backend.py @@ -0,0 +1,213 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack.block_storage.v3 import capabilities as _capabilities +from openstack.block_storage.v3 import stats as _stats +from osc_lib.cli import format_columns + +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import volume_backend + + +def _create_fake_capability(): + """Create a fake volume backend capability. + + :param dict attrs: + A dictionary with all attributes of the Capabilities. + :return: + A FakeResource object with capability name and attrs. + """ + # Set default attribute + return _capabilities.Capabilities( + namespace="OS::Storage::Capabilities::fake", + vendor_name="OpenStack", + volume_backend_name="lvmdriver-1", + pool_name="pool", + driver_version="2.0.0", + storage_protocol="iSCSI", + display_name="Capabilities of Cinder LVM driver", + description="Blah, blah.", + visibility="public", + replication_targets=[], + properties={ + "compression": { + "title": "Compression", + "description": "Enables compression.", + "type": "boolean", + }, + "qos": { + "title": "QoS", + "description": "Enables QoS.", + "type": "boolean", + }, + "replication": { + "title": "Replication", + "description": "Enables replication.", + "type": "boolean", + }, + "thin_provisioning": { + "title": "Thin Provisioning", + "description": "Sets thin provisioning.", + "type": "boolean", + }, + }, + ) + + +def _create_fake_pool(): + return _stats.Pools( + name='host@lvmdriver-1#lvmdriver-1', + capabilities={ + 'storage_protocol': 'iSCSI', + 'thick_provisioning_support': False, + 'thin_provisioning_support': True, + 'total_volumes': 99, + 'total_capacity_gb': 1000.00, + 'allocated_capacity_gb': 100, + 'max_over_subscription_ratio': 200.0, + }, + ) + + +class TestShowVolumeCapability(volume_fakes.TestVolume): + """Test backend capability functionality.""" + + def setUp(self): + super().setUp() + + self.capability = _create_fake_capability() + self.volume_client.get_capabilities.return_value = self.capability + + self.cmd = volume_backend.ShowCapability(self.app, None) + + def test_capability_show(self): + self.set_volume_api_version('3.0') + + arglist = [ + 'fake', + ] + verifylist = [ + ('host', 'fake'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + expected_columns = [ + 'Title', + 'Key', + 'Type', + 'Description', + ] + + # confirming if all expected columns are present in the result. + self.assertEqual(expected_columns, columns) + + capabilities = [ + 'Compression', + 'Replication', + 'QoS', + 'Thin Provisioning', + ] + + # confirming if all expected values are present in the result. + for cap in data: + self.assertIn(cap[0], capabilities) + + # checking if proper call was made to get capabilities + self.volume_client.get_capabilities.assert_called_with( + 'fake', + ) + + +class TestListVolumePool(volume_fakes.TestVolume): + """Tests for volume backend pool listing.""" + + def setUp(self): + super().setUp() + + self.pool = _create_fake_pool() + self.volume_client.backend_pools.return_value = [self.pool] + + # Get the command object to test + self.cmd = volume_backend.ListPool(self.app, None) + + def test_pool_list(self): + self.set_volume_api_version('3.0') + + arglist = [] + verifylist = [] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + expected_columns = [ + 'Name', + ] + + # confirming if all expected columns are present in the result. + self.assertEqual(expected_columns, columns) + + datalist = ((self.pool.name,),) + + # confirming if all expected values are present in the result. + self.assertEqual(datalist, tuple(data)) + + # checking if proper call was made to list pools + self.volume_client.backend_pools.assert_called_with( + detailed=False, + ) + + # checking if long columns are present in output + self.assertNotIn("total_volumes", columns) + self.assertNotIn("storage_protocol", columns) + + def test_service_list_with_long_option(self): + self.set_volume_api_version('3.0') + + arglist = ['--long'] + verifylist = [('long', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + # In base command class Lister in cliff, abstract method take_action() + # returns a tuple containing the column names and an iterable + # containing the data to be listed. + columns, data = self.cmd.take_action(parsed_args) + + expected_columns = [ + 'Name', + 'Capabilities', + ] + + # confirming if all expected columns are present in the result. + self.assertEqual(expected_columns, columns) + + datalist = ( + ( + self.pool.name, + format_columns.DictColumn(self.pool.capabilities), + ), + ) + + # confirming if all expected values are present in the result. + self.assertEqual(datalist, tuple(data)) + + self.volume_client.backend_pools.assert_called_with( + detailed=True, + ) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_backup.py b/openstackclient/tests/unit/volume/v3/test_volume_backup.py index fb0a46508c..84d0e56c25 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_backup.py @@ -11,91 +11,84 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest.mock import call - +from unittest import mock + +from openstack.block_storage.v3 import backup as _backup +from openstack.block_storage.v3 import snapshot as _snapshot +from openstack.block_storage.v3 import volume as _volume +from openstack import exceptions as sdk_exceptions +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes from openstackclient.volume.v3 import volume_backup -class TestBackupLegacy(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.backups_mock = self.volume_client.backups - self.backups_mock.reset_mock() - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - self.snapshots_mock = self.volume_client.volume_snapshots - self.snapshots_mock.reset_mock() - self.restores_mock = self.volume_client.restores - self.restores_mock.reset_mock() - - class TestBackupCreate(volume_fakes.TestVolume): - volume = volume_fakes.create_one_volume() - snapshot = volume_fakes.create_one_snapshot() - new_backup = volume_fakes.create_one_backup( - attrs={'volume_id': volume.id, 'snapshot_id': snapshot.id} - ) - columns = ( 'id', 'name', 'volume_id', ) - data = ( - new_backup.id, - new_backup.name, - new_backup.volume_id, - ) def setUp(self): super().setUp() - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.find_snapshot.return_value = self.snapshot - self.volume_sdk_client.create_backup.return_value = self.new_backup + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) + self.volume_client.find_snapshot.return_value = self.snapshot + self.backup = sdk_fakes.generate_fake_resource( + _backup.Backup, + volume_id=self.volume.id, + snapshot_id=self.snapshot.id, + ) + self.volume_client.create_backup.return_value = self.backup + + self.data = ( + self.backup.id, + self.backup.name, + self.backup.volume_id, + ) - # Get the command object to test self.cmd = volume_backup.CreateVolumeBackup(self.app, None) def test_backup_create(self): arglist = [ "--name", - self.new_backup.name, + self.backup.name, "--description", - self.new_backup.description, + self.backup.description, "--container", - self.new_backup.container, + self.backup.container, "--force", "--incremental", "--snapshot", - self.new_backup.snapshot_id, - self.new_backup.volume_id, + self.backup.snapshot_id, + self.backup.volume_id, ] verifylist = [ - ("name", self.new_backup.name), - ("description", self.new_backup.description), - ("container", self.new_backup.container), + ("name", self.backup.name), + ("description", self.backup.description), + ("container", self.backup.container), ("force", True), ("incremental", True), - ("snapshot", self.new_backup.snapshot_id), - ("volume", self.new_backup.volume_id), + ("snapshot", self.backup.snapshot_id), + ("volume", self.backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( - volume_id=self.new_backup.volume_id, - container=self.new_backup.container, - name=self.new_backup.name, - description=self.new_backup.description, + self.volume_client.create_backup.assert_called_with( + volume_id=self.backup.volume_id, + container=self.backup.container, + name=self.backup.name, + description=self.backup.description, force=True, is_incremental=True, - snapshot_id=self.new_backup.snapshot_id, + snapshot_id=self.backup.snapshot_id, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -108,18 +101,18 @@ def test_backup_create_with_properties(self): "foo=bar", "--property", "wow=much-cool", - self.new_backup.volume_id, + self.backup.volume_id, ] verifylist = [ ("properties", {"foo": "bar", "wow": "much-cool"}), - ("volume", self.new_backup.volume_id), + ("volume", self.backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( - volume_id=self.new_backup.volume_id, + self.volume_client.create_backup.assert_called_with( + volume_id=self.backup.volume_id, container=None, name=None, description=None, @@ -138,11 +131,11 @@ def test_backup_create_with_properties_pre_v343(self): "foo=bar", "--property", "wow=much-cool", - self.new_backup.volume_id, + self.backup.volume_id, ] verifylist = [ ("properties", {"foo": "bar", "wow": "much-cool"}), - ("volume", self.new_backup.volume_id), + ("volume", self.backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -157,18 +150,18 @@ def test_backup_create_with_availability_zone(self): arglist = [ "--availability-zone", "my-az", - self.new_backup.volume_id, + self.backup.volume_id, ] verifylist = [ ("availability_zone", "my-az"), - ("volume", self.new_backup.volume_id), + ("volume", self.backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( - volume_id=self.new_backup.volume_id, + self.volume_client.create_backup.assert_called_with( + volume_id=self.backup.volume_id, container=None, name=None, description=None, @@ -185,11 +178,11 @@ def test_backup_create_with_availability_zone_pre_v351(self): arglist = [ "--availability-zone", "my-az", - self.new_backup.volume_id, + self.backup.volume_id, ] verifylist = [ ("availability_zone", "my-az"), - ("volume", self.new_backup.volume_id), + ("volume", self.backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -201,25 +194,25 @@ def test_backup_create_with_availability_zone_pre_v351(self): def test_backup_create_without_name(self): arglist = [ "--description", - self.new_backup.description, + self.backup.description, "--container", - self.new_backup.container, - self.new_backup.volume_id, + self.backup.container, + self.backup.volume_id, ] verifylist = [ - ("description", self.new_backup.description), - ("container", self.new_backup.container), - ("volume", self.new_backup.volume_id), + ("description", self.backup.description), + ("container", self.backup.container), + ("volume", self.backup.volume_id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( - volume_id=self.new_backup.volume_id, - container=self.new_backup.container, + self.volume_client.create_backup.assert_called_with( + volume_id=self.backup.volume_id, + container=self.backup.container, name=None, - description=self.new_backup.description, + description=self.backup.description, force=False, is_incremental=False, ) @@ -228,17 +221,13 @@ def test_backup_create_without_name(self): class TestBackupDelete(volume_fakes.TestVolume): - backups = volume_fakes.create_backups(count=2) - def setUp(self): super().setUp() - self.volume_sdk_client.find_backup = volume_fakes.get_backups( - self.backups - ) - self.volume_sdk_client.delete_backup.return_value = None + self.backups = list(sdk_fakes.generate_fake_resources(_backup.Backup)) + self.volume_client.find_backup.side_effect = self.backups + self.volume_client.delete_backup.return_value = None - # Get the command object to mock self.cmd = volume_backup.DeleteVolumeBackup(self.app, None) def test_backup_delete(self): @@ -248,7 +237,7 @@ def test_backup_delete(self): result = self.cmd.take_action(parsed_args) - self.volume_sdk_client.delete_backup.assert_called_with( + self.volume_client.delete_backup.assert_called_with( self.backups[0].id, ignore_missing=False, force=False ) self.assertIsNone(result) @@ -263,7 +252,7 @@ def test_backup_delete_with_force(self): result = self.cmd.take_action(parsed_args) - self.volume_sdk_client.delete_backup.assert_called_with( + self.volume_client.delete_backup.assert_called_with( self.backups[0].id, ignore_missing=False, force=True ) self.assertIsNone(result) @@ -281,8 +270,8 @@ def test_delete_multiple_backups(self): calls = [] for b in self.backups: - calls.append(call(b.id, ignore_missing=False, force=False)) - self.volume_sdk_client.delete_backup.assert_has_calls(calls) + calls.append(mock.call(b.id, ignore_missing=False, force=False)) + self.volume_client.delete_backup.assert_has_calls(calls) self.assertIsNone(result) def test_delete_multiple_backups_with_exception(self): @@ -297,7 +286,7 @@ def test_delete_multiple_backups_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) find_mock_result = [self.backups[0], exceptions.CommandError] - self.volume_sdk_client.find_backup.side_effect = find_mock_result + self.volume_client.find_backup.side_effect = find_mock_result try: self.cmd.take_action(parsed_args) @@ -305,15 +294,15 @@ def test_delete_multiple_backups_with_exception(self): except exceptions.CommandError as e: self.assertEqual('1 of 2 backups failed to delete.', str(e)) - self.volume_sdk_client.find_backup.assert_any_call( + self.volume_client.find_backup.assert_any_call( self.backups[0].id, ignore_missing=False ) - self.volume_sdk_client.find_backup.assert_any_call( + self.volume_client.find_backup.assert_any_call( 'unexist_backup', ignore_missing=False ) - self.assertEqual(2, self.volume_sdk_client.find_backup.call_count) - self.volume_sdk_client.delete_backup.assert_called_once_with( + self.assertEqual(2, self.volume_client.find_backup.call_count) + self.volume_client.delete_backup.assert_called_once_with( self.backups[0].id, ignore_missing=False, force=False, @@ -321,11 +310,6 @@ def test_delete_multiple_backups_with_exception(self): class TestBackupList(volume_fakes.TestVolume): - volume = volume_fakes.create_one_volume() - backups = volume_fakes.create_backups( - attrs={'volume_id': volume.name}, count=3 - ) - columns = ( 'ID', 'Name', @@ -335,51 +319,53 @@ class TestBackupList(volume_fakes.TestVolume): 'Incremental', 'Created At', ) - columns_long = columns + ( - 'Availability Zone', - 'Volume', - 'Container', - ) - - data = [] - for b in backups: - data.append( - ( - b.id, - b.name, - b.description, - b.status, - b.size, - b.is_incremental, - b.created_at, - ) - ) - data_long = [] - for b in backups: - data_long.append( - ( - b.id, - b.name, - b.description, - b.status, - b.size, - b.is_incremental, - b.created_at, - b.availability_zone, - volume_backup.VolumeIdColumn(b.volume_id), - b.container, - ) - ) + columns_long = (*columns, 'Availability Zone', 'Volume', 'Container') def setUp(self): super().setUp() - self.volume_sdk_client.volumes.return_value = [self.volume] - self.volume_sdk_client.backups.return_value = self.backups - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.find_backup.return_value = self.backups[0] + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.volume_client.volumes.return_value = [self.volume] + self.backups = list( + sdk_fakes.generate_fake_resources( + _backup.Backup, + attrs={'volume_id': self.volume.id}, + ) + ) + self.volume_client.backups.return_value = self.backups + self.volume_client.find_backup.return_value = self.backups[0] + + self.data = [] + for b in self.backups: + self.data.append( + ( + b.id, + b.name, + b.description, + b.status, + b.size, + b.is_incremental, + b.created_at, + ) + ) + self.data_long = [] + for b in self.backups: + self.data_long.append( + ( + b.id, + b.name, + b.description, + b.status, + b.size, + b.is_incremental, + b.created_at, + b.availability_zone, + volume_backup.VolumeIdColumn(b.volume_id), + b.container, + ) + ) - # Get the command to test self.cmd = volume_backup.ListVolumeBackup(self.app, None) def test_backup_list_without_options(self): @@ -392,25 +378,30 @@ def test_backup_list_without_options(self): ("marker", None), ("limit", None), ('all_projects', False), + ("project", None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.find_volume.assert_not_called() - self.volume_sdk_client.find_backup.assert_not_called() - self.volume_sdk_client.backups.assert_called_with( + self.volume_client.find_volume.assert_not_called() + self.volume_client.find_backup.assert_not_called() + self.volume_client.backups.assert_called_with( name=None, status=None, volume_id=None, all_tenants=False, marker=None, limit=None, + max_items=None, + project_id=None, ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) def test_backup_list_with_options(self): + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project arglist = [ "--long", "--name", @@ -424,6 +415,8 @@ def test_backup_list_with_options(self): "--all-projects", "--limit", "3", + "--project", + project.id, ] verifylist = [ ("long", True), @@ -433,65 +426,65 @@ def test_backup_list_with_options(self): ("marker", self.backups[0].id), ('all_projects', True), ("limit", 3), + ("project", project.id), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.volume.id, ignore_missing=False ) - self.volume_sdk_client.find_backup.assert_called_once_with( + self.volume_client.find_backup.assert_called_once_with( self.backups[0].id, ignore_missing=False ) - self.volume_sdk_client.backups.assert_called_with( + self.volume_client.backups.assert_called_with( name=self.backups[0].name, status="error", volume_id=self.volume.id, all_tenants=True, marker=self.backups[0].id, limit=3, + max_items=None, + project_id=project.id, ) self.assertEqual(self.columns_long, columns) self.assertCountEqual(self.data_long, list(data)) class TestBackupRestore(volume_fakes.TestVolume): - volume = volume_fakes.create_one_volume() - backup = volume_fakes.create_one_backup( - attrs={'volume_id': volume.id}, - ) - columns = ( "id", "volume_id", "volume_name", ) - data = ( - backup.id, - volume.id, - volume.name, - ) - def setUp(self): super().setUp() - self.volume_sdk_client.find_backup.return_value = self.backup - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.restore_backup.return_value = { + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.backup = sdk_fakes.generate_fake_resource( + _backup.Backup, volume_id=self.volume.id + ) + self.volume_client.find_backup.return_value = self.backup + self.volume_client.create_backup.return_value = self.backup + self.volume_client.restore_backup.return_value = { 'id': self.backup['id'], 'volume_id': self.volume['id'], 'volume_name': self.volume['name'], } - # Get the command object to mock + self.data = ( + self.backup.id, + self.volume.id, + self.volume.name, + ) + self.cmd = volume_backup.RestoreVolumeBackup(self.app, None) def test_backup_restore(self): - self.volume_sdk_client.find_volume.side_effect = ( - exceptions.CommandError() - ) + self.volume_client.find_volume.side_effect = exceptions.CommandError() arglist = [self.backup.id] verifylist = [ ("backup", self.backup.id), @@ -500,19 +493,15 @@ def test_backup_restore(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.restore_backup.assert_called_with( - self.backup.id, - volume_id=None, - name=None, + self.volume_client.restore_backup.assert_called_with( + self.backup.id, volume=None, name=None ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) def test_backup_restore_with_volume(self): - self.volume_sdk_client.find_volume.side_effect = ( - exceptions.CommandError() - ) + self.volume_client.find_volume.side_effect = exceptions.CommandError() arglist = [ self.backup.id, self.backup.volume_id, @@ -524,10 +513,8 @@ def test_backup_restore_with_volume(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.restore_backup.assert_called_with( - self.backup.id, - volume_id=None, - name=self.backup.volume_id, + self.volume_client.restore_backup.assert_called_with( + self.backup.id, volume=None, name=self.backup.volume_id ) self.assertEqual(self.columns, columns) @@ -547,10 +534,8 @@ def test_backup_restore_with_volume_force(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.restore_backup.assert_called_with( - self.backup.id, - volume_id=self.volume.id, - name=None, + self.volume_client.restore_backup.assert_called_with( + self.backup.id, volume=self.volume.id, name=None ) self.assertEqual(self.columns, columns) @@ -574,17 +559,15 @@ def test_backup_restore_with_volume_existing(self): ) -class TestBackupSet(TestBackupLegacy): - backup = volume_fakes.create_one_backup( - attrs={'metadata': {'wow': 'cool'}}, - ) - +class TestBackupSet(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.backups_mock.get.return_value = self.backup + self.backup = sdk_fakes.generate_fake_resource( + _backup.Backup, metadata={'wow': 'cool'} + ) + self.volume_client.find_backup.return_value = self.backup - # Get the command object to test self.cmd = volume_backup.SetVolumeBackup(self.app, None) def test_backup_set_name(self): @@ -601,14 +584,16 @@ def test_backup_set_name(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class ShowOne in cliff, abstract method take_action() - # returns nothing result = self.cmd.take_action(parsed_args) - self.backups_mock.update.assert_called_once_with( - self.backup.id, **{'name': 'new_name'} - ) self.assertIsNone(result) + self.volume_client.find_backup.assert_called_with( + self.backup.id, ignore_missing=False + ) + self.volume_client.update_backup.assert_called_once_with( + self.backup, name='new_name' + ) + def test_backup_set_name_pre_v39(self): self.set_volume_api_version('3.8') @@ -644,13 +629,14 @@ def test_backup_set_description(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - # Set expected values - kwargs = {'description': 'new_description'} - self.backups_mock.update.assert_called_once_with( - self.backup.id, **kwargs + self.volume_client.find_backup.assert_called_with( + self.backup.id, ignore_missing=False + ) + self.volume_client.update_backup.assert_called_once_with( + self.backup, description='new_description' ) - self.assertIsNone(result) def test_backup_set_description_pre_v39(self): self.set_volume_api_version('3.8') @@ -679,26 +665,34 @@ def test_backup_set_state(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.backups_mock.reset_state.assert_called_once_with( - self.backup.id, 'error' - ) self.assertIsNone(result) + self.volume_client.find_backup.assert_called_with( + self.backup.id, ignore_missing=False + ) + self.volume_client.reset_backup_status.assert_called_with( + self.backup, status='error' + ) + def test_backup_set_state_failed(self): - self.backups_mock.reset_state.side_effect = exceptions.CommandError() + self.volume_client.reset_backup_status.side_effect = ( + sdk_exceptions.NotFoundException('foo') + ) + arglist = ['--state', 'error', self.backup.id] verifylist = [('state', 'error'), ('backup', self.backup.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - 'One or more of the set operations failed', str(e) - ) - self.backups_mock.reset_state.assert_called_with( - self.backup.id, 'error' + exc = self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + self.assertEqual('One or more of the set operations failed', str(exc)) + + self.volume_client.find_backup.assert_called_with( + self.backup.id, ignore_missing=False + ) + self.volume_client.reset_backup_status.assert_called_with( + self.backup, status='error' ) def test_backup_set_no_property(self): @@ -715,15 +709,14 @@ def test_backup_set_no_property(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - # Set expected values - kwargs = { - 'metadata': {}, - } - self.backups_mock.update.assert_called_once_with( - self.backup.id, **kwargs + self.volume_client.find_backup.assert_called_with( + self.backup.id, ignore_missing=False + ) + self.volume_client.update_backup.assert_called_once_with( + self.backup, metadata={} ) - self.assertIsNone(result) def test_backup_set_no_property_pre_v343(self): self.set_volume_api_version('3.42') @@ -758,15 +751,14 @@ def test_backup_set_property(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - # Set expected values - kwargs = { - 'metadata': {'wow': 'cool', 'foo': 'bar'}, - } - self.backups_mock.update.assert_called_once_with( - self.backup.id, **kwargs + self.volume_client.find_backup.assert_called_with( + self.backup.id, ignore_missing=False + ) + self.volume_client.update_backup.assert_called_once_with( + self.backup, metadata={'wow': 'cool', 'foo': 'bar'} ) - self.assertIsNone(result) def test_backup_set_property_pre_v343(self): self.set_volume_api_version('3.42') @@ -788,17 +780,16 @@ def test_backup_set_property_pre_v343(self): self.assertIn("--os-volume-api-version 3.43 or greater", str(exc)) -class TestBackupUnset(TestBackupLegacy): - backup = volume_fakes.create_one_backup( - attrs={'metadata': {'foo': 'bar'}}, - ) - +class TestBackupUnset(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.backups_mock.get.return_value = self.backup + self.backup = sdk_fakes.generate_fake_resource( + _backup.Backup, metadata={'foo': 'bar', 'wow': 'cool'} + ) + self.volume_client.find_backup.return_value = self.backup + self.volume_client.delete_backup_metadata.return_value = None - # Get the command object to test self.cmd = volume_backup.UnsetVolumeBackup(self.app, None) def test_backup_unset_property(self): @@ -816,15 +807,14 @@ def test_backup_unset_property(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - # Set expected values - kwargs = { - 'metadata': {}, - } - self.backups_mock.update.assert_called_once_with( - self.backup.id, **kwargs + self.volume_client.find_backup.assert_called_with( + self.backup.id, ignore_missing=False + ) + self.volume_client.delete_backup_metadata.assert_called_once_with( + self.backup, keys=['wow'] ) - self.assertIsNone(result) def test_backup_unset_property_pre_v343(self): self.set_volume_api_version('3.42') @@ -847,8 +837,6 @@ def test_backup_unset_property_pre_v343(self): class TestBackupShow(volume_fakes.TestVolume): - backup = volume_fakes.create_one_backup() - columns = ( "availability_zone", "container", @@ -871,34 +859,36 @@ class TestBackupShow(volume_fakes.TestVolume): "user_id", "volume_id", ) - data = ( - backup.availability_zone, - backup.container, - backup.created_at, - backup.data_timestamp, - backup.description, - backup.encryption_key_id, - backup.fail_reason, - backup.has_dependent_backups, - backup.id, - backup.is_incremental, - backup.metadata, - backup.name, - backup.object_count, - backup.project_id, - backup.size, - backup.snapshot_id, - backup.status, - backup.updated_at, - backup.user_id, - backup.volume_id, - ) def setUp(self): super().setUp() - self.volume_sdk_client.find_backup.return_value = self.backup - # Get the command object to test + self.backup = sdk_fakes.generate_fake_resource(_backup.Backup) + self.volume_client.find_backup.return_value = self.backup + + self.data = ( + self.backup.availability_zone, + self.backup.container, + self.backup.created_at, + self.backup.data_timestamp, + self.backup.description, + self.backup.encryption_key_id, + self.backup.fail_reason, + self.backup.has_dependent_backups, + self.backup.id, + self.backup.is_incremental, + self.backup.metadata, + self.backup.name, + self.backup.object_count, + self.backup.project_id, + self.backup.size, + self.backup.snapshot_id, + self.backup.status, + self.backup.updated_at, + self.backup.user_id, + self.backup.volume_id, + ) + self.cmd = volume_backup.ShowVolumeBackup(self.app, None) def test_backup_show(self): @@ -907,7 +897,9 @@ def test_backup_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.find_backup.assert_called_with(self.backup.id) + self.volume_client.find_backup.assert_called_with( + self.backup.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_group.py b/openstackclient/tests/unit/volume/v3/test_volume_group.py index 3b64ad95c5..cdcfc7a23d 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_group.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_group.py @@ -12,6 +12,11 @@ from unittest import mock +from openstack.block_storage.v3 import group as _group +from openstack.block_storage.v3 import group_snapshot as _group_snapshot +from openstack.block_storage.v3 import group_type as _group_type +from openstack.block_storage.v3 import type as _type +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions from openstackclient.tests.unit import utils as tests_utils @@ -19,34 +24,18 @@ from openstackclient.volume.v3 import volume_group -class TestVolumeGroup(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.volume_groups_mock = self.volume_client.groups - self.volume_groups_mock.reset_mock() - - self.volume_group_types_mock = self.volume_client.group_types - self.volume_group_types_mock.reset_mock() - - self.volume_types_mock = self.volume_client.volume_types - self.volume_types_mock.reset_mock() - - self.volume_group_snapshots_mock = self.volume_client.group_snapshots - self.volume_group_snapshots_mock.reset_mock() - - -class TestVolumeGroupCreate(TestVolumeGroup): - fake_volume_type = volume_fakes.create_one_volume_type() - fake_volume_group_type = volume_fakes.create_one_volume_group_type() - fake_volume_group = volume_fakes.create_one_volume_group( - attrs={ - 'group_type': fake_volume_group_type.id, - 'volume_types': [fake_volume_type.id], - }, +class TestVolumeGroupCreate(volume_fakes.TestVolume): + fake_volume_type = sdk_fakes.generate_fake_resource(_type.Type) + fake_volume_group_type = sdk_fakes.generate_fake_resource( + _group_type.GroupType ) - fake_volume_group_snapshot = ( - volume_fakes.create_one_volume_group_snapshot() + fake_volume_group = sdk_fakes.generate_fake_resource( + _group.Group, + group_type=fake_volume_group_type.id, + volume_types=[fake_volume_type.id], + ) + fake_volume_group_snapshot = sdk_fakes.generate_fake_resource( + _group_snapshot.GroupSnapshot ) columns = ( @@ -79,18 +68,19 @@ class TestVolumeGroupCreate(TestVolumeGroup): def setUp(self): super().setUp() - self.volume_types_mock.get.return_value = self.fake_volume_type - self.volume_group_types_mock.get.return_value = ( + self.volume_client.find_type.return_value = self.fake_volume_type + self.volume_client.find_group_type.return_value = ( self.fake_volume_group_type ) - self.volume_groups_mock.create.return_value = self.fake_volume_group - self.volume_groups_mock.get.return_value = self.fake_volume_group - self.volume_groups_mock.create_from_src.return_value = ( + self.volume_client.create_group.return_value = self.fake_volume_group + self.volume_client.get_group.return_value = self.fake_volume_group + self.volume_client.create_group_from_source.return_value = ( self.fake_volume_group ) - self.volume_group_snapshots_mock.get.return_value = ( + self.volume_client.find_group_snapshot.return_value = ( self.fake_volume_group_snapshot ) + self.volume_client.find_group.return_value = self.fake_volume_group self.cmd = volume_group.CreateVolumeGroup(self.app, None) @@ -114,19 +104,22 @@ def test_volume_group_create(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_group_types_mock.get.assert_called_once_with( - self.fake_volume_group_type.id + self.volume_client.find_group_type.assert_called_once_with( + self.fake_volume_group_type.id, ignore_missing=False ) - self.volume_types_mock.get.assert_called_once_with( - self.fake_volume_type.id + self.volume_client.find_type.assert_called_once_with( + self.fake_volume_type.id, ignore_missing=False ) - self.volume_groups_mock.create.assert_called_once_with( - self.fake_volume_group_type.id, - self.fake_volume_type.id, - None, - None, + self.volume_client.create_group.assert_called_once_with( + group_type=self.fake_volume_group_type.id, + volume_types=[self.fake_volume_type.id], + name=None, + description=None, availability_zone=None, ) + self.volume_client.get_group.assert_called_once_with( + self.fake_volume_group.id + ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) @@ -149,17 +142,17 @@ def test_volume_group_create__legacy(self): with mock.patch.object(self.cmd.log, 'warning') as mock_warning: columns, data = self.cmd.take_action(parsed_args) - self.volume_group_types_mock.get.assert_called_once_with( - self.fake_volume_group_type.id + self.volume_client.find_group_type.assert_called_once_with( + self.fake_volume_group_type.id, ignore_missing=False ) - self.volume_types_mock.get.assert_called_once_with( - self.fake_volume_type.id + self.volume_client.find_type.assert_called_once_with( + self.fake_volume_type.id, ignore_missing=False ) - self.volume_groups_mock.create.assert_called_once_with( - self.fake_volume_group_type.id, - self.fake_volume_type.id, - None, - None, + self.volume_client.create_group.assert_called_once_with( + group_type=self.fake_volume_group_type.id, + volume_types=[self.fake_volume_type.id], + name=None, + description=None, availability_zone=None, ) self.assertEqual(self.columns, columns) @@ -218,17 +211,17 @@ def test_volume_group_create_with_options(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_group_types_mock.get.assert_called_once_with( - self.fake_volume_group_type.id + self.volume_client.find_group_type.assert_called_once_with( + self.fake_volume_group_type.id, ignore_missing=False ) - self.volume_types_mock.get.assert_called_once_with( - self.fake_volume_type.id + self.volume_client.find_type.assert_called_once_with( + self.fake_volume_type.id, ignore_missing=False ) - self.volume_groups_mock.create.assert_called_once_with( - self.fake_volume_group_type.id, - self.fake_volume_type.id, - 'foo', - 'hello, world', + self.volume_client.create_group.assert_called_once_with( + group_type=self.fake_volume_group_type.id, + volume_types=[self.fake_volume_type.id], + name='foo', + description='hello, world', availability_zone='bar', ) self.assertEqual(self.columns, columns) @@ -273,17 +266,17 @@ def test_volume_group_create_from_source_group(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_groups_mock.get.assert_has_calls( - [ - mock.call(self.fake_volume_group.id), - mock.call(self.fake_volume_group.id), - ] + self.volume_client.find_group.assert_called_once_with( + self.fake_volume_group.id, ignore_missing=False ) - self.volume_groups_mock.create_from_src.assert_called_once_with( - None, - self.fake_volume_group.id, - None, - None, + self.volume_client.create_group_from_source.assert_called_once_with( + group_snapshot_id=None, + source_group_id=self.fake_volume_group.id, + name=None, + description=None, + ) + self.volume_client.get_group.assert_called_once_with( + self.fake_volume_group.id ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) @@ -302,17 +295,17 @@ def test_volume_group_create_from_group_snapshot(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_group_snapshots_mock.get.assert_called_once_with( - self.fake_volume_group_snapshot.id + self.volume_client.find_group_snapshot.assert_called_once_with( + self.fake_volume_group_snapshot.id, ignore_missing=False ) - self.volume_groups_mock.get.assert_called_once_with( - self.fake_volume_group.id + self.volume_client.create_group_from_source.assert_called_once_with( + group_snapshot_id=self.fake_volume_group_snapshot.id, + source_group_id=None, + name=None, + description=None, ) - self.volume_groups_mock.create_from_src.assert_called_once_with( - self.fake_volume_group_snapshot.id, - None, - None, - None, + self.volume_client.get_group.assert_called_once_with( + self.fake_volume_group.id ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) @@ -337,8 +330,6 @@ def test_volume_group_create_from_src_pre_v314(self): ) def test_volume_group_create_from_src_source_group_group_snapshot(self): - self.set_volume_api_version('3.14') - arglist = [ '--source-group', self.fake_volume_group.id, @@ -363,14 +354,14 @@ def test_volume_group_create_from_src_source_group_group_snapshot(self): ) -class TestVolumeGroupDelete(TestVolumeGroup): - fake_volume_group = volume_fakes.create_one_volume_group() +class TestVolumeGroupDelete(volume_fakes.TestVolume): + fake_volume_group = sdk_fakes.generate_fake_resource(_group.Group) def setUp(self): super().setUp() - self.volume_groups_mock.get.return_value = self.fake_volume_group - self.volume_groups_mock.delete.return_value = None + self.volume_client.find_group.return_value = self.fake_volume_group + self.volume_client.delete_group.return_value = None self.cmd = volume_group.DeleteVolumeGroup(self.app, None) @@ -389,9 +380,11 @@ def test_volume_group_delete(self): result = self.cmd.take_action(parsed_args) - self.volume_groups_mock.delete.assert_called_once_with( - self.fake_volume_group.id, - delete_volumes=True, + self.volume_client.find_group.assert_called_once_with( + self.fake_volume_group.id, ignore_missing=False + ) + self.volume_client.delete_group.assert_called_once_with( + self.fake_volume_group, delete_volumes=True ) self.assertIsNone(result) @@ -415,8 +408,8 @@ def test_volume_group_delete_pre_v313(self): ) -class TestVolumeGroupSet(TestVolumeGroup): - fake_volume_group = volume_fakes.create_one_volume_group() +class TestVolumeGroupSet(volume_fakes.TestVolume): + fake_volume_group = sdk_fakes.generate_fake_resource(_group.Group) columns = ( 'ID', @@ -448,8 +441,8 @@ class TestVolumeGroupSet(TestVolumeGroup): def setUp(self): super().setUp() - self.volume_groups_mock.get.return_value = self.fake_volume_group - self.volume_groups_mock.update.return_value = self.fake_volume_group + self.volume_client.find_group.return_value = self.fake_volume_group + self.volume_client.update_group.return_value = self.fake_volume_group self.cmd = volume_group.SetVolumeGroup(self.app, None) @@ -472,8 +465,11 @@ def test_volume_group_set(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_groups_mock.update.assert_called_once_with( - self.fake_volume_group.id, + self.volume_client.find_group.assert_called_once_with( + self.fake_volume_group.id, ignore_missing=False + ) + self.volume_client.update_group.assert_called_once_with( + self.fake_volume_group, name='foo', description='hello, world', ) @@ -495,8 +491,8 @@ def test_volume_group_with_enable_replication_option(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_groups_mock.enable_replication.assert_called_once_with( - self.fake_volume_group.id + self.volume_client.enable_group_replication.assert_called_once_with( + self.fake_volume_group ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) @@ -546,8 +542,10 @@ def test_volume_group_with_enable_replication_option_pre_v338(self): ) -class TestVolumeGroupList(TestVolumeGroup): - fake_volume_groups = volume_fakes.create_volume_groups() +class TestVolumeGroupList(volume_fakes.TestVolume): + fake_volume_groups = list( + sdk_fakes.generate_fake_resources(_group.Group, count=2) + ) columns = ( 'ID', @@ -566,7 +564,7 @@ class TestVolumeGroupList(TestVolumeGroup): def setUp(self): super().setUp() - self.volume_groups_mock.list.return_value = self.fake_volume_groups + self.volume_client.groups.return_value = self.fake_volume_groups self.cmd = volume_group.ListVolumeGroup(self.app, None) @@ -583,10 +581,8 @@ def test_volume_group_list(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_groups_mock.list.assert_called_once_with( - search_opts={ - 'all_tenants': True, - }, + self.volume_client.groups.assert_called_once_with( + all_projects=True, ) self.assertEqual(self.columns, columns) self.assertCountEqual(tuple(self.data), data) @@ -610,14 +606,14 @@ def test_volume_group_list_pre_v313(self): ) -class TestVolumeGroupFailover(TestVolumeGroup): - fake_volume_group = volume_fakes.create_one_volume_group() +class TestVolumeGroupFailover(volume_fakes.TestVolume): + fake_volume_group = sdk_fakes.generate_fake_resource(_group.Group) def setUp(self): super().setUp() - self.volume_groups_mock.get.return_value = self.fake_volume_group - self.volume_groups_mock.failover_replication.return_value = None + self.volume_client.find_group.return_value = self.fake_volume_group + self.volume_client.failover_group_replication.return_value = None self.cmd = volume_group.FailoverVolumeGroup(self.app, None) @@ -639,9 +635,12 @@ def test_volume_group_failover(self): result = self.cmd.take_action(parsed_args) - self.volume_groups_mock.failover_replication.assert_called_once_with( - self.fake_volume_group.id, - allow_attached_volume=True, + self.volume_client.find_group.assert_called_once_with( + self.fake_volume_group.id, ignore_missing=False + ) + self.volume_client.failover_group_replication.assert_called_once_with( + self.fake_volume_group, + allowed_attached_volume=True, secondary_backend_id='foo', ) self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py b/openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py index 23ba4ec97b..4b605bd77c 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_group_snapshot.py @@ -45,11 +45,11 @@ class TestVolumeGroupSnapshotCreate(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volume_sdk_client.find_group.return_value = self.fake_volume_group - self.volume_sdk_client.create_group_snapshot.return_value = ( + self.volume_client.find_group.return_value = self.fake_volume_group + self.volume_client.create_group_snapshot.return_value = ( self.fake_volume_group_snapshot ) - self.volume_sdk_client.find_group_snapshot.return_value = ( + self.volume_client.find_group_snapshot.return_value = ( self.fake_volume_group_snapshot ) @@ -72,12 +72,12 @@ def test_volume_group_snapshot_create(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.find_group.assert_called_once_with( + self.volume_client.find_group.assert_called_once_with( self.fake_volume_group.id, ignore_missing=False, details=False, ) - self.volume_sdk_client.create_group_snapshot.assert_called_once_with( + self.volume_client.create_group_snapshot.assert_called_once_with( group_id=self.fake_volume_group.id, name=None, description=None, @@ -104,12 +104,12 @@ def test_volume_group_snapshot_create_with_options(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.find_group.assert_called_once_with( + self.volume_client.find_group.assert_called_once_with( self.fake_volume_group.id, ignore_missing=False, details=False, ) - self.volume_sdk_client.create_group_snapshot.assert_called_once_with( + self.volume_client.create_group_snapshot.assert_called_once_with( group_id=self.fake_volume_group.id, name='foo', description='hello, world', @@ -149,10 +149,10 @@ class TestVolumeGroupSnapshotDelete(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volume_sdk_client.find_group_snapshot.return_value = ( + self.volume_client.find_group_snapshot.return_value = ( self.fake_volume_group_snapshot ) - self.volume_sdk_client.delete_group_snapshot.return_value = None + self.volume_client.delete_group_snapshot.return_value = None self.cmd = volume_group_snapshot.DeleteVolumeGroupSnapshot( self.app, None @@ -171,7 +171,7 @@ def test_volume_group_snapshot_delete(self): result = self.cmd.take_action(parsed_args) - self.volume_sdk_client.delete_group_snapshot.assert_called_once_with( + self.volume_client.delete_group_snapshot.assert_called_once_with( self.fake_volume_group_snapshot.id, ) self.assertIsNone(result) @@ -223,7 +223,7 @@ class TestVolumeGroupSnapshotList(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volume_sdk_client.group_snapshots.return_value = ( + self.volume_client.group_snapshots.return_value = ( self.fake_volume_group_snapshots ) @@ -244,7 +244,7 @@ def test_volume_group_snapshot_list(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.group_snapshots.assert_called_once_with( + self.volume_client.group_snapshots.assert_called_once_with( all_projects=True, ) self.assertEqual(self.columns, columns) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_group_type.py b/openstackclient/tests/unit/volume/v3/test_volume_group_type.py index 21db03d77c..e87dc65307 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_group_type.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_group_type.py @@ -10,8 +10,8 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest import mock - +from openstack.block_storage.v3 import group_type as _group_type +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions @@ -22,13 +22,13 @@ class TestVolumeGroupType(volume_fakes.TestVolume): def setUp(self): super().setUp() - - self.volume_group_types_mock = self.volume_client.group_types - self.volume_group_types_mock.reset_mock() + self.set_volume_api_version('3.11') class TestVolumeGroupTypeCreate(TestVolumeGroupType): - fake_volume_group_type = volume_fakes.create_one_volume_group_type() + fake_volume_group_type = sdk_fakes.generate_fake_resource( + _group_type.GroupType + ) columns = ( 'ID', @@ -48,15 +48,13 @@ class TestVolumeGroupTypeCreate(TestVolumeGroupType): def setUp(self): super().setUp() - self.volume_group_types_mock.create.return_value = ( + self.volume_client.create_group_type.return_value = ( self.fake_volume_group_type ) self.cmd = volume_group_type.CreateVolumeGroupType(self.app, None) def test_volume_group_type_create(self): - self.set_volume_api_version('3.11') - arglist = [ self.fake_volume_group_type.name, ] @@ -69,15 +67,15 @@ def test_volume_group_type_create(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_group_types_mock.create.assert_called_once_with( - self.fake_volume_group_type.name, None, True + self.volume_client.create_group_type.assert_called_once_with( + name=self.fake_volume_group_type.name, + description=None, + is_public=True, ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) def test_volume_group_type_create_with_options(self): - self.set_volume_api_version('3.11') - arglist = [ self.fake_volume_group_type.name, '--description', @@ -93,8 +91,10 @@ def test_volume_group_type_create_with_options(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_group_types_mock.create.assert_called_once_with( - self.fake_volume_group_type.name, 'foo', False + self.volume_client.create_group_type.assert_called_once_with( + name=self.fake_volume_group_type.name, + description='foo', + is_public=False, ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) @@ -121,21 +121,21 @@ def test_volume_group_type_create_pre_v311(self): class TestVolumeGroupTypeDelete(TestVolumeGroupType): - fake_volume_group_type = volume_fakes.create_one_volume_group_type() + fake_volume_group_type = sdk_fakes.generate_fake_resource( + _group_type.GroupType + ) def setUp(self): super().setUp() - self.volume_group_types_mock.get.return_value = ( + self.volume_client.find_group_type.return_value = ( self.fake_volume_group_type ) - self.volume_group_types_mock.delete.return_value = None + self.volume_client.delete_group_type.return_value = None self.cmd = volume_group_type.DeleteVolumeGroupType(self.app, None) def test_volume_group_type_delete(self): - self.set_volume_api_version('3.11') - arglist = [ self.fake_volume_group_type.id, ] @@ -146,8 +146,12 @@ def test_volume_group_type_delete(self): result = self.cmd.take_action(parsed_args) - self.volume_group_types_mock.delete.assert_called_once_with( + self.volume_client.find_group_type.assert_called_once_with( self.fake_volume_group_type.id, + ignore_missing=False, + ) + self.volume_client.delete_group_type.assert_called_once_with( + self.fake_volume_group_type, ) self.assertIsNone(result) @@ -171,12 +175,8 @@ def test_volume_group_type_delete_pre_v311(self): class TestVolumeGroupTypeSet(TestVolumeGroupType): - fake_volume_group_type = volume_fakes.create_one_volume_group_type( - methods={ - 'get_keys': {'foo': 'bar'}, - 'set_keys': None, - 'unset_keys': None, - }, + fake_volume_group_type = sdk_fakes.generate_fake_resource( + _group_type.GroupType, group_specs={'foo': 'bar'} ) columns = ( @@ -197,20 +197,16 @@ class TestVolumeGroupTypeSet(TestVolumeGroupType): def setUp(self): super().setUp() - self.volume_group_types_mock.get.return_value = ( + self.volume_client.find_group_type.return_value = ( self.fake_volume_group_type ) - self.volume_group_types_mock.update.return_value = ( + self.volume_client.update_group_type.return_value = ( self.fake_volume_group_type ) self.cmd = volume_group_type.SetVolumeGroupType(self.app, None) def test_volume_group_type_set(self): - self.set_volume_api_version('3.11') - - self.fake_volume_group_type.set_keys.return_value = None - arglist = [ self.fake_volume_group_type.id, '--name', @@ -233,21 +229,20 @@ def test_volume_group_type_set(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_group_types_mock.update.assert_called_once_with( + self.volume_client.update_group_type.assert_called_once_with( self.fake_volume_group_type.id, name='foo', description='hello, world', is_public=True, ) - self.fake_volume_group_type.set_keys.assert_called_once_with( + self.volume_client.create_group_type_group_specs.assert_called_once_with( + self.fake_volume_group_type.id, {'fizz': 'buzz'}, ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) def test_volume_group_type_with_no_property_option(self): - self.set_volume_api_version('3.11') - arglist = [ self.fake_volume_group_type.id, '--no-property', @@ -266,12 +261,13 @@ def test_volume_group_type_with_no_property_option(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_group_types_mock.get.assert_called_once_with( - self.fake_volume_group_type.id + self.volume_client.delete_group_type_group_specs_property.assert_called_once_with( + self.fake_volume_group_type.id, + 'foo', ) - self.fake_volume_group_type.get_keys.assert_called_once_with() - self.fake_volume_group_type.unset_keys.assert_called_once_with( - {'foo': 'bar'}.keys() + self.volume_client.create_group_type_group_specs.assert_called_once_with( + self.fake_volume_group_type.id, + {'fizz': 'buzz'}, ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) @@ -305,8 +301,8 @@ def test_volume_group_type_set_pre_v311(self): class TestVolumeGroupTypeUnset(TestVolumeGroupType): - fake_volume_group_type = volume_fakes.create_one_volume_group_type( - methods={'unset_keys': None}, + fake_volume_group_type = sdk_fakes.generate_fake_resource( + _group_type.GroupType ) columns = ( @@ -327,15 +323,13 @@ class TestVolumeGroupTypeUnset(TestVolumeGroupType): def setUp(self): super().setUp() - self.volume_group_types_mock.get.return_value = ( + self.volume_client.find_group_type.return_value = ( self.fake_volume_group_type ) self.cmd = volume_group_type.UnsetVolumeGroupType(self.app, None) def test_volume_group_type_unset(self): - self.set_volume_api_version('3.11') - arglist = [ self.fake_volume_group_type.id, '--property', @@ -349,14 +343,13 @@ def test_volume_group_type_unset(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_group_types_mock.get.assert_has_calls( - [ - mock.call(self.fake_volume_group_type.id), - mock.call(self.fake_volume_group_type.id), - ] + self.volume_client.delete_group_type_group_specs_property.assert_called_once_with( + self.fake_volume_group_type.id, + 'fizz', ) - self.fake_volume_group_type.unset_keys.assert_called_once_with( - ['fizz'] + self.volume_client.find_group_type.assert_called_once_with( + self.fake_volume_group_type.id, + ignore_missing=False, ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) @@ -384,20 +377,22 @@ def test_volume_group_type_unset_pre_v311(self): class TestVolumeGroupTypeList(TestVolumeGroupType): - fake_volume_group_types = volume_fakes.create_volume_group_types() + fake_volume_group_types = list( + sdk_fakes.generate_fake_resources(_group_type.GroupType, count=2) + ) columns = ( 'ID', 'Name', 'Is Public', - 'Properties', + 'Group Specs', ) data = [ ( fake_volume_group_type.id, fake_volume_group_type.name, fake_volume_group_type.is_public, - fake_volume_group_type.group_specs, + format_columns.DictColumn(fake_volume_group_type.group_specs), ) for fake_volume_group_type in fake_volume_group_types ] @@ -405,18 +400,16 @@ class TestVolumeGroupTypeList(TestVolumeGroupType): def setUp(self): super().setUp() - self.volume_group_types_mock.list.return_value = ( + self.volume_client.group_types.return_value = ( self.fake_volume_group_types ) - self.volume_group_types_mock.default.return_value = ( + self.volume_client.get_group_type.return_value = ( self.fake_volume_group_types[0] ) self.cmd = volume_group_type.ListVolumeGroupType(self.app, None) def test_volume_group_type_list(self): - self.set_volume_api_version('3.11') - arglist = [] verifylist = [ ('show_default', False), @@ -425,13 +418,11 @@ def test_volume_group_type_list(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_group_types_mock.list.assert_called_once_with() + self.volume_client.group_types.assert_called_once_with() self.assertEqual(self.columns, columns) self.assertCountEqual(tuple(self.data), data) def test_volume_group_type_list_with_default_option(self): - self.set_volume_api_version('3.11') - arglist = [ '--default', ] @@ -442,9 +433,12 @@ def test_volume_group_type_list_with_default_option(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_group_types_mock.default.assert_called_once_with() + self.volume_client.get_group_type.assert_called_once_with('default') self.assertEqual(self.columns, columns) - self.assertCountEqual(tuple([self.data[0]]), data) + self.assertCountEqual( + tuple([self.data[0]]), + data, + ) def test_volume_group_type_list_pre_v311(self): self.set_volume_api_version('3.10') @@ -459,3 +453,69 @@ def test_volume_group_type_list_pre_v311(self): self.assertIn( '--os-volume-api-version 3.11 or greater is required', str(exc) ) + + +class TestVolumeGroupTypeShow(TestVolumeGroupType): + fake_volume_group_type = sdk_fakes.generate_fake_resource( + _group_type.GroupType + ) + + columns = ( + 'ID', + 'Name', + 'Description', + 'Is Public', + 'Properties', + ) + data = ( + fake_volume_group_type.id, + fake_volume_group_type.name, + fake_volume_group_type.description, + fake_volume_group_type.is_public, + format_columns.DictColumn(fake_volume_group_type.group_specs), + ) + + def setUp(self): + super().setUp() + + self.volume_client.find_group_type.return_value = ( + self.fake_volume_group_type + ) + + self.cmd = volume_group_type.ShowVolumeGroupType(self.app, None) + + def test_volume_group_type_show(self): + arglist = [ + self.fake_volume_group_type.id, + ] + verifylist = [ + ('group_type', self.fake_volume_group_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.find_group_type.assert_called_once_with( + self.fake_volume_group_type.id, + ignore_missing=False, + ) + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + def test_volume_group_type_show_pre_v311(self): + self.set_volume_api_version('3.10') + + arglist = [ + self.fake_volume_group_type.id, + ] + verifylist = [ + ('group_type', self.fake_volume_group_type.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + self.assertIn( + '--os-volume-api-version 3.11 or greater is required', str(exc) + ) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_host.py b/openstackclient/tests/unit/volume/v3/test_volume_host.py new file mode 100644 index 0000000000..fc6c33041f --- /dev/null +++ b/openstackclient/tests/unit/volume/v3/test_volume_host.py @@ -0,0 +1,85 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +from openstack.block_storage.v3 import service as _service +from openstack.test import fakes as sdk_fakes + +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes +from openstackclient.volume.v3 import volume_host + + +class TestVolumeHostSet(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.cmd = volume_host.SetVolumeHost(self.app, None) + + def test_volume_host_set_nothing(self): + arglist = [self.service.host] + verifylist = [('host', self.service.host)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.freeze_service.assert_not_called() + self.volume_client.thaw_service.assert_not_called() + self.assertIsNone(result) + + def test_volume_host_set_enable(self): + arglist = ['--enable', self.service.host] + verifylist = [('enable', True), ('host', self.service.host)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.thaw_service.assert_called_once_with( + _service.Service(host=self.service.host) + ) + self.volume_client.freeze_service.assert_not_called() + self.assertIsNone(result) + + def test_volume_host_set_disable(self): + arglist = ['--disable', self.service.host] + verifylist = [('disable', True), ('host', self.service.host)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.freeze_service.assert_called_once_with( + _service.Service(host=self.service.host) + ) + self.volume_client.thaw_service.assert_not_called() + self.assertIsNone(result) + + +class TestVolumeHostFailover(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + self.service = sdk_fakes.generate_fake_resource(_service.Service) + self.cmd = volume_host.FailoverVolumeHost(self.app, None) + + def test_volume_host_failover(self): + arglist = ['--volume-backend', 'backend_test', self.service.host] + verifylist = [ + ('volume_backend', 'backend_test'), + ('host', self.service.host), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.volume_client.failover_service.assert_called_once_with( + _service.Service(host=self.service.host), + backend_id='backend_test', + ) + self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_message.py b/openstackclient/tests/unit/volume/v3/test_volume_message.py index 4b4f6e41df..0f186abc19 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_message.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_message.py @@ -12,34 +12,24 @@ from unittest.mock import call +from openstack.block_storage.v3 import message as _message +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes from openstackclient.volume.v3 import volume_message -class TestVolumeMessage(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() - - self.volume_messages_mock = self.volume_client.messages - self.volume_messages_mock.reset_mock() - - -class TestVolumeMessageDelete(TestVolumeMessage): - fake_messages = volume_fakes.create_volume_messages(count=2) +class TestVolumeMessageDelete(volume_fakes.TestVolume): + fake_messages = list( + sdk_fakes.generate_fake_resources(_message.Message, count=2) + ) def setUp(self): super().setUp() - self.volume_messages_mock.get = volume_fakes.get_volume_messages( - self.fake_messages, - ) - self.volume_messages_mock.delete.return_value = None + self.volume_client.delete_message.return_value = None # Get the command object to mock self.cmd = volume_message.DeleteMessage(self.app, None) @@ -57,8 +47,8 @@ def test_message_delete(self): result = self.cmd.take_action(parsed_args) - self.volume_messages_mock.delete.assert_called_with( - self.fake_messages[0].id + self.volume_client.delete_message.assert_called_with( + self.fake_messages[0].id, ignore_missing=False ) self.assertIsNone(result) @@ -78,8 +68,8 @@ def test_message_delete_multiple_messages(self): calls = [] for m in self.fake_messages: - calls.append(call(m.id)) - self.volume_messages_mock.delete.assert_has_calls(calls) + calls.append(call(m.id, ignore_missing=False)) + self.volume_client.delete_message.assert_has_calls(calls) self.assertIsNone(result) def test_message_delete_multiple_messages_with_exception(self): @@ -95,8 +85,8 @@ def test_message_delete_multiple_messages_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.volume_messages_mock.delete.side_effect = [ - self.fake_messages[0], + self.volume_client.delete_message.side_effect = [ + None, exceptions.CommandError, ] @@ -105,12 +95,14 @@ def test_message_delete_multiple_messages_with_exception(self): ) self.assertEqual('Failed to delete 1 of 2 messages.', str(exc)) - self.volume_messages_mock.delete.assert_any_call( - self.fake_messages[0].id + self.volume_client.delete_message.assert_any_call( + self.fake_messages[0].id, ignore_missing=False + ) + self.volume_client.delete_message.assert_any_call( + 'invalid_message', ignore_missing=False ) - self.volume_messages_mock.delete.assert_any_call('invalid_message') - self.assertEqual(2, self.volume_messages_mock.delete.call_count) + self.assertEqual(2, self.volume_client.delete_message.call_count) def test_message_delete_pre_v33(self): self.set_volume_api_version('3.2') @@ -131,9 +123,10 @@ def test_message_delete_pre_v33(self): ) -class TestVolumeMessageList(TestVolumeMessage): - fake_project = identity_fakes.FakeProject.create_one_project() - fake_messages = volume_fakes.create_volume_messages(count=3) +class TestVolumeMessageList(volume_fakes.TestVolume): + fake_messages = list( + sdk_fakes.generate_fake_resources(_message.Message, count=3) + ) columns = ( 'ID', @@ -165,8 +158,9 @@ class TestVolumeMessageList(TestVolumeMessage): def setUp(self): super().setUp() - self.projects_mock.get.return_value = self.fake_project - self.volume_messages_mock.list.return_value = self.fake_messages + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project + self.volume_client.messages.return_value = self.fake_messages # Get the command to test self.cmd = volume_message.ListMessages(self.app, None) @@ -183,11 +177,8 @@ def test_message_list(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'project_id': None, - } - self.volume_messages_mock.list.assert_called_with( - search_opts=search_opts, + self.volume_client.messages.assert_called_once_with( + project_id=None, marker=None, limit=None, ) @@ -199,14 +190,14 @@ def test_message_list_with_options(self): arglist = [ '--project', - self.fake_project.name, + self.project.name, '--marker', self.fake_messages[0].id, '--limit', '3', ] verifylist = [ - ('project', self.fake_project.name), + ('project', self.project.name), ('marker', self.fake_messages[0].id), ('limit', 3), ] @@ -214,11 +205,8 @@ def test_message_list_with_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - search_opts = { - 'project_id': self.fake_project.id, - } - self.volume_messages_mock.list.assert_called_with( - search_opts=search_opts, + self.volume_client.messages.assert_called_once_with( + project_id=self.project.id, marker=self.fake_messages[0].id, limit=3, ) @@ -245,19 +233,19 @@ def test_message_list_pre_v33(self): ) -class TestVolumeMessageShow(TestVolumeMessage): - fake_message = volume_fakes.create_one_volume_message() +class TestVolumeMessageShow(volume_fakes.TestVolume): + fake_message = sdk_fakes.generate_fake_resource(_message.Message) columns = ( - 'created_at', - 'event_id', - 'guaranteed_until', - 'id', - 'message_level', - 'request_id', - 'resource_type', - 'resource_uuid', - 'user_message', + 'Created At', + 'Event ID', + 'Guaranteed Until', + 'ID', + 'Message Level', + 'Request ID', + 'Resource Type', + 'Resource UUID', + 'User Message', ) data = ( fake_message.created_at, @@ -274,7 +262,7 @@ class TestVolumeMessageShow(TestVolumeMessage): def setUp(self): super().setUp() - self.volume_messages_mock.get.return_value = self.fake_message + self.volume_client.get_message.return_value = self.fake_message # Get the command object to test self.cmd = volume_message.ShowMessage(self.app, None) @@ -286,7 +274,9 @@ def test_message_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_messages_mock.get.assert_called_with(self.fake_message.id) + self.volume_client.get_message.assert_called_once_with( + self.fake_message.id + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_snapshot.py b/openstackclient/tests/unit/volume/v3/test_volume_snapshot.py index 4c990d840a..2200b002f3 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_snapshot.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_snapshot.py @@ -13,34 +13,238 @@ from unittest import mock +from openstack.block_storage.v3 import snapshot as _snapshot +from openstack.block_storage.v3 import volume as _volume +from openstack import exceptions as sdk_exceptions +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes +from osc_lib.cli import format_columns from osc_lib import exceptions -from osc_lib import utils -from openstackclient.tests.unit.volume.v2 import fakes as volume_fakes -from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes_v3 +from openstackclient.tests.unit import utils as test_utils +from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes from openstackclient.volume.v3 import volume_snapshot -class TestVolumeSnapshot(volume_fakes_v3.TestVolume): +class TestVolumeSnapshotCreate(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.snapshots_mock = self.volume_client.volume_snapshots - self.snapshots_mock.reset_mock() + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_client.find_volume.return_value = self.volume + self.snapshot = sdk_fakes.generate_fake_resource( + _snapshot.Snapshot, volume_id=self.volume.id + ) + self.volume_client.create_snapshot.return_value = self.snapshot + self.volume_client.manage_snapshot.return_value = self.snapshot + + self.columns = ( + 'created_at', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'status', + 'volume_id', + ) + self.data = ( + self.snapshot.created_at, + self.snapshot.description, + self.snapshot.id, + self.snapshot.name, + format_columns.DictColumn(self.snapshot.metadata), + self.snapshot.size, + self.snapshot.status, + self.snapshot.volume_id, + ) + + self.cmd = volume_snapshot.CreateVolumeSnapshot(self.app, None) + + def test_snapshot_create(self): + arglist = [ + "--volume", + self.snapshot.volume_id, + "--description", + self.snapshot.description, + "--force", + '--property', + 'Alpha=a', + '--property', + 'Beta=b', + self.snapshot.name, + ] + verifylist = [ + ("volume", self.snapshot.volume_id), + ("description", self.snapshot.description), + ("force", True), + ('properties', {'Alpha': 'a', 'Beta': 'b'}), + ("snapshot_name", self.snapshot.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.find_volume.assert_called_once_with( + self.snapshot.volume_id, ignore_missing=False + ) + self.volume_client.create_snapshot.assert_called_with( + volume_id=self.snapshot.volume_id, + force=True, + name=self.snapshot.name, + description=self.snapshot.description, + metadata={'Alpha': 'a', 'Beta': 'b'}, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_snapshot_create_without_name(self): + arglist = [ + "--volume", + self.snapshot.volume_id, + ] + verifylist = [ + ("volume", self.snapshot.volume_id), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + def test_snapshot_create_without_volume(self): + arglist = [ + "--description", + self.snapshot.description, + "--force", + self.snapshot.name, + ] + verifylist = [ + ("description", self.snapshot.description), + ("force", True), + ("snapshot_name", self.snapshot.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.find_volume.assert_called_once_with( + self.snapshot.name, ignore_missing=False + ) + self.volume_client.create_snapshot.assert_called_with( + volume_id=self.snapshot.volume_id, + force=True, + name=self.snapshot.name, + description=self.snapshot.description, + metadata=None, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_snapshot_create_with_remote_source(self): + arglist = [ + '--remote-source', + 'source-name=test_source_name', + '--remote-source', + 'source-id=test_source_id', + '--volume', + self.snapshot.volume_id, + self.snapshot.name, + ] + ref_dict = { + 'source-name': 'test_source_name', + 'source-id': 'test_source_id', + } + verifylist = [ + ('remote_source', ref_dict), + ('volume', self.snapshot.volume_id), + ("snapshot_name", self.snapshot.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.volume_sdk_client.unmanage_snapshot.return_value = None + columns, data = self.cmd.take_action(parsed_args) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + self.volume_client.find_volume.assert_called_once_with( + self.snapshot.volume_id, ignore_missing=False + ) + self.volume_client.manage_snapshot.assert_called_with( + volume_id=self.snapshot.volume_id, + ref=ref_dict, + name=self.snapshot.name, + description=None, + metadata=None, + ) + self.volume_client.create_snapshot.assert_not_called() + + def test_snapshot_create_pre_v366(self): + self.set_volume_api_version('3.65') + + arglist = ["--force", self.snapshot.name] + verifylist = [("force", True), ("snapshot_name", self.snapshot.name)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + self.cmd.take_action(parsed_args) + + # force parameter should be passed + self.volume_client.create_snapshot.assert_called_with( + volume_id=self.snapshot.volume_id, + force=True, + name=self.snapshot.name, + description=None, + metadata=None, + ) + + def test_snapshot_create_v366_or_later(self): + self.set_volume_api_version('3.66') + + arglist = [self.snapshot.name] + verifylist = [("force", False), ("snapshot_name", self.snapshot.name)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # force parameter should not be passed, for >=3.66 + self.volume_client.create_snapshot.assert_called_with( + volume_id=self.snapshot.volume_id, + name=self.snapshot.name, + description=None, + metadata=None, + ) -class TestVolumeSnapshotDelete(TestVolumeSnapshot): - snapshots = volume_fakes.create_snapshots(count=2) + def test_snapshot_create_v366_or_later_with_force(self): + """--force should be ignored for microversion >= 3.66.""" + self.set_volume_api_version('3.66') + arglist = ["--force", self.snapshot.name] + verifylist = [("force", True), ("snapshot_name", self.snapshot.name)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + self.cmd.take_action(parsed_args) + + # passed but ignored + self.volume_client.create_snapshot.assert_called_with( + volume_id=self.snapshot.volume_id, + name=self.snapshot.name, + description=None, + metadata=None, + ) + + +class TestVolumeSnapshotDelete(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.snapshots_mock.get = volume_fakes.get_snapshots(self.snapshots) - self.snapshots_mock.delete.return_value = None + self.snapshots = list( + sdk_fakes.generate_fake_resources(_snapshot.Snapshot) + ) + self.volume_client.find_snapshot.side_effect = self.snapshots + self.volume_client.delete_snapshot.return_value = None + self.volume_client.unmanage_snapshot.return_value = None - # Get the command object to mock self.cmd = volume_snapshot.DeleteVolumeSnapshot(self.app, None) def test_snapshot_delete(self): @@ -49,11 +253,14 @@ def test_snapshot_delete(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.snapshots_mock.delete.assert_called_with( - self.snapshots[0].id, False + self.volume_client.find_snapshot.assert_called_once_with( + self.snapshots[0].id, ignore_missing=False + ) + self.volume_client.delete_snapshot.assert_called_once_with( + self.snapshots[0].id, force=False ) - self.assertIsNone(result) def test_snapshot_delete_with_force(self): arglist = ['--force', self.snapshots[0].id] @@ -61,11 +268,14 @@ def test_snapshot_delete_with_force(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.snapshots_mock.delete.assert_called_with( - self.snapshots[0].id, True + self.volume_client.find_snapshot.assert_called_once_with( + self.snapshots[0].id, ignore_missing=False + ) + self.volume_client.delete_snapshot.assert_called_once_with( + self.snapshots[0].id, force=True ) - self.assertIsNone(result) def test_delete_multiple_snapshots(self): arglist = [] @@ -74,17 +284,24 @@ def test_delete_multiple_snapshots(self): verifylist = [ ('snapshots', arglist), ] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - result = self.cmd.take_action(parsed_args) - calls = [] - for s in self.snapshots: - calls.append(mock.call(s.id, False)) - self.snapshots_mock.delete.assert_has_calls(calls) + result = self.cmd.take_action(parsed_args) self.assertIsNone(result) + self.volume_client.find_snapshot.assert_has_calls( + [mock.call(x.id, ignore_missing=False) for x in self.snapshots] + ) + self.volume_client.delete_snapshot.assert_has_calls( + [mock.call(x.id, force=False) for x in self.snapshots] + ) + def test_delete_multiple_snapshots_with_exception(self): + self.volume_client.find_snapshot.side_effect = [ + self.snapshots[0], + sdk_exceptions.NotFoundException(), + ] + arglist = [ self.snapshots[0].id, 'unexist_snapshot', @@ -95,25 +312,24 @@ def test_delete_multiple_snapshots_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.snapshots[0], exceptions.CommandError] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual('1 of 2 snapshots failed to delete.', str(e)) - - find_mock.assert_any_call( - self.snapshots_mock, self.snapshots[0].id - ) - find_mock.assert_any_call(self.snapshots_mock, 'unexist_snapshot') + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + self.assertEqual('1 of 2 snapshots failed to delete.', str(exc)) - self.assertEqual(2, find_mock.call_count) - self.snapshots_mock.delete.assert_called_once_with( - self.snapshots[0].id, False - ) + self.volume_client.find_snapshot.assert_has_calls( + [ + mock.call(self.snapshots[0].id, ignore_missing=False), + mock.call('unexist_snapshot', ignore_missing=False), + ] + ) + self.volume_client.delete_snapshot.assert_has_calls( + [ + mock.call(self.snapshots[0].id, force=False), + ] + ) def test_snapshot_delete_remote(self): arglist = ['--remote', self.snapshots[0].id] @@ -121,11 +337,11 @@ def test_snapshot_delete_remote(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) - self.volume_sdk_client.unmanage_snapshot.assert_called_with( + self.volume_client.unmanage_snapshot.assert_called_with( self.snapshots[0].id ) - self.assertIsNone(result) def test_snapshot_delete_with_remote_force(self): arglist = ['--remote', '--force', self.snapshots[0].id] @@ -149,12 +365,491 @@ def test_delete_multiple_snapshots_remote(self): for s in self.snapshots: arglist.append(s.id) verifylist = [('remote', True), ('snapshots', arglist[1:])] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.volume_client.unmanage_snapshot.assert_has_calls( + [mock.call(s.id) for s in self.snapshots] + ) + + +class TestVolumeSnapshotList(volume_fakes.TestVolume): + def setUp(self): + super().setUp() - calls = [] + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.snapshots = list( + sdk_fakes.generate_fake_resources( + _snapshot.Snapshot, attrs={'volume_id': self.volume.name} + ) + ) + self.volume_client.volumes.return_value = [self.volume] + self.volume_client.find_volume.return_value = self.volume + self.volume_client.snapshots.return_value = self.snapshots + + self.columns = ("ID", "Name", "Description", "Status", "Size") + self.columns_long = ( + *self.columns, + "Created At", + "Volume", + "Properties", + ) + + self.data = [] + self.data_long = [] for s in self.snapshots: - calls.append(mock.call(s.id)) - self.volume_sdk_client.unmanage_snapshot.assert_has_calls(calls) + self.data.append( + ( + s.id, + s.name, + s.description, + s.status, + s.size, + ) + ) + self.data_long.append( + ( + s.id, + s.name, + s.description, + s.status, + s.size, + s.created_at, + volume_snapshot.VolumeIdColumn( + s.volume_id, volume_cache={self.volume.id: self.volume} + ), + format_columns.DictColumn(s.metadata), + ) + ) + + self.cmd = volume_snapshot.ListVolumeSnapshot(self.app, None) + + def test_snapshot_list_without_options(self): + arglist = [] + verifylist = [('all_projects', False), ('long', False)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.snapshots.assert_called_once_with( + limit=None, + marker=None, + max_items=None, + all_projects=False, + name=None, + status=None, + project_id=None, + volume_id=None, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_with_options(self): + project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = project + + arglist = [ + "--long", + "--limit", + "2", + "--project", + project.id, + "--marker", + self.snapshots[0].id, + ] + verifylist = [ + ("long", True), + ("limit", 2), + ("project", project.id), + ("marker", self.snapshots[0].id), + ('all_projects', False), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.snapshots.assert_called_once_with( + limit=2, + marker=self.snapshots[0].id, + max_items=None, + all_projects=True, + project_id=project.id, + name=None, + status=None, + volume_id=None, + ) + self.assertEqual(self.columns_long, columns) + self.assertEqual(self.data_long, list(data)) + + def test_snapshot_list_all_projects(self): + arglist = [ + '--all-projects', + ] + verifylist = [('long', False), ('all_projects', True)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.snapshots.assert_called_once_with( + limit=None, + marker=None, + max_items=None, + all_projects=True, + name=None, + status=None, + project_id=None, + volume_id=None, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_name_option(self): + arglist = [ + '--name', + self.snapshots[0].name, + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('name', self.snapshots[0].name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.snapshots.assert_called_once_with( + limit=None, + marker=None, + max_items=None, + all_projects=False, + name=self.snapshots[0].name, + status=None, + project_id=None, + volume_id=None, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_status_option(self): + arglist = [ + '--status', + 'available', + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('status', 'available'), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.snapshots.assert_called_once_with( + limit=None, + marker=None, + max_items=None, + all_projects=False, + name=None, + status='available', + project_id=None, + volume_id=None, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_volumeid_option(self): + arglist = [ + '--volume', + self.volume.id, + ] + verifylist = [ + ('all_projects', False), + ('long', False), + ('volume', self.volume.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.snapshots.assert_called_once_with( + limit=None, + marker=None, + max_items=None, + all_projects=False, + name=None, + status=None, + project_id=None, + volume_id=self.volume.id, + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, list(data)) + + def test_snapshot_list_negative_limit(self): + arglist = [ + "--limit", + "-2", + ] + verifylist = [ + ("limit", -2), + ] + self.assertRaises( + test_utils.ParserException, + self.check_parser, + self.cmd, + arglist, + verifylist, + ) + + +class TestVolumeSnapshotSet(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.snapshot = sdk_fakes.generate_fake_resource( + _snapshot.Snapshot, metadata={'foo': 'bar'} + ) + self.volume_client.find_snapshot.return_value = self.snapshot + self.volume_client.set_snapshot_metadata.return_value = None + self.volume_client.update_snapshot.return_value = None + # Get the command object to mock + self.cmd = volume_snapshot.SetVolumeSnapshot(self.app, None) + + def test_snapshot_set_no_option(self): + arglist = [ + self.snapshot.id, + ] + verifylist = [ + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + self.volume_client.find_snapshot.assert_called_once_with( + parsed_args.snapshot, ignore_missing=False + ) + self.volume_client.reset_snapshot_status.assert_not_called() + self.volume_client.update_snapshot.assert_not_called() + self.volume_client.set_snapshot_metadata.assert_not_called() + + def test_snapshot_set_name_and_property(self): + arglist = [ + "--name", + "new_snapshot", + "--property", + "x=y", + "--property", + "foo=foo", + self.snapshot.id, + ] + verifylist = [ + ("name", "new_snapshot"), + ("properties", {"x": "y", "foo": "foo"}), + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.volume_client.update_snapshot.assert_called_with( + self.snapshot.id, name="new_snapshot" + ) + self.volume_client.set_snapshot_metadata.assert_called_with( + self.snapshot.id, x="y", foo="foo" + ) + + def test_snapshot_set_with_no_property(self): + arglist = [ + "--no-property", + self.snapshot.id, + ] + verifylist = [ + ("no_property", True), + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.volume_client.find_snapshot.assert_called_once_with( + parsed_args.snapshot, ignore_missing=False + ) + self.volume_client.reset_snapshot_status.assert_not_called() + self.volume_client.update_snapshot.assert_not_called() + self.volume_client.set_snapshot_metadata.assert_not_called() + self.volume_client.delete_snapshot_metadata.assert_called_with( + self.snapshot.id, keys=["foo"] + ) + + def test_snapshot_set_with_no_property_and_property(self): + arglist = [ + "--no-property", + "--property", + "foo_1=bar_1", + self.snapshot.id, + ] + verifylist = [ + ("no_property", True), + ("properties", {"foo_1": "bar_1"}), + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.volume_client.find_snapshot.assert_called_once_with( + parsed_args.snapshot, ignore_missing=False + ) + self.volume_client.reset_snapshot_status.assert_not_called() + self.volume_client.update_snapshot.assert_not_called() + self.volume_client.delete_snapshot_metadata.assert_called_with( + self.snapshot.id, keys=["foo"] + ) + self.volume_client.set_snapshot_metadata.assert_called_once_with( + self.snapshot.id, + foo_1="bar_1", + ) + + def test_snapshot_set_state_to_error(self): + arglist = ["--state", "error", self.snapshot.id] + verifylist = [("state", "error"), ("snapshot", self.snapshot.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.volume_client.reset_snapshot_status.assert_called_with( + self.snapshot.id, "error" + ) + + def test_volume_set_state_failed(self): + self.volume_client.reset_snapshot_status.side_effect = ( + exceptions.CommandError() + ) + arglist = ['--state', 'error', self.snapshot.id] + verifylist = [('state', 'error'), ('snapshot', self.snapshot.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + self.assertEqual('One or more of the set operations failed', str(exc)) + self.volume_client.reset_snapshot_status.assert_called_once_with( + self.snapshot.id, 'error' + ) + + def test_volume_set_name_and_state_failed(self): + self.volume_client.reset_snapshot_status.side_effect = ( + exceptions.CommandError() + ) + arglist = [ + '--state', + 'error', + "--name", + "new_snapshot", + self.snapshot.id, + ] + verifylist = [ + ('state', 'error'), + ("name", "new_snapshot"), + ('snapshot', self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + self.assertEqual('One or more of the set operations failed', str(exc)) + self.volume_client.update_snapshot.assert_called_once_with( + self.snapshot.id, name="new_snapshot" + ) + self.volume_client.reset_snapshot_status.assert_called_once_with( + self.snapshot.id, 'error' + ) + + +class TestVolumeSnapshotShow(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) + + self.columns = ( + 'created_at', + 'description', + 'id', + 'name', + 'properties', + 'size', + 'status', + 'volume_id', + ) + self.data = ( + self.snapshot.created_at, + self.snapshot.description, + self.snapshot.id, + self.snapshot.name, + format_columns.DictColumn(self.snapshot.metadata), + self.snapshot.size, + self.snapshot.status, + self.snapshot.volume_id, + ) + + self.volume_client.find_snapshot.return_value = self.snapshot + + self.cmd = volume_snapshot.ShowVolumeSnapshot(self.app, None) + + def test_snapshot_show(self): + arglist = [self.snapshot.id] + verifylist = [("snapshot", self.snapshot.id)] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + self.volume_client.find_snapshot.assert_called_with( + self.snapshot.id, ignore_missing=False + ) + + self.assertEqual(self.columns, columns) + self.assertCountEqual(self.data, data) + + +class TestVolumeSnapshotUnset(volume_fakes.TestVolume): + def setUp(self): + super().setUp() + + self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) + self.volume_client.find_snapshot.return_value = self.snapshot + self.volume_client.delete_snapshot_metadata.return_value = None + + self.cmd = volume_snapshot.UnsetVolumeSnapshot(self.app, None) + + def test_snapshot_unset(self): + arglist = [ + "--property", + "foo", + self.snapshot.id, + ] + verifylist = [ + ("properties", ["foo"]), + ("snapshot", self.snapshot.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + + self.assertIsNone(result) + self.volume_client.delete_snapshot_metadata.assert_called_with( + self.snapshot.id, keys=["foo"] + ) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_transfer_request.py b/openstackclient/tests/unit/volume/v3/test_volume_transfer_request.py index ffe59db659..4e3952ea7c 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_transfer_request.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_transfer_request.py @@ -12,31 +12,20 @@ # under the License. # -from unittest import mock from unittest.mock import call +from openstack.block_storage.v3 import transfer as _transfer +from openstack.block_storage.v3 import volume as _volume +from openstack import exceptions as sdk_exceptions +from openstack.test import fakes as sdk_fakes from osc_lib import exceptions -from osc_lib import utils from openstackclient.tests.unit import utils as test_utils from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes from openstackclient.volume.v3 import volume_transfer_request -class TestTransfer(volume_fakes.TestVolume): - def setUp(self): - super().setUp() - - # Get a shortcut to the TransferManager Mock - self.transfer_mock = self.volume_client.transfers - self.transfer_mock.reset_mock() - - # Get a shortcut to the VolumeManager Mock - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - - -class TestTransferAccept(TestTransfer): +class TestTransferAccept(volume_fakes.TestVolume): columns = ( 'id', 'name', @@ -46,17 +35,18 @@ class TestTransferAccept(TestTransfer): def setUp(self): super().setUp() - self.volume_transfer = volume_fakes.create_one_transfer() + self.volume_transfer = sdk_fakes.generate_fake_resource( + _transfer.Transfer + ) self.data = ( self.volume_transfer.id, self.volume_transfer.name, self.volume_transfer.volume_id, ) - self.transfer_mock.get.return_value = self.volume_transfer - self.transfer_mock.accept.return_value = self.volume_transfer + self.volume_client.find_transfer.return_value = self.volume_transfer + self.volume_client.accept_transfer.return_value = self.volume_transfer - # Get the command object to test self.cmd = volume_transfer_request.AcceptTransferRequest( self.app, None ) @@ -75,23 +65,44 @@ def test_transfer_accept(self): columns, data = self.cmd.take_action(parsed_args) - self.transfer_mock.get.assert_called_once_with( - self.volume_transfer.id, + self.volume_client.find_transfer.assert_called_once_with( + self.volume_transfer.id, ignore_missing=False ) - self.transfer_mock.accept.assert_called_once_with( + self.volume_client.accept_transfer.assert_called_once_with( self.volume_transfer.id, 'key_value', ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - def test_transfer_accept_no_option(self): + def test_transfer_accept_non_admin(self): + """Non-admin users get ResourceNotFound on find_transfer; we fall back.""" + self.volume_client.find_transfer.side_effect = ( + sdk_exceptions.ResourceNotFound + ) arglist = [ + '--auth-key', + 'key_value', self.volume_transfer.id, ] verifylist = [ ('transfer_request', self.volume_transfer.id), + ('auth_key', 'key_value'), ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.volume_client.accept_transfer.assert_called_once_with( + self.volume_transfer.id, + 'key_value', + ) + self.assertEqual(self.columns, columns) + self.assertEqual(self.data, data) + + def test_transfer_accept_no_option(self): + arglist = [self.volume_transfer.id] + verifylist = [('transfer_request', self.volume_transfer.id)] self.assertRaises( test_utils.ParserException, @@ -102,9 +113,7 @@ def test_transfer_accept_no_option(self): ) -class TestTransferCreate(TestTransfer): - volume = volume_fakes.create_one_volume() - +class TestTransferCreate(volume_fakes.TestVolume): columns = ( 'auth_key', 'created_at', @@ -116,12 +125,9 @@ class TestTransferCreate(TestTransfer): def setUp(self): super().setUp() - self.volume_transfer = volume_fakes.create_one_transfer( - attrs={ - 'volume_id': self.volume.id, - 'auth_key': 'key', - 'created_at': 'time', - }, + self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) + self.volume_transfer = sdk_fakes.generate_fake_resource( + _transfer.Transfer, volume_id=self.volume.id ) self.data = ( self.volume_transfer.auth_key, @@ -131,26 +137,24 @@ def setUp(self): self.volume_transfer.volume_id, ) - self.transfer_mock.create.return_value = self.volume_transfer - self.volumes_mock.get.return_value = self.volume + self.volume_client.find_volume.return_value = self.volume + self.volume_client.create_transfer.return_value = self.volume_transfer - # Get the command object to test self.cmd = volume_transfer_request.CreateTransferRequest( self.app, None ) def test_transfer_create_without_name(self): - arglist = [ - self.volume.id, - ] - verifylist = [ - ('volume', self.volume.id), - ] + arglist = [self.volume.id] + verifylist = [('volume', self.volume.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.transfer_mock.create.assert_called_once_with(self.volume.id, None) + self.volume_client.create_transfer.assert_called_once_with( + volume_id=self.volume.id, + name=None, + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -168,9 +172,9 @@ def test_transfer_create_with_name(self): columns, data = self.cmd.take_action(parsed_args) - self.transfer_mock.create.assert_called_once_with( - self.volume.id, - self.volume_transfer.name, + self.volume_client.create_transfer.assert_called_once_with( + volume_id=self.volume.id, + name=self.volume_transfer.name, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -191,8 +195,10 @@ def test_transfer_create_with_no_snapshots(self): columns, data = self.cmd.take_action(parsed_args) - self.transfer_mock.create.assert_called_once_with( - self.volume.id, None, no_snapshots=True + self.volume_client.create_transfer.assert_called_once_with( + volume_id=self.volume.id, + name=None, + no_snapshots=True, ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) @@ -219,18 +225,16 @@ def test_transfer_create_pre_v355(self): ) -class TestTransferDelete(TestTransfer): - volume_transfers = volume_fakes.create_transfers(count=2) - +class TestTransferDelete(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.transfer_mock.get = volume_fakes.get_transfers( - self.volume_transfers, - ) - self.transfer_mock.delete.return_value = None + self.volume_transfers = [ + sdk_fakes.generate_fake_resource(_transfer.Transfer), + sdk_fakes.generate_fake_resource(_transfer.Transfer), + ] + self.volume_client.delete_transfer.return_value = None - # Get the command object to mock self.cmd = volume_transfer_request.DeleteTransferRequest( self.app, None ) @@ -242,26 +246,22 @@ def test_transfer_delete(self): result = self.cmd.take_action(parsed_args) - self.transfer_mock.delete.assert_called_with( - self.volume_transfers[0].id + self.volume_client.delete_transfer.assert_called_once_with( + self.volume_transfers[0].id, ignore_missing=False ) self.assertIsNone(result) def test_delete_multiple_transfers(self): - arglist = [] - for v in self.volume_transfers: - arglist.append(v.id) - verifylist = [ - ('transfer_request', arglist), - ] + arglist = [v.id for v in self.volume_transfers] + verifylist = [('transfer_request', arglist)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - calls = [] - for v in self.volume_transfers: - calls.append(call(v.id)) - self.transfer_mock.delete.assert_has_calls(calls) + calls = [ + call(v.id, ignore_missing=False) for v in self.volume_transfers + ] + self.volume_client.delete_transfer.assert_has_calls(calls) self.assertIsNone(result) def test_delete_multiple_transfers_with_exception(self): @@ -269,46 +269,33 @@ def test_delete_multiple_transfers_with_exception(self): self.volume_transfers[0].id, 'unexist_transfer', ] - verifylist = [ - ('transfer_request', arglist), - ] - + verifylist = [('transfer_request', arglist)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.volume_transfers[0], exceptions.CommandError] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - '1 of 2 volume transfer requests failed to delete', - str(e), - ) - - find_mock.assert_any_call( - self.transfer_mock, self.volume_transfers[0].id - ) - find_mock.assert_any_call(self.transfer_mock, 'unexist_transfer') + self.volume_client.delete_transfer.side_effect = [ + None, + exceptions.CommandError, + ] - self.assertEqual(2, find_mock.call_count) - self.transfer_mock.delete.assert_called_once_with( - self.volume_transfers[0].id, + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual( + '1 of 2 volume transfer requests failed to delete', + str(e), ) -class TestTransferList(TestTransfer): - # The Transfers to be listed - volume_transfers = volume_fakes.create_one_transfer() - +class TestTransferList(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.transfer_mock.list.return_value = [self.volume_transfers] + self.volume_transfers = [ + sdk_fakes.generate_fake_resource(_transfer.Transfer), + ] + self.volume_client.transfers.return_value = self.volume_transfers - # Get the command object to test self.cmd = volume_transfer_request.ListTransferRequest(self.app, None) def test_transfer_list_without_argument(self): @@ -316,74 +303,37 @@ def test_transfer_list_without_argument(self): verifylist = [] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - expected_columns = [ - 'ID', - 'Name', - 'Volume', - ] - - # confirming if all expected columns are present in the result. - self.assertEqual(expected_columns, columns) - - datalist = ( + self.volume_client.transfers.assert_called_once_with( + details=True, all_projects=False + ) + self.assertEqual(('ID', 'Name', 'Volume'), columns) + self.assertEqual( ( - self.volume_transfers.id, - self.volume_transfers.name, - self.volume_transfers.volume_id, + ( + self.volume_transfers[0].id, + self.volume_transfers[0].name, + self.volume_transfers[0].volume_id, + ), ), - ) - - # confirming if all expected values are present in the result. - self.assertEqual(datalist, tuple(data)) - - # checking if proper call was made to list volume_transfers - self.transfer_mock.list.assert_called_with( - detailed=True, search_opts={'all_tenants': 0} + tuple(data), ) def test_transfer_list_with_argument(self): arglist = ["--all-projects"] verifylist = [("all_projects", True)] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - # In base command class Lister in cliff, abstract method take_action() - # returns a tuple containing the column names and an iterable - # containing the data to be listed. - columns, data = self.cmd.take_action(parsed_args) - - expected_columns = [ - 'ID', - 'Name', - 'Volume', - ] - - # confirming if all expected columns are present in the result. - self.assertEqual(expected_columns, columns) - - datalist = ( - ( - self.volume_transfers.id, - self.volume_transfers.name, - self.volume_transfers.volume_id, - ), - ) - - # confirming if all expected values are present in the result. - self.assertEqual(datalist, tuple(data)) + columns, _data = self.cmd.take_action(parsed_args) - # checking if proper call was made to list volume_transfers - self.transfer_mock.list.assert_called_with( - detailed=True, search_opts={'all_tenants': 1} + self.volume_client.transfers.assert_called_once_with( + details=True, all_projects=True ) + self.assertEqual(('ID', 'Name', 'Volume'), columns) -class TestTransferShow(TestTransfer): +class TestTransferShow(volume_fakes.TestVolume): columns = ( 'created_at', 'id', @@ -394,8 +344,8 @@ class TestTransferShow(TestTransfer): def setUp(self): super().setUp() - self.volume_transfer = volume_fakes.create_one_transfer( - attrs={'created_at': 'time'}, + self.volume_transfer = sdk_fakes.generate_fake_resource( + _transfer.Transfer ) self.data = ( self.volume_transfer.created_at, @@ -404,22 +354,19 @@ def setUp(self): self.volume_transfer.volume_id, ) - self.transfer_mock.get.return_value = self.volume_transfer + self.volume_client.find_transfer.return_value = self.volume_transfer - # Get the command object to test self.cmd = volume_transfer_request.ShowTransferRequest(self.app, None) def test_transfer_show(self): - arglist = [ - self.volume_transfer.id, - ] - verifylist = [ - ('transfer_request', self.volume_transfer.id), - ] + arglist = [self.volume_transfer.id] + verifylist = [('transfer_request', self.volume_transfer.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.transfer_mock.get.assert_called_once_with(self.volume_transfer.id) + self.volume_client.find_transfer.assert_called_once_with( + self.volume_transfer.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_type.py b/openstackclient/tests/unit/volume/v3/test_volume_type.py index 828f8b0902..6fed24f859 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_type.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_type.py @@ -11,62 +11,45 @@ # License for the specific language governing permissions and limitations # under the License. -from unittest import mock from unittest.mock import call +from openstack.block_storage.v3 import type as _type +from openstack import exceptions as sdk_exceptions +from openstack.identity.v3 import project as _project +from openstack.test import fakes as sdk_fakes from osc_lib.cli import format_columns from osc_lib import exceptions from osc_lib import utils -from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes from openstackclient.tests.unit import utils as tests_utils from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes from openstackclient.volume.v3 import volume_type -class TestType(volume_fakes.TestVolume): +class TestTypeCreate(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volume_types_mock = self.volume_client.volume_types - self.volume_types_mock.reset_mock() - - self.volume_type_access_mock = self.volume_client.volume_type_access - self.volume_type_access_mock.reset_mock() - - self.volume_encryption_types_mock = ( - self.volume_client.volume_encryption_types - ) - self.volume_encryption_types_mock.reset_mock() - - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() - - -class TestTypeCreate(TestType): - def setUp(self): - super().setUp() - - self.new_volume_type = volume_fakes.create_one_volume_type( - methods={'set_keys': None}, - ) - self.project = identity_fakes.FakeProject.create_one_project() + self.new_volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.project = sdk_fakes.generate_fake_resource(_project.Project) self.columns = ( 'description', 'id', 'is_public', 'name', + 'properties', ) self.data = ( self.new_volume_type.description, self.new_volume_type.id, - True, + self.new_volume_type.is_public, self.new_volume_type.name, + format_columns.DictColumn(self.new_volume_type.extra_specs), ) - self.volume_types_mock.create.return_value = self.new_volume_type - self.projects_mock.get.return_value = self.project - # Get the command object to test + self.volume_client.create_type.return_value = self.new_volume_type + self.identity_sdk_client.find_project.return_value = self.project + self.cmd = volume_type.CreateVolumeType(self.app, None) def test_type_create_public(self): @@ -84,8 +67,8 @@ def test_type_create_public(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.create.assert_called_with( - self.new_volume_type.name, + self.volume_client.create_type.assert_called_with( + name=self.new_volume_type.name, description=self.new_volume_type.description, is_public=True, ) @@ -111,8 +94,8 @@ def test_type_create_private(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.create.assert_called_with( - self.new_volume_type.name, + self.volume_client.create_type.assert_called_with( + name=self.new_volume_type.name, description=self.new_volume_type.description, is_public=False, ) @@ -142,25 +125,41 @@ def test_type_create_with_properties(self): ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.create.assert_called_with( - self.new_volume_type.name, description=None - ) - self.new_volume_type.set_keys.assert_called_once_with( - { + result_type = sdk_fakes.generate_fake_resource( + _type.Type, + extra_specs={ 'myprop': 'myvalue', 'multiattach': ' True', 'cacheable': ' True', 'replication_enabled': ' True', 'RESKEY:availability_zones': 'az1', - } + }, + ) + self.volume_client.update_type_extra_specs.return_value = result_type + + columns, data = self.cmd.take_action(parsed_args) + self.volume_client.create_type.assert_called_with( + name=self.new_volume_type.name, description=None + ) + self.volume_client.update_type_extra_specs.assert_called_once_with( + self.new_volume_type.id, + myprop='myvalue', + multiattach=' True', + cacheable=' True', + replication_enabled=' True', + **{'RESKEY:availability_zones': 'az1'}, ) - self.columns += ('properties',) - self.data += (format_columns.DictColumn(None),) + expected_data = ( + self.new_volume_type.description, + self.new_volume_type.id, + self.new_volume_type.is_public, + self.new_volume_type.name, + format_columns.DictColumn(result_type.extra_specs), + ) self.assertEqual(self.columns, columns) - self.assertCountEqual(self.data, data) + self.assertCountEqual(expected_data, data) def test_public_type_create_with_project_public(self): arglist = [ @@ -182,33 +181,40 @@ def test_public_type_create_with_project_public(self): ) def test_type_create_with_encryption(self): - encryption_info = { - 'provider': 'LuksEncryptor', - 'cipher': 'aes-xts-plain64', - 'key_size': '128', - 'control_location': 'front-end', - } - encryption_type = volume_fakes.create_one_encryption_volume_type( - attrs=encryption_info, + encryption_type = sdk_fakes.generate_fake_resource( + _type.TypeEncryption, + provider='LuksEncryptor', + cipher='aes-xts-plain64', + key_size='128', + control_location='front-end', ) - self.new_volume_type = volume_fakes.create_one_volume_type( - attrs={'encryption': encryption_info}, + self.new_volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.volume_client.create_type.return_value = self.new_volume_type + self.volume_client.create_type_encryption.return_value = ( + encryption_type ) - self.volume_types_mock.create.return_value = self.new_volume_type - self.volume_encryption_types_mock.create.return_value = encryption_type + expected_encryption_info = { + 'provider': encryption_type.provider, + 'cipher': encryption_type.cipher, + 'key_size': encryption_type.key_size, + 'control_location': encryption_type.control_location, + 'encryption_id': encryption_type.encryption_id, + } encryption_columns = ( 'description', 'encryption', 'id', 'is_public', 'name', + 'properties', ) encryption_data = ( self.new_volume_type.description, - format_columns.DictColumn(encryption_info), + format_columns.DictColumn(expected_encryption_info), self.new_volume_type.id, - True, + self.new_volume_type.is_public, self.new_volume_type.name, + format_columns.DictColumn(self.new_volume_type.extra_specs), ) arglist = [ '--encryption-provider', @@ -231,36 +237,30 @@ def test_type_create_with_encryption(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.create.assert_called_with( - self.new_volume_type.name, + self.volume_client.create_type.assert_called_with( + name=self.new_volume_type.name, description=None, ) - body = { - 'provider': 'LuksEncryptor', - 'cipher': 'aes-xts-plain64', - 'key_size': 128, - 'control_location': 'front-end', - } - self.volume_encryption_types_mock.create.assert_called_with( + self.volume_client.create_type_encryption.assert_called_with( self.new_volume_type, - body, + provider='LuksEncryptor', + cipher='aes-xts-plain64', + key_size=128, + control_location='front-end', ) self.assertEqual(encryption_columns, columns) self.assertCountEqual(encryption_data, data) -class TestTypeDelete(TestType): - volume_types = volume_fakes.create_volume_types(count=2) +class TestTypeDelete(volume_fakes.TestVolume): + volume_types = list(sdk_fakes.generate_fake_resources(_type.Type, count=2)) def setUp(self): super().setUp() - self.volume_types_mock.get = volume_fakes.get_volume_types( - self.volume_types, - ) - self.volume_types_mock.delete.return_value = None + self.volume_client.find_type.side_effect = self.volume_types + self.volume_client.delete_type.return_value = None - # Get the command object to mock self.cmd = volume_type.DeleteVolumeType(self.app, None) def test_type_delete(self): @@ -270,7 +270,10 @@ def test_type_delete(self): result = self.cmd.take_action(parsed_args) - self.volume_types_mock.delete.assert_called_with(self.volume_types[0]) + self.volume_client.find_type.assert_called_with( + self.volume_types[0].id, ignore_missing=False + ) + self.volume_client.delete_type.assert_called_with(self.volume_types[0]) self.assertIsNone(result) def test_delete_multiple_types(self): @@ -287,7 +290,7 @@ def test_delete_multiple_types(self): calls = [] for t in self.volume_types: calls.append(call(t)) - self.volume_types_mock.delete.assert_has_calls(calls) + self.volume_client.delete_type.assert_has_calls(calls) self.assertIsNone(result) def test_delete_multiple_types_with_exception(self): @@ -301,37 +304,37 @@ def test_delete_multiple_types_with_exception(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) - find_mock_result = [self.volume_types[0], exceptions.CommandError] - with mock.patch.object( - utils, 'find_resource', side_effect=find_mock_result - ) as find_mock: - try: - self.cmd.take_action(parsed_args) - self.fail('CommandError should be raised.') - except exceptions.CommandError as e: - self.assertEqual( - '1 of 2 volume types failed to delete.', str(e) - ) - find_mock.assert_any_call( - self.volume_types_mock, self.volume_types[0].id - ) - find_mock.assert_any_call(self.volume_types_mock, 'unexist_type') + self.volume_client.find_type.side_effect = [ + self.volume_types[0], + exceptions.CommandError, + ] + try: + self.cmd.take_action(parsed_args) + self.fail('CommandError should be raised.') + except exceptions.CommandError as e: + self.assertEqual('1 of 2 volume types failed to delete.', str(e)) + self.volume_client.find_type.assert_any_call( + self.volume_types[0].id, ignore_missing=False + ) + self.volume_client.find_type.assert_any_call( + 'unexist_type', ignore_missing=False + ) - self.assertEqual(2, find_mock.call_count) - self.volume_types_mock.delete.assert_called_once_with( - self.volume_types[0] - ) + self.assertEqual(2, self.volume_client.find_type.call_count) + self.volume_client.delete_type.assert_called_once_with( + self.volume_types[0] + ) -class TestTypeList(TestType): - volume_types = volume_fakes.create_volume_types() +class TestTypeList(volume_fakes.TestVolume): + volume_types = list(sdk_fakes.generate_fake_resources(_type.Type, count=2)) columns = [ "ID", "Name", "Is Public", ] - columns_long = columns + ["Description", "Properties"] + columns_long = [*columns, "Description", "Properties"] data_with_default_type = [(volume_types[0].id, volume_types[0].name, True)] data = [] for t in volume_types: @@ -357,24 +360,21 @@ class TestTypeList(TestType): def setUp(self): super().setUp() - self.volume_types_mock.list.return_value = self.volume_types - self.volume_types_mock.default.return_value = self.volume_types[0] - # get the command to test + self.volume_client.types.return_value = self.volume_types + self.cmd = volume_type.ListVolumeType(self.app, None) def test_type_list_without_options(self): arglist = [] verifylist = [ ("long", False), - ("is_public", None), + ("is_public", 'none'), ("default", False), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.list.assert_called_once_with( - search_opts={}, is_public=None - ) + self.volume_client.types.assert_called_once_with(is_public='none') self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) @@ -391,9 +391,7 @@ def test_type_list_with_options(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.list.assert_called_once_with( - search_opts={}, is_public=True - ) + self.volume_client.types.assert_called_once_with(is_public=True) self.assertEqual(self.columns_long, columns) self.assertCountEqual(self.data_long, list(data)) @@ -409,26 +407,26 @@ def test_type_list_with_private_option(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.list.assert_called_once_with( - search_opts={}, is_public=False - ) + self.volume_client.types.assert_called_once_with(is_public=False) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) def test_type_list_with_default_option(self): + self.volume_client.get_type.return_value = self.volume_types[0] + arglist = [ "--default", ] verifylist = [ ("encryption_type", False), ("long", False), - ("is_public", None), + ("is_public", 'none'), ("default", True), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.default.assert_called_once_with() + self.volume_client.get_type.assert_called_once_with('default') self.assertEqual(self.columns, columns) self.assertCountEqual(self.data_with_default_type, list(data)) @@ -447,7 +445,7 @@ def test_type_list_with_properties(self): verifylist = [ ("encryption_type", False), ("long", False), - ("is_public", None), + ("is_public", 'none'), ("default", False), ("properties", {"foo": "bar"}), ("multiattach", True), @@ -458,17 +456,13 @@ def test_type_list_with_properties(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.list.assert_called_once_with( - search_opts={ - "extra_specs": { - "foo": "bar", - "multiattach": " True", - "cacheable": " True", - "replication_enabled": " True", - "RESKEY:availability_zones": "az1", - } - }, - is_public=None, + self.volume_client.types.assert_called_once_with( + is_public='none', + foo="bar", + multiattach=" True", + cacheable=" True", + replication_enabled=" True", + **{"RESKEY:availability_zones": "az1"}, ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, list(data)) @@ -483,7 +477,7 @@ def test_type_list_with_properties_pre_v352(self): verifylist = [ ("encryption_type", False), ("long", False), - ("is_public", None), + ("is_public", 'none'), ("default", False), ("properties", {"foo": "bar"}), ] @@ -500,40 +494,38 @@ def test_type_list_with_properties_pre_v352(self): ) def test_type_list_with_encryption(self): - encryption_type = volume_fakes.create_one_encryption_volume_type( - attrs={'volume_type_id': self.volume_types[0].id}, - ) - encryption_info = { - 'provider': 'LuksEncryptor', - 'cipher': None, - 'key_size': None, - 'control_location': 'front-end', - } - encryption_columns = self.columns + [ - "Encryption", + encryption_types = [ + sdk_fakes.generate_fake_resource( + _type.TypeEncryption, + volume_type_id=vt.id, + ) + for vt in self.volume_types ] - encryption_data = [] - encryption_data.append( + expected_encryption_info = [ + { + 'provider': encryption_type.provider, + 'cipher': encryption_type.cipher, + 'key_size': encryption_type.key_size, + 'control_location': encryption_type.control_location, + 'encryption_id': encryption_type.encryption_id, + } + for encryption_type in encryption_types + ] + encryption_columns = [*self.columns, "Encryption"] + encryption_data = [ ( - self.volume_types[0].id, - self.volume_types[0].name, - self.volume_types[0].is_public, + self.volume_types[x].id, + self.volume_types[x].name, + self.volume_types[x].is_public, volume_type.EncryptionInfoColumn( - self.volume_types[0].id, - {self.volume_types[0].id: encryption_info}, + self.volume_types[x].id, + {self.volume_types[x].id: expected_encryption_info[x]}, ), ) - ) - encryption_data.append( - ( - self.volume_types[1].id, - self.volume_types[1].name, - self.volume_types[1].is_public, - volume_type.EncryptionInfoColumn(self.volume_types[1].id, {}), - ) - ) + for x in (0, 1) + ] - self.volume_encryption_types_mock.list.return_value = [encryption_type] + self.volume_client.get_type_encryption.side_effect = encryption_types arglist = [ "--encryption-type", ] @@ -543,27 +535,25 @@ def test_type_list_with_encryption(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_encryption_types_mock.list.assert_called_once_with() - self.volume_types_mock.list.assert_called_once_with( - search_opts={}, is_public=None + self.volume_client.get_type_encryption.assert_has_calls( + [call(self.volume_types[0].id), call(self.volume_types[1].id)], ) + self.volume_client.types.assert_called_once_with(is_public='none') self.assertEqual(encryption_columns, columns) self.assertCountEqual(encryption_data, list(data)) -class TestTypeSet(TestType): +class TestTypeSet(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.project = identity_fakes.FakeProject.create_one_project() - self.projects_mock.get.return_value = self.project + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project - self.volume_type = volume_fakes.create_one_volume_type( - methods={'set_keys': None}, - ) - self.volume_types_mock.get.return_value = self.volume_type - self.volume_encryption_types_mock.create.return_value = None - self.volume_encryption_types_mock.update.return_value = None + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.volume_client.find_type.return_value = self.volume_type + self.volume_client.create_type_encryption.return_value = None + self.volume_client.update_type_encryption.return_value = None self.cmd = volume_type.SetVolumeType(self.app, None) @@ -591,14 +581,14 @@ def test_type_set(self): 'description': 'new_description', 'is_public': False, } - self.volume_types_mock.update.assert_called_with( + self.volume_client.update_type.assert_called_with( self.volume_type.id, **kwargs ) self.assertIsNone(result) - self.volume_type_access_mock.add_project_access.assert_not_called() - self.volume_encryption_types_mock.update.assert_not_called() - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.add_type_access.assert_not_called() + self.volume_client.update_type_encryption.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() def test_type_set_property(self): arglist = [ @@ -627,18 +617,17 @@ def test_type_set_property(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_type.set_keys.assert_called_once_with( - { - 'myprop': 'myvalue', - 'multiattach': ' True', - 'cacheable': ' True', - 'replication_enabled': ' True', - 'RESKEY:availability_zones': 'az1', - } + self.volume_client.update_type_extra_specs.assert_called_once_with( + self.volume_type.id, + myprop='myvalue', + multiattach=' True', + cacheable=' True', + replication_enabled=' True', + **{'RESKEY:availability_zones': 'az1'}, ) - self.volume_type_access_mock.add_project_access.assert_not_called() - self.volume_encryption_types_mock.update.assert_not_called() - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.add_type_access.assert_not_called() + self.volume_client.update_type_encryption.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() def test_type_set_with_empty_project(self): arglist = [ @@ -656,10 +645,10 @@ def test_type_set_with_empty_project(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_type.set_keys.assert_not_called() - self.volume_type_access_mock.add_project_access.assert_not_called() - self.volume_encryption_types_mock.update.assert_not_called() - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.update_type_extra_specs.assert_not_called() + self.volume_client.add_type_access.assert_not_called() + self.volume_client.update_type_encryption.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() def test_type_set_with_project(self): arglist = [ @@ -676,17 +665,17 @@ def test_type_set_with_project(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_type.set_keys.assert_not_called() - self.volume_type_access_mock.add_project_access.assert_called_with( + self.volume_client.update_type_extra_specs.assert_not_called() + self.volume_client.add_type_access.assert_called_with( self.volume_type.id, self.project.id, ) - self.volume_encryption_types_mock.update.assert_not_called() - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.update_type_encryption.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() def test_type_set_with_new_encryption(self): - self.volume_encryption_types_mock.update.side_effect = ( - exceptions.NotFound('NotFound') + self.volume_client.update_type_encryption.side_effect = ( + sdk_exceptions.NotFoundException('NotFound') ) arglist = [ '--encryption-provider', @@ -711,24 +700,23 @@ def test_type_set_with_new_encryption(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - body = { - 'provider': 'LuksEncryptor', - 'cipher': 'aes-xts-plain64', - 'key_size': 128, - 'control_location': 'front-end', - } - self.volume_encryption_types_mock.update.assert_called_with( - self.volume_type, - body, + self.volume_client.update_type_encryption.assert_called_with( + encryption=None, + volume_type=self.volume_type, + provider='LuksEncryptor', + cipher='aes-xts-plain64', + key_size=128, + control_location='front-end', ) - self.volume_encryption_types_mock.create.assert_called_with( + self.volume_client.create_type_encryption.assert_called_with( self.volume_type, - body, + provider='LuksEncryptor', + cipher='aes-xts-plain64', + key_size=128, + control_location='front-end', ) - @mock.patch.object(utils, 'find_resource') - def test_type_set_with_existing_encryption(self, mock_find): - mock_find.side_effect = [self.volume_type, "existing_encryption_type"] + def test_type_set_with_existing_encryption(self): arglist = [ '--encryption-provider', 'LuksEncryptor', @@ -749,22 +737,20 @@ def test_type_set_with_existing_encryption(self, mock_find): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_type.set_keys.assert_not_called() - self.volume_type_access_mock.add_project_access.assert_not_called() - body = { - 'provider': 'LuksEncryptor', - 'cipher': 'aes-xts-plain64', - 'control_location': 'front-end', - } - self.volume_encryption_types_mock.update.assert_called_with( - self.volume_type, - body, + self.volume_client.update_type_extra_specs.assert_not_called() + self.volume_client.add_type_access.assert_not_called() + self.volume_client.update_type_encryption.assert_called_with( + encryption=None, + volume_type=self.volume_type, + provider='LuksEncryptor', + cipher='aes-xts-plain64', + control_location='front-end', ) - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() def test_type_set_new_encryption_without_provider(self): - self.volume_encryption_types_mock.update.side_effect = ( - exceptions.NotFound('NotFound') + self.volume_client.update_type_encryption.side_effect = ( + sdk_exceptions.NotFoundException('NotFound') ) arglist = [ '--encryption-cipher', @@ -793,21 +779,19 @@ def test_type_set_new_encryption_without_provider(self): str(exc), ) - self.volume_type.set_keys.assert_not_called() - self.volume_type_access_mock.add_project_access.assert_not_called() - body = { - 'cipher': 'aes-xts-plain64', - 'key_size': 128, - 'control_location': 'front-end', - } - self.volume_encryption_types_mock.update.assert_called_with( - self.volume_type, - body, + self.volume_client.update_type_extra_specs.assert_not_called() + self.volume_client.add_type_access.assert_not_called() + self.volume_client.update_type_encryption.assert_called_with( + encryption=None, + volume_type=self.volume_type, + cipher='aes-xts-plain64', + key_size=128, + control_location='front-end', ) - self.volume_encryption_types_mock.create.assert_not_called() + self.volume_client.create_type_encryption.assert_not_called() -class TestTypeShow(TestType): +class TestTypeShow(volume_fakes.TestVolume): columns = ( 'access_project_ids', 'description', @@ -820,19 +804,18 @@ class TestTypeShow(TestType): def setUp(self): super().setUp() - self.volume_type = volume_fakes.create_one_volume_type() + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) self.data = ( None, self.volume_type.description, self.volume_type.id, - True, + self.volume_type.is_public, self.volume_type.name, format_columns.DictColumn(self.volume_type.extra_specs), ) - self.volume_types_mock.get.return_value = self.volume_type + self.volume_client.find_type.return_value = self.volume_type - # Get the command object to test self.cmd = volume_type.ShowVolumeType(self.app, None) def test_type_show(self): @@ -844,7 +827,9 @@ def test_type_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.get.assert_called_with(self.volume_type.id) + self.volume_client.find_type.assert_called_with( + self.volume_type.id, ignore_missing=False + ) self.assertEqual(self.columns, columns) self.assertCountEqual(self.data, data) @@ -854,31 +839,27 @@ def test_type_show_with_access(self): verifylist = [("volume_type", self.volume_type.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - private_type = volume_fakes.create_one_volume_type( - attrs={'is_public': False}, + private_type = sdk_fakes.generate_fake_resource( + _type.Type, is_public=False + ) + type_access_list = { + 'volume_type_id': private_type.id, + 'project_id': 'project-id-test', + } + self.volume_client.find_type.return_value = private_type + self.volume_client.get_type_access.return_value = [type_access_list] + + columns, data = self.cmd.take_action(parsed_args) + self.volume_client.find_type.assert_called_once_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.get_type_access.assert_called_once_with( + private_type.id ) - type_access_list = volume_fakes.create_one_type_access() - with mock.patch.object( - self.volume_types_mock, - 'get', - return_value=private_type, - ): - with mock.patch.object( - self.volume_type_access_mock, - 'list', - return_value=[type_access_list], - ): - columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.get.assert_called_once_with( - self.volume_type.id - ) - self.volume_type_access_mock.list.assert_called_once_with( - private_type.id - ) self.assertEqual(self.columns, columns) private_type_data = ( - format_columns.ListColumn([type_access_list.project_id]), + format_columns.ListColumn([type_access_list['project_id']]), private_type.description, private_type.id, private_type.is_public, @@ -892,22 +873,19 @@ def test_type_show_with_list_access_exec(self): verifylist = [("volume_type", self.volume_type.id)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) - private_type = volume_fakes.create_one_volume_type( - attrs={'is_public': False}, + private_type = sdk_fakes.generate_fake_resource( + _type.Type, is_public=False + ) + self.volume_client.find_type.return_value = private_type + self.volume_client.get_type_access.side_effect = Exception() + + columns, data = self.cmd.take_action(parsed_args) + self.volume_client.find_type.assert_called_once_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.get_type_access.assert_called_once_with( + private_type.id ) - with mock.patch.object( - self.volume_types_mock, 'get', return_value=private_type - ): - with mock.patch.object( - self.volume_type_access_mock, 'list', side_effect=Exception() - ): - columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.get.assert_called_once_with( - self.volume_type.id - ) - self.volume_type_access_mock.list.assert_called_once_with( - private_type.id - ) self.assertEqual(self.columns, columns) private_type_data = ( @@ -921,18 +899,19 @@ def test_type_show_with_list_access_exec(self): self.assertCountEqual(private_type_data, data) def test_type_show_with_encryption(self): - encryption_type = volume_fakes.create_one_encryption_volume_type() - encryption_info = { - 'provider': 'LuksEncryptor', - 'cipher': None, - 'key_size': None, - 'control_location': 'front-end', - } - self.volume_type = volume_fakes.create_one_volume_type( - attrs={'encryption': encryption_info}, + encryption_type = sdk_fakes.generate_fake_resource( + _type.TypeEncryption, ) - self.volume_types_mock.get.return_value = self.volume_type - self.volume_encryption_types_mock.get.return_value = encryption_type + self.volume_type = sdk_fakes.generate_fake_resource(_type.Type) + self.volume_client.find_type.return_value = self.volume_type + self.volume_client.get_type_encryption.return_value = encryption_type + expected_encryption_info = { + 'cipher': encryption_type.cipher, + 'control_location': encryption_type.control_location, + 'encryption_id': encryption_type.encryption_id, + 'key_size': encryption_type.key_size, + 'provider': encryption_type.provider, + } encryption_columns = ( 'access_project_ids', 'description', @@ -945,9 +924,9 @@ def test_type_show_with_encryption(self): encryption_data = ( None, self.volume_type.description, - format_columns.DictColumn(encryption_info), + format_columns.DictColumn(expected_encryption_info), self.volume_type.id, - True, + self.volume_type.is_public, self.volume_type.name, format_columns.DictColumn(self.volume_type.extra_specs), ) @@ -959,29 +938,27 @@ def test_type_show_with_encryption(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_types_mock.get.assert_called_with(self.volume_type.id) - self.volume_encryption_types_mock.get.assert_called_with( + self.volume_client.find_type.assert_called_with( + self.volume_type.id, ignore_missing=False + ) + self.volume_client.get_type_encryption.assert_called_with( self.volume_type.id ) self.assertEqual(encryption_columns, columns) self.assertCountEqual(encryption_data, data) -class TestTypeUnset(TestType): - project = identity_fakes.FakeProject.create_one_project() - volume_type = volume_fakes.create_one_volume_type( - methods={'unset_keys': None}, - ) +class TestTypeUnset(volume_fakes.TestVolume): + volume_type = sdk_fakes.generate_fake_resource(_type.Type) def setUp(self): super().setUp() - self.volume_types_mock.get.return_value = self.volume_type + self.volume_client.find_type.return_value = self.volume_type - # Return a project - self.projects_mock.get.return_value = self.project + self.project = sdk_fakes.generate_fake_resource(_project.Project) + self.identity_sdk_client.find_project.return_value = self.project - # Get the command object to test self.cmd = volume_type.UnsetVolumeType(self.app, None) def test_type_unset(self): @@ -1000,8 +977,8 @@ def test_type_unset(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.volume_type.unset_keys.assert_called_once_with( - ['property', 'multi_property'] + self.volume_client.delete_type_extra_specs.assert_called_once_with( + self.volume_type.id, ['property', 'multi_property'] ) self.assertIsNone(result) @@ -1020,7 +997,7 @@ def test_type_unset_project_access(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_type_access_mock.remove_project_access.assert_called_with( + self.volume_client.remove_type_access.assert_called_with( self.volume_type.id, self.project.id, ) @@ -1041,18 +1018,16 @@ def test_type_unset_not_called_without_project_argument(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_encryption_types_mock.delete.assert_not_called() - self.assertFalse( - self.volume_type_access_mock.remove_project_access.called - ) + self.volume_client.delete_type_encryption.assert_not_called() + self.volume_client.remove_type_access.assert_not_called() def test_type_unset_failed_with_missing_volume_type_argument(self): arglist = [ '--project', - 'identity_fakes.project_id', + 'foo', ] verifylist = [ - ('project', 'identity_fakes.project_id'), + ('project', 'foo'), ] self.assertRaises( @@ -1075,15 +1050,15 @@ def test_type_unset_encryption_type(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) result = self.cmd.take_action(parsed_args) - self.volume_encryption_types_mock.delete.assert_called_with( - self.volume_type + self.volume_client.delete_type_encryption.assert_called_with( + None, self.volume_type.id ) self.assertIsNone(result) -class TestColumns(TestType): +class TestColumns(volume_fakes.TestVolume): def test_encryption_info_column_with_info(self): - fake_volume_type = volume_fakes.create_one_volume_type() + fake_volume_type = sdk_fakes.generate_fake_resource(_type.Type) type_id = fake_volume_type.id encryption_info = { @@ -1101,7 +1076,7 @@ def test_encryption_info_column_with_info(self): self.assertEqual(encryption_info, col.machine_readable()) def test_encryption_info_column_without_info(self): - fake_volume_type = volume_fakes.create_one_volume_type() + fake_volume_type = sdk_fakes.generate_fake_resource(_type.Type) type_id = fake_volume_type.id col = volume_type.EncryptionInfoColumn(type_id, {}) diff --git a/openstackclient/volume/client.py b/openstackclient/volume/client.py index d3a3406a3c..f5a35a7978 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -11,95 +11,36 @@ # WARRANTIES 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 logging +from typing import Any -from osc_lib import exceptions from osc_lib import utils from openstackclient.i18n import _ - LOG = logging.getLogger(__name__) +# global variables used when building the shell DEFAULT_API_VERSION = '3' API_VERSION_OPTION = 'os_volume_api_version' -API_NAME = "volume" -API_VERSIONS = { - "1": "cinderclient.v1.client.Client", - "2": "cinderclient.v2.client.Client", - "3": "cinderclient.v3.client.Client", -} +API_NAME = 'volume' +API_VERSIONS = ('2', '3') -# Save the microversion if in use -_volume_api_version = None - -def make_client(instance): +def make_client(instance: Any) -> Any: """Returns a volume service client.""" - - # Defer client imports until we actually need them - from cinderclient import extension - from cinderclient.v3.contrib import list_extensions - from cinderclient.v3 import volume_snapshots - from cinderclient.v3 import volumes - - # Check whether the available cinderclient supports v1 or v2 - try: - from cinderclient.v1 import services # noqa - except Exception: - del API_VERSIONS['1'] - try: - from cinderclient.v2 import services # noqa - except Exception: - del API_VERSIONS['2'] - - if _volume_api_version is not None: - version = _volume_api_version - else: - version = instance._api_version[API_NAME] - from cinderclient import api_versions - - # convert to APIVersion object - version = api_versions.get_api_version(version) - - if version.ver_major == '1': - # Monkey patch for v1 cinderclient - volumes.Volume.NAME_ATTR = 'display_name' - volume_snapshots.Snapshot.NAME_ATTR = 'display_name' - - volume_client = utils.get_client_class( - API_NAME, version.ver_major, API_VERSIONS + LOG.debug( + 'Volume client initialized using OpenStack SDK: %s', + instance.sdk_connection.block_storage, ) - LOG.debug('Instantiating volume client: %s', volume_client) - - # Set client http_log_debug to True if verbosity level is high enough - http_log_debug = utils.get_effective_log_level() <= logging.DEBUG + return instance.sdk_connection.block_storage - extensions = [extension.Extension('list_extensions', list_extensions)] - - # Remember interface only if it is set - kwargs = utils.build_kwargs_dict('endpoint_type', instance.interface) - - endpoint_override = instance.sdk_connection.config.get_endpoint( - 'block-storage' - ) - client = volume_client( - session=instance.session, - extensions=extensions, - http_log_debug=http_log_debug, - region_name=instance.region_name, - endpoint_override=endpoint_override, - api_version=version, - **kwargs, - ) - - return client - - -def build_option_parser(parser): +def build_option_parser( + parser: argparse.ArgumentParser, +) -> argparse.ArgumentParser: """Hook to add global options""" parser.add_argument( '--os-volume-api-version', @@ -111,40 +52,6 @@ def build_option_parser(parser): return parser -def check_api_version(check_version): - """Validate version supplied by user - - Returns: - - * True if version is OK - * False if the version has not been checked and the previous plugin - check should be performed - * throws an exception if the version is no good - """ - - # Defer client imports until we actually need them - from cinderclient import api_versions - - global _volume_api_version - - # Copy some logic from novaclient 3.3.0 for basic version detection - # NOTE(dtroyer): This is only enough to resume operations using API - # version 3.0 or any valid version supplied by the user. - _volume_api_version = api_versions.get_api_version(check_version) - - # Bypass X.latest format microversion - if not _volume_api_version.is_latest(): - if _volume_api_version > api_versions.APIVersion("3.0"): - if not _volume_api_version.matches( - api_versions.MIN_VERSION, - api_versions.MAX_VERSION, - ): - msg = _("versions supported by client: %(min)s - %(max)s") % { - "min": api_versions.MIN_VERSION, - "max": api_versions.MAX_VERSION, - } - raise exceptions.CommandError(msg) - - return True - - return False +def check_api_version(check_version: str) -> bool: + # SDK supports auto-negotiation for us: always return True + return True diff --git a/openstackclient/volume/v2/backup_record.py b/openstackclient/volume/v2/backup_record.py index 98f7c7171a..bc25d99e7c 100644 --- a/openstackclient/volume/v2/backup_record.py +++ b/openstackclient/volume/v2/backup_record.py @@ -14,11 +14,14 @@ """Volume v2 Backup action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command -from osc_lib import utils +from openstack import utils as sdk_utils +from openstackclient import command from openstackclient.i18n import _ @@ -33,7 +36,7 @@ class ExportBackupRecord(command.ShowOne): restore.""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "backup", @@ -42,10 +45,16 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - backup = utils.find_resource(volume_client.backups, parsed_args.backup) - backup_data = volume_client.backups.export_record(backup.id) + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + backup = volume_client.find_backup( + parsed_args.backup, ignore_missing=False + ) + backup_data = volume_client.export_backup(backup)['backup-record'] # We only want to show "friendly" display names, but also want to keep # json structure compatibility with cinderclient @@ -53,7 +62,8 @@ def take_action(self, parsed_args): backup_data['Backup Service'] = backup_data.pop('backup_service') backup_data['Metadata'] = backup_data.pop('backup_url') - return zip(*sorted(backup_data.items())) + col_headers, col_data = zip(*sorted(backup_data.items())) + return col_headers, col_data class ImportBackupRecord(command.ShowOne): @@ -64,7 +74,7 @@ class ImportBackupRecord(command.ShowOne): rebuilt service instance""" ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "backup_service", @@ -78,10 +88,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - backup_data = volume_client.backups.import_record( + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + backup_data = volume_client.import_backup( parsed_args.backup_service, parsed_args.backup_metadata ) backup_data.pop('links', None) - return zip(*sorted(backup_data.items())) + col_headers, col_data = zip(*sorted(backup_data.items())) + return col_headers, col_data diff --git a/openstackclient/volume/v2/consistency_group.py b/openstackclient/volume/v2/consistency_group.py index 347724f925..08bae165ff 100644 --- a/openstackclient/volume/v2/consistency_group.py +++ b/openstackclient/volume/v2/consistency_group.py @@ -16,39 +16,63 @@ import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any +from openstack.block_storage import v2 as block_storage_v2 +from openstack.block_storage.v2 import consistency_group as _consistency_group +from openstack import utils as sdk_utils from osc_lib.cli import format_columns -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -def _find_volumes(parsed_args_volumes, volume_client): +def _find_volumes( + parsed_args_volumes: list[str], volume_client: block_storage_v2.Proxy +) -> tuple[int, str]: result = 0 uuid = '' for volume in parsed_args_volumes: try: - volume_id = utils.find_resource(volume_client.volumes, volume).id + volume_id = volume_client.find_volume( + volume, ignore_missing=False + ).id uuid += volume_id + ',' except Exception as e: result += 1 LOG.error( - _("Failed to find volume with name or ID '%(volume)s':%(e)s") - % {'volume': volume, 'e': e} + _("Failed to find volume with name or ID '%(volume)s':%(e)s"), + {'volume': volume, 'e': e}, ) return result, uuid +def _format_consistency_group( + consistency_group: _consistency_group.ConsistencyGroup, +) -> tuple[Sequence[str], Iterable[Any]]: + columns = ( + 'availability_zone', + 'created_at', + 'description', + 'id', + 'name', + 'status', + 'volume_types', + ) + return columns, utils.get_item_properties(consistency_group, columns) + + class AddVolumeToConsistencyGroup(command.Command): _description = _("Add volume(s) to consistency group") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'consistency_group', @@ -66,31 +90,33 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) result, add_uuid = _find_volumes(parsed_args.volumes, volume_client) if result > 0: total = len(parsed_args.volumes) LOG.error( - _("%(result)s of %(total)s volumes failed to add.") - % {'result': result, 'total': total} + _("%(result)s of %(total)s volumes failed to add."), + {'result': result, 'total': total}, ) if add_uuid: add_uuid = add_uuid.rstrip(',') - consistency_group_id = utils.find_resource( - volume_client.consistencygroups, parsed_args.consistency_group - ).id - volume_client.consistencygroups.update( - consistency_group_id, add_volumes=add_uuid + consistency_group = volume_client.find_consistency_group( + parsed_args.consistency_group, ignore_missing=False + ) + volume_client.update_consistency_group( + consistency_group, add_volumes=add_uuid ) class CreateConsistencyGroup(command.ShowOne): _description = _("Create new consistency group.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "name", @@ -144,14 +170,18 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) if parsed_args.volume_type: - volume_type_id = utils.find_resource( - volume_client.volume_types, parsed_args.volume_type + volume_type_id = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False ).id - consistency_group = volume_client.consistencygroups.create( - volume_type_id, + consistency_group = volume_client.create_consistency_group( + volume_types=volume_type_id, name=parsed_args.name, description=parsed_args.description, availability_zone=parsed_args.availability_zone, @@ -165,34 +195,34 @@ def take_action(self, parsed_args): LOG.warning(msg) consistency_group_id = None - consistency_group_snapshot = None + consistency_group_snapshot_id = None if parsed_args.source: - consistency_group_id = utils.find_resource( - volume_client.consistencygroups, - parsed_args.source, + consistency_group_id = volume_client.find_consistency_group( + parsed_args.source, ignore_missing=False ).id elif parsed_args.snapshot: - consistency_group_snapshot = utils.find_resource( - volume_client.cgsnapshots, - parsed_args.snapshot, - ).id + consistency_group_snapshot_id = ( + volume_client.find_consistency_group_snapshot( + parsed_args.snapshot, ignore_missing=False + ).id + ) consistency_group = ( - volume_client.consistencygroups.create_from_src( - consistency_group_snapshot, - consistency_group_id, + volume_client.create_consistency_group_from_source( + consistency_group_snapshot=consistency_group_snapshot_id, + consistency_group=consistency_group_id, name=parsed_args.name, description=parsed_args.description, ) ) - return zip(*sorted(consistency_group._info.items())) + return _format_consistency_group(consistency_group) class DeleteConsistencyGroup(command.Command): _description = _("Delete consistency group(s).") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'consistency_groups', @@ -208,17 +238,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) result = 0 for i in parsed_args.consistency_groups: try: - consistency_group_id = utils.find_resource( - volume_client.consistencygroups, i - ).id - volume_client.consistencygroups.delete( - consistency_group_id, parsed_args.force + consistency_group = volume_client.find_consistency_group( + i, ignore_missing=False + ) + volume_client.delete_consistency_group( + consistency_group, force=parsed_args.force ) except Exception as e: result += 1 @@ -226,8 +258,8 @@ def take_action(self, parsed_args): _( "Failed to delete consistency group with " "name or ID '%(consistency_group)s':%(e)s" - ) - % {'consistency_group': i, 'e': e} + ), + {'consistency_group': i, 'e': e}, ) if result > 0: @@ -241,7 +273,7 @@ def take_action(self, parsed_args): class ListConsistencyGroup(command.Lister): _description = _("List consistency groups.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--all-projects', @@ -258,9 +290,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + if parsed_args.long: - columns = [ + column_headers = [ 'ID', 'Status', 'Availability Zone', @@ -268,21 +306,29 @@ def take_action(self, parsed_args): 'Description', 'Volume Types', ] + columns = [ + 'id', + 'status', + 'availability_zone', + 'name', + 'description', + 'volume_types', + ] else: - columns = ['ID', 'Status', 'Name'] - volume_client = self.app.client_manager.volume - consistency_groups = volume_client.consistencygroups.list( - detailed=True, - search_opts={'all_tenants': parsed_args.all_projects}, + column_headers = ['ID', 'Status', 'Name'] + columns = ['id', 'status', 'name'] + + consistency_groups = volume_client.consistency_groups( + all_tenants=parsed_args.all_projects, ) return ( - columns, + column_headers, ( utils.get_item_properties( s, columns, - formatters={'Volume Types': format_columns.ListColumn}, + formatters={'volume_types': format_columns.ListColumn}, ) for s in consistency_groups ), @@ -292,7 +338,7 @@ def take_action(self, parsed_args): class RemoveVolumeFromConsistencyGroup(command.Command): _description = _("Remove volume(s) from consistency group") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'consistency_group', @@ -310,31 +356,33 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) result, remove_uuid = _find_volumes(parsed_args.volumes, volume_client) if result > 0: total = len(parsed_args.volumes) LOG.error( - _("%(result)s of %(total)s volumes failed to remove.") - % {'result': result, 'total': total} + _("%(result)s of %(total)s volumes failed to remove."), + {'result': result, 'total': total}, ) if remove_uuid: remove_uuid = remove_uuid.rstrip(',') - consistency_group_id = utils.find_resource( - volume_client.consistencygroups, parsed_args.consistency_group - ).id - volume_client.consistencygroups.update( - consistency_group_id, remove_volumes=remove_uuid + consistency_group = volume_client.find_consistency_group( + parsed_args.consistency_group, ignore_missing=False + ) + volume_client.update_consistency_group( + consistency_group, remove_volumes=remove_uuid ) class SetConsistencyGroup(command.Command): _description = _("Set consistency group properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'consistency_group', @@ -353,26 +401,26 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) kwargs = {} if parsed_args.name: kwargs['name'] = parsed_args.name if parsed_args.description: kwargs['description'] = parsed_args.description if kwargs: - consistency_group_id = utils.find_resource( - volume_client.consistencygroups, parsed_args.consistency_group - ).id - volume_client.consistencygroups.update( - consistency_group_id, **kwargs + consistency_group = volume_client.find_consistency_group( + parsed_args.consistency_group, ignore_missing=False ) + volume_client.update_consistency_group(consistency_group, **kwargs) class ShowConsistencyGroup(command.ShowOne): _description = _("Display consistency group details.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "consistency_group", @@ -381,9 +429,13 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - consistency_group = utils.find_resource( - volume_client.consistencygroups, parsed_args.consistency_group + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + consistency_group = volume_client.find_consistency_group( + parsed_args.consistency_group, ignore_missing=False ) - return zip(*sorted(consistency_group._info.items())) + return _format_consistency_group(consistency_group) diff --git a/openstackclient/volume/v2/consistency_group_snapshot.py b/openstackclient/volume/v2/consistency_group_snapshot.py index 3c582d6b1c..7f35a079e5 100644 --- a/openstackclient/volume/v2/consistency_group_snapshot.py +++ b/openstackclient/volume/v2/consistency_group_snapshot.py @@ -14,22 +14,45 @@ """Volume v2 consistency group snapshot action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack.block_storage.v2 import ( + consistency_group_snapshot as _cg_snapshot, +) +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) +def _format_consistency_group_snapshot( + consistency_group_snapshot: _cg_snapshot.ConsistencyGroupSnapshot, +) -> tuple[Sequence[str], Iterable[Any]]: + columns = ( + 'consistencygroup_id', + 'created_at', + 'description', + 'id', + 'name', + 'status', + ) + return columns, utils.get_item_properties( + consistency_group_snapshot, columns + ) + + class CreateConsistencyGroupSnapshot(command.ShowOne): _description = _("Create new consistency group snapshot.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "snapshot_name", @@ -52,29 +75,35 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) consistency_group = parsed_args.consistency_group if not parsed_args.consistency_group: # If "--consistency-group" not specified, then consistency_group # will be the same as the new consistency group snapshot name consistency_group = parsed_args.snapshot_name - consistency_group_id = utils.find_resource( - volume_client.consistencygroups, consistency_group + consistency_group_id = volume_client.find_consistency_group( + consistency_group, ignore_missing=False ).id - consistency_group_snapshot = volume_client.cgsnapshots.create( - consistency_group_id, - name=parsed_args.snapshot_name, - description=parsed_args.description, + consistency_group_snapshot = ( + volume_client.create_consistency_group_snapshot( + consistencygroup_id=consistency_group_id, + name=parsed_args.snapshot_name, + description=parsed_args.description, + ) ) - return zip(*sorted(consistency_group_snapshot._info.items())) + return _format_consistency_group_snapshot(consistency_group_snapshot) class DeleteConsistencyGroupSnapshot(command.Command): _description = _("Delete consistency group snapshot(s).") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "consistency_group_snapshot", @@ -84,25 +113,30 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) result = 0 for snapshot in parsed_args.consistency_group_snapshot: try: - snapshot_id = utils.find_resource( - volume_client.cgsnapshots, snapshot - ).id - - volume_client.cgsnapshots.delete(snapshot_id) + consistency_group_snapshot = ( + volume_client.find_consistency_group_snapshot( + snapshot, ignore_missing=False + ) + ) + volume_client.delete_consistency_group_snapshot( + consistency_group_snapshot + ) except Exception as e: result += 1 LOG.error( _( "Failed to delete consistency group snapshot " "with name or ID '%(snapshot)s': %(e)s" - ) - % {'snapshot': snapshot, 'e': e} + ), + {'snapshot': snapshot, 'e': e}, ) if result > 0: @@ -117,7 +151,7 @@ def take_action(self, parsed_args): class ListConsistencyGroupSnapshot(command.Lister): _description = _("List consistency group snapshots.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--all-projects', @@ -153,9 +187,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + if parsed_args.long: - columns = [ + column_headers = [ 'ID', 'Status', 'ConsistencyGroup ID', @@ -163,27 +203,34 @@ def take_action(self, parsed_args): 'Description', 'Created At', ] + columns = [ + 'id', + 'status', + 'consistencygroup_id', + 'name', + 'description', + 'created_at', + ] else: - columns = ['ID', 'Status', 'Name'] - volume_client = self.app.client_manager.volume + column_headers = ['ID', 'Status', 'Name'] + columns = ['id', 'status', 'name'] + consistency_group_id = None if parsed_args.consistency_group: - consistency_group_id = utils.find_resource( - volume_client.consistencygroups, - parsed_args.consistency_group, + consistency_group_id = volume_client.find_consistency_group( + parsed_args.consistency_group, ignore_missing=False ).id - search_opts = { - 'all_tenants': parsed_args.all_projects, - 'status': parsed_args.status, - 'consistencygroup_id': consistency_group_id, - } - consistency_group_snapshots = volume_client.cgsnapshots.list( - detailed=True, - search_opts=search_opts, + + consistency_group_snapshots = ( + volume_client.consistency_group_snapshots( + all_tenants=parsed_args.all_projects, + status=parsed_args.status, + consistencygroup_id=consistency_group_id, + ) ) return ( - columns, + column_headers, ( utils.get_item_properties(s, columns) for s in consistency_group_snapshots @@ -194,7 +241,7 @@ def take_action(self, parsed_args): class ShowConsistencyGroupSnapshot(command.ShowOne): _description = _("Display consistency group snapshot details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "consistency_group_snapshot", @@ -203,9 +250,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - consistency_group_snapshot = utils.find_resource( - volume_client.cgsnapshots, parsed_args.consistency_group_snapshot + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + consistency_group_snapshot = ( + volume_client.find_consistency_group_snapshot( + parsed_args.consistency_group_snapshot, ignore_missing=False + ) ) - return zip(*sorted(consistency_group_snapshot._info.items())) + return _format_consistency_group_snapshot(consistency_group_snapshot) diff --git a/openstackclient/volume/v2/qos_specs.py b/openstackclient/volume/v2/qos_specs.py index 72706a7409..fd5fc50990 100644 --- a/openstackclient/volume/v2/qos_specs.py +++ b/openstackclient/volume/v2/qos_specs.py @@ -15,14 +15,18 @@ """Volume v2 QoS action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -32,7 +36,7 @@ class AssociateQos(command.Command): _description = _("Associate a QoS specification to a volume type") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'qos_spec', @@ -46,22 +50,23 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - qos_spec = utils.find_resource( - volume_client.qos_specs, parsed_args.qos_spec + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' ) - volume_type = utils.find_resource( - volume_client.volume_types, parsed_args.volume_type + qos_spec = volume_client.find_qos_spec( + parsed_args.qos_spec, ignore_missing=False ) - - volume_client.qos_specs.associate(qos_spec.id, volume_type.id) + volume_type = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False + ) + volume_client.associate_qos_spec(qos_spec.id, volume_type.id) class CreateQos(command.ShowOne): _description = _("Create new QoS specification") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -85,6 +90,7 @@ def get_parser(self, prog_name): parser.add_argument( '--property', metavar='', + dest='properties', action=parseractions.KeyValueAction, help=_( 'Set a QoS specification property ' @@ -93,30 +99,32 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - specs = {} - specs.update({'consumer': parsed_args.consumer}) - - if parsed_args.property: - specs.update(parsed_args.property) - - qos_spec = volume_client.qos_specs.create(parsed_args.name, specs) - - qos_spec._info.update( - { - 'properties': format_columns.DictColumn( - qos_spec._info.pop('specs') - ) - } + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + attrs: dict[str, Any] = {'consumer': parsed_args.consumer} + if parsed_args.properties: + attrs.update(parsed_args.properties) + qos_spec = volume_client.create_qos_spec( + name=parsed_args.name, **attrs + ) + columns = ('consumer', 'id', 'name', 'properties') + data = ( + qos_spec.consumer, + qos_spec.id, + qos_spec.name, + format_columns.DictColumn(qos_spec.specs), ) - return zip(*sorted(qos_spec._info.items())) + return columns, data class DeleteQos(command.Command): _description = _("Delete QoS specification") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'qos_specs', @@ -132,22 +140,26 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) result = 0 for i in parsed_args.qos_specs: try: - qos_spec = utils.find_resource(volume_client.qos_specs, i) - volume_client.qos_specs.delete(qos_spec.id, parsed_args.force) + qos_spec = volume_client.find_qos_spec(i, ignore_missing=False) + volume_client.delete_qos_spec( + qos_spec.id, ignore_missing=False, force=parsed_args.force + ) except Exception as e: result += 1 LOG.error( _( "Failed to delete QoS specification with " "name or ID '%(qos)s': %(e)s" - ) - % {'qos': i, 'e': e} + ), + {'qos': i, 'e': e}, ) if result > 0: @@ -161,7 +173,7 @@ def take_action(self, parsed_args): class DisassociateQos(command.Command): _description = _("Disassociate a QoS specification from a volume type") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'qos_spec', @@ -183,43 +195,33 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - qos_spec = utils.find_resource( - volume_client.qos_specs, parsed_args.qos_spec + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + qos_spec = volume_client.find_qos_spec( + parsed_args.qos_spec, ignore_missing=False ) if parsed_args.volume_type: - volume_type = utils.find_resource( - volume_client.volume_types, parsed_args.volume_type + volume_type = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False ) - volume_client.qos_specs.disassociate(qos_spec.id, volume_type.id) + volume_client.disassociate_qos_spec(qos_spec.id, volume_type.id) elif parsed_args.all: - volume_client.qos_specs.disassociate_all(qos_spec.id) + volume_client.disassociate_all_qos_spec(qos_spec.id) class ListQos(command.Lister): _description = _("List QoS specifications") - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - qos_specs_list = volume_client.qos_specs.list() - - for qos in qos_specs_list: - try: - qos_associations = volume_client.qos_specs.get_associations( - qos, - ) - if qos_associations: - associations = [ - association.name for association in qos_associations - ] - qos._info.update({'associations': associations}) - except Exception as ex: - if type(ex).__name__ == 'NotFound': - qos._info.update({'associations': None}) - else: - raise + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + qos_specs_list = list(volume_client.qos_specs()) display_columns = ( 'ID', @@ -229,27 +231,27 @@ def take_action(self, parsed_args): 'Properties', ) - columns = ('ID', 'Name', 'Consumer', 'Associations', 'Specs') - return ( - display_columns, - ( - utils.get_dict_properties( - s._info, - columns, - formatters={ - 'Specs': format_columns.DictColumn, - 'Associations': format_columns.ListColumn, - }, + data = [] + for qos in qos_specs_list: + qos_associations = volume_client.qos_spec_associations(qos) + associations = [a.name for a in qos_associations] + data.append( + ( + qos.id, + qos.name, + qos.consumer, + format_columns.ListColumn(associations), + format_columns.DictColumn(qos.specs), ) - for s in qos_specs_list - ), - ) + ) + + return display_columns, iter(data) class SetQos(command.Command): _description = _("Set QoS specification properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'qos_spec', @@ -269,6 +271,7 @@ def get_parser(self, prog_name): parser.add_argument( '--property', metavar='', + dest='properties', action=parseractions.KeyValueAction, help=_( 'Property to add or modify for this QoS specification ' @@ -277,26 +280,27 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - qos_spec = utils.find_resource( - volume_client.qos_specs, parsed_args.qos_spec + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + qos_spec = volume_client.find_qos_spec( + parsed_args.qos_spec, ignore_missing=False ) result = 0 if parsed_args.no_property: try: - key_list = list(qos_spec._info['specs'].keys()) - volume_client.qos_specs.unset_keys(qos_spec.id, key_list) + key_list = list(qos_spec.specs.keys()) + volume_client.delete_qos_spec_metadata(qos_spec.id, key_list) except Exception as e: LOG.error(_("Failed to clean qos properties: %s"), e) result += 1 - if parsed_args.property: + if parsed_args.properties: try: - volume_client.qos_specs.set_keys( - qos_spec.id, - parsed_args.property, + volume_client.update_qos_spec( + qos_spec.id, **parsed_args.properties ) except Exception as e: LOG.error(_("Failed to set qos property: %s"), e) @@ -311,7 +315,7 @@ def take_action(self, parsed_args): class ShowQos(command.ShowOne): _description = _("Display QoS specification details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'qos_spec', @@ -320,35 +324,47 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - qos_spec = utils.find_resource( - volume_client.qos_specs, parsed_args.qos_spec + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' ) - - qos_associations = volume_client.qos_specs.get_associations(qos_spec) - if qos_associations: - associations = [ - association.name for association in qos_associations - ] - qos_spec._info.update( - {'associations': format_columns.ListColumn(associations)} - ) - qos_spec._info.update( - { - 'properties': format_columns.DictColumn( - qos_spec._info.pop('specs') - ) - } + qos_spec = volume_client.find_qos_spec( + parsed_args.qos_spec, ignore_missing=False ) - - return zip(*sorted(qos_spec._info.items())) + qos_associations = list(volume_client.qos_spec_associations(qos_spec)) + associations = [a.name for a in qos_associations] + if associations: + columns: tuple[str, ...] = ( + 'associations', + 'consumer', + 'id', + 'name', + 'properties', + ) + data: tuple[Any, ...] = ( + format_columns.ListColumn(associations), + qos_spec.consumer, + qos_spec.id, + qos_spec.name, + format_columns.DictColumn(qos_spec.specs), + ) + else: + columns = ('consumer', 'id', 'name', 'properties') + data = ( + qos_spec.consumer, + qos_spec.id, + qos_spec.name, + format_columns.DictColumn(qos_spec.specs), + ) + return columns, data class UnsetQos(command.Command): _description = _("Unset QoS specification properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'qos_spec', @@ -358,6 +374,7 @@ def get_parser(self, prog_name): parser.add_argument( '--property', metavar='', + dest='properties', action='append', default=[], help=_( @@ -367,13 +384,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - qos_spec = utils.find_resource( - volume_client.qos_specs, parsed_args.qos_spec + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' ) - - if parsed_args.property: - volume_client.qos_specs.unset_keys( - qos_spec.id, parsed_args.property + qos_spec = volume_client.find_qos_spec( + parsed_args.qos_spec, ignore_missing=False + ) + if parsed_args.properties: + volume_client.delete_qos_spec_metadata( + qos_spec.id, parsed_args.properties ) diff --git a/openstackclient/volume/v2/service.py b/openstackclient/volume/v2/service.py index 2a33fc0b9a..fdb42f796c 100644 --- a/openstackclient/volume/v2/service.py +++ b/openstackclient/volume/v2/service.py @@ -14,17 +14,22 @@ """Service action implementations""" -from osc_lib.command import command +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ class ListService(command.Lister): _description = _("List service command") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--host", @@ -44,34 +49,39 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - service_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + + columns: tuple[str, ...] = ( + "binary", + "host", + "availability_zone", + "status", + "state", + "updated_at", + ) + column_names: tuple[str, ...] = ( + "Binary", + "Host", + "Zone", + "Status", + "State", + "Updated At", + ) if parsed_args.long: - columns = [ - "Binary", - "Host", - "Zone", - "Status", - "State", - "Updated At", - "Disabled Reason", - ] - else: - columns = [ - "Binary", - "Host", - "Zone", - "Status", - "State", - "Updated At", - ] - - data = service_client.services.list( - parsed_args.host, parsed_args.service + columns += ("disabled_reason",) + column_names += ("Disabled Reason",) + + data = volume_client.services( + host=parsed_args.host, binary=parsed_args.service ) return ( - columns, + column_names, ( utils.get_item_properties( s, @@ -85,9 +95,13 @@ def take_action(self, parsed_args): class SetService(command.Command): _description = _("Set volume service properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) - parser.add_argument("host", metavar="", help=_("Name of host")) + parser.add_argument( + "host", + metavar="", + help=_("Name of host"), + ) parser.add_argument( "service", metavar="", @@ -110,7 +124,7 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): + def take_action(self, parsed_args: argparse.Namespace) -> None: if parsed_args.disable_reason and not parsed_args.disable: msg = _( "Cannot specify option --disable-reason without " @@ -118,19 +132,19 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - service_client = self.app.client_manager.volume + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + + service = volume_client.find_service( + parsed_args.service, ignore_missing=False, host=parsed_args.host + ) + if parsed_args.enable: - service_client.services.enable( - parsed_args.host, parsed_args.service - ) + service.enable(volume_client) + if parsed_args.disable: - if parsed_args.disable_reason: - service_client.services.disable_log_reason( - parsed_args.host, - parsed_args.service, - parsed_args.disable_reason, - ) - else: - service_client.services.disable( - parsed_args.host, parsed_args.service - ) + service.disable( + volume_client, + reason=parsed_args.disable_reason, + ) diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 193e82f013..c71884dd70 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -15,18 +15,22 @@ """Volume V2 Volume action implementations""" import argparse -import copy import functools import logging +from collections.abc import Iterable, Sequence +from typing import Any from cliff import columns as cliff_columns +from openstack.block_storage.v2 import volume as _volume from openstack import exceptions as sdk_exceptions +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient.api import volume_v2 +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -40,14 +44,20 @@ class KeyValueHintAction(argparse.Action): APPEND_KEYS = ('same_host', 'different_host') - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: self._key_value_action = parseractions.KeyValueAction(*args, **kwargs) self._key_value_append_action = parseractions.KeyValueAppendAction( *args, **kwargs ) super().__init__(*args, **kwargs) - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Any, + option_string: str | None = None, + ) -> None: if values.startswith(self.APPEND_KEYS): self._key_value_append_action( parser, namespace, values, option_string=option_string @@ -58,7 +68,7 @@ def __call__(self, parser, namespace, values, option_string=None): ) -class AttachmentsColumn(cliff_columns.FormattableColumn): +class AttachmentsColumn(cliff_columns.FormattableColumn[list[Any]]): """Formattable column for attachments column. Unlike the parent FormattableColumn class, the initializer of the @@ -69,11 +79,13 @@ class takes server_cache as the second argument. ``functools.partial(AttachmentsColumn, server_cache)``. """ - def __init__(self, value, server_cache=None): + def __init__( + self, value: list[Any], server_cache: dict[str, Any] | None = None + ) -> None: super().__init__(value) self._server_cache = server_cache or {} - def human_readable(self): + def human_readable(self) -> str: """Return a formatted string of a volume's attached instances :rtype: a string of formatted instances @@ -89,11 +101,52 @@ def human_readable(self): return msg +def _format_volume(volume: _volume.Volume) -> dict[str, object]: + # Some columns returned by openstacksdk should not be shown because they're + # either irrelevant or duplicates + ignored_columns = { + # computed columns + 'location', + # create-only columns + 'OS-SCH-HNT:scheduler_hints', + 'imageRef', + # unnecessary columns + 'links', + } + optional_columns = { + # only present if part of a consistency group + 'consistencygroup_id', + # only present if there are image properties associated + 'volume_image_metadata', + } + + info = volume.to_dict(original_names=True) + data = {} + for key, value in info.items(): + if key in ignored_columns: + continue + + if key in optional_columns: + if info[key] is None: + continue + + data[key] = value + + data.update( + { + 'properties': format_columns.DictColumn(data.pop('metadata')), + 'type': data.pop('volume_type'), + } + ) + + return data + + class CreateVolume(command.ShowOne): _description = _("Create new volume") @staticmethod - def _check_size_arg(args): + def _check_size_arg(args: argparse.Namespace) -> None: """Check whether --size option is required or not. Require size parameter only in case when snapshot or source @@ -107,7 +160,7 @@ def _check_size_arg(args): ) raise exceptions.CommandError(msg) - def _get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "name", @@ -216,36 +269,36 @@ def _get_parser(self, prog_name): default=None, help=_("Set volume to read-write access mode (default)"), ) - return parser, source_group - - def get_parser(self, prog_name): - parser, _ = self._get_parser(prog_name) return parser - def take_action(self, parsed_args): - CreateVolume._check_size_arg(parsed_args) + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + self._check_size_arg(parsed_args) # size is validated in the above call to # _check_size_arg where we check that size # should be passed if we are not creating a # volume from snapshot or source volume size = parsed_args.size - volume_client = self.app.client_manager.volume + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) image_client = self.app.client_manager.image source_volume = None if parsed_args.source: - source_volume_obj = utils.find_resource( - volume_client.volumes, parsed_args.source + source_volume_obj = volume_client.find_volume( + parsed_args.source, ignore_missing=False ) source_volume = source_volume_obj.id size = max(size or 0, source_volume_obj.size) consistency_group = None if parsed_args.consistency_group: - consistency_group = utils.find_resource( - volume_client.consistencygroups, parsed_args.consistency_group - ).id + consistency_group = volume_v2.find_consistency_group( + volume_client, parsed_args.consistency_group + )['id'] image = None if parsed_args.image: @@ -255,8 +308,8 @@ def take_action(self, parsed_args): snapshot = None if parsed_args.snapshot: - snapshot_obj = utils.find_resource( - volume_client.volume_snapshots, parsed_args.snapshot + snapshot_obj = volume_client.find_snapshot( + parsed_args.snapshot, ignore_missing=False ) snapshot = snapshot_obj.id # Cinder requires a value for size when creating a volume @@ -267,7 +320,7 @@ def take_action(self, parsed_args): # snapshot size. size = max(size or 0, snapshot_obj.size) - volume = volume_client.volumes.create( + volume = volume_client.create_volume( size=size, snapshot_id=snapshot, name=parsed_args.name, @@ -275,23 +328,23 @@ def take_action(self, parsed_args): volume_type=parsed_args.type, availability_zone=parsed_args.availability_zone, metadata=parsed_args.properties, - imageRef=image, - source_volid=source_volume, - consistencygroup_id=consistency_group, + image_id=image, + source_volume_id=source_volume, + consistency_group_id=consistency_group, scheduler_hints=parsed_args.hint, ) if parsed_args.bootable is not None: try: if utils.wait_for_status( - volume_client.volumes.get, + volume_client.get_volume, volume.id, success_status=['available'], error_status=['error'], sleep_time=1, ): - volume_client.volumes.set_bootable( - volume.id, parsed_args.bootable + volume_client.set_volume_bootable_status( + volume, parsed_args.bootable ) else: msg = _( @@ -304,14 +357,14 @@ def take_action(self, parsed_args): if parsed_args.read_only is not None: try: if utils.wait_for_status( - volume_client.volumes.get, + volume_client.get_volume, volume.id, success_status=['available'], error_status=['error'], sleep_time=1, ): - volume_client.volumes.update_readonly_flag( - volume.id, parsed_args.read_only + volume_client.set_volume_readonly( + volume, parsed_args.read_only ) else: msg = _( @@ -325,23 +378,15 @@ def take_action(self, parsed_args): e, ) - # Remove key links from being displayed - volume._info.update( - { - 'properties': format_columns.DictColumn( - volume._info.pop('metadata') - ), - 'type': volume._info.pop('volume_type'), - } - ) - volume._info.pop("links", None) - return zip(*sorted(volume._info.items())) + data = _format_volume(volume) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class DeleteVolume(command.Command): _description = _("Delete volume(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "volumes", @@ -359,27 +404,37 @@ def get_parser(self, prog_name): ), ) group.add_argument( - "--purge", + "--cascade", action="store_true", help=_( "Remove any snapshots along with volume(s) (defaults to False)" ), ) + group.add_argument( + # now called "cascade", accept old arg for compatibility + "--purge", + action="store_true", + help=argparse.SUPPRESS, + dest='cascade', + ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) result = 0 - for i in parsed_args.volumes: + for volume in parsed_args.volumes: try: - volume_obj = utils.find_resource(volume_client.volumes, i) - if parsed_args.force: - volume_client.volumes.force_delete(volume_obj.id) - else: - volume_client.volumes.delete( - volume_obj.id, cascade=parsed_args.purge - ) + volume_obj = volume_client.find_volume( + volume, ignore_missing=False + ) + volume_client.delete_volume( + volume_obj.id, + force=parsed_args.force, + cascade=parsed_args.cascade, + ) except Exception as e: result += 1 LOG.error( @@ -387,7 +442,7 @@ def take_action(self, parsed_args): "Failed to delete volume with " "name or ID '%(volume)s': %(e)s" ), - {'volume': i, 'e': e}, + {'volume': volume, 'e': e}, ) if result > 0: @@ -402,7 +457,7 @@ def take_action(self, parsed_args): class ListVolume(command.Lister): _description = _("List volumes") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--project', @@ -438,69 +493,65 @@ def get_parser(self, prog_name): default=False, help=_('List additional fields in output'), ) - pagination.add_marker_pagination_option_to_parser(parser) + pagination.add_marker_pagination_option_to_parser( + parser, include_max_items=False + ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + # we don't narrow types here since we want to handle both v2.0 and v3 + identity_client = self.app.client_manager.sdk_connection.identity + columns: tuple[str, ...] = ('id', 'name', 'status', 'size') + column_headers: tuple[str, ...] = ('ID', 'Name', 'Status', 'Size') if parsed_args.long: - columns = [ - 'ID', - 'Name', - 'Status', - 'Size', - 'Volume Type', - 'Bootable', - 'Attachments', - 'Metadata', - ] - column_headers = copy.deepcopy(columns) - column_headers[4] = 'Type' - column_headers[6] = 'Attached to' - column_headers[7] = 'Properties' - else: - columns = [ - 'ID', - 'Name', - 'Status', - 'Size', - 'Attachments', - ] - column_headers = copy.deepcopy(columns) - column_headers[4] = 'Attached to' - - project_id = None + columns += ('volume_type', 'is_bootable') + column_headers += ('Type', 'Bootable') + columns += ('attachments',) + column_headers += ('Attached to',) + if parsed_args.long: + columns += ('metadata',) + column_headers += ('Properties',) + + kwargs = {} + + if parsed_args.name: + kwargs['name'] = parsed_args.name + + if parsed_args.status: + kwargs['status'] = parsed_args.status + + if parsed_args.limit: + kwargs['limit'] = parsed_args.limit + + if parsed_args.marker: + kwargs['marker'] = parsed_args.marker + if parsed_args.project: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) + kwargs['project_id'] = project_id user_id = None if parsed_args.user: - user_id = identity_common.find_user( + user_id = identity_common.find_user_id_sdk( identity_client, parsed_args.user, parsed_args.user_domain - ).id + ) + kwargs['user_id'] = user_id # set value of 'all_tenants' when using project option all_projects = bool(parsed_args.project) or parsed_args.all_projects + kwargs['all_projects'] = all_projects - search_opts = { - 'all_tenants': all_projects, - 'project_id': project_id, - 'user_id': user_id, - 'name': parsed_args.name, - 'status': parsed_args.status, - } - - data = volume_client.volumes.list( - search_opts=search_opts, - marker=parsed_args.marker, - limit=parsed_args.limit, - ) + data = list(volume_client.volumes(**kwargs)) do_server_list = False @@ -516,17 +567,14 @@ def take_action(self, parsed_args): compute_client = self.app.client_manager.compute for s in compute_client.servers(): server_cache[s.id] = s - except sdk_exceptions.SDKException: # noqa: S110 + except sdk_exceptions.SDKException: # Just forget it if there's any trouble pass + AttachmentsColumnWithCache = functools.partial( AttachmentsColumn, server_cache=server_cache ) - column_headers = utils.backward_compat_col_lister( - column_headers, parsed_args.columns, {'Display Name': 'Name'} - ) - return ( column_headers, ( @@ -534,8 +582,8 @@ def take_action(self, parsed_args): s, columns, formatters={ - 'Metadata': format_columns.DictColumn, - 'Attachments': AttachmentsColumnWithCache, + 'metadata': format_columns.DictColumn, + 'attachments': AttachmentsColumnWithCache, }, ) for s in data @@ -546,7 +594,7 @@ def take_action(self, parsed_args): class MigrateVolume(command.Command): _description = _("Migrate volume to a new host") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume', @@ -580,21 +628,26 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume = utils.find_resource(volume_client.volumes, parsed_args.volume) - volume_client.volumes.migrate_volume( + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + + volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False + ) + volume_client.migrate_volume( volume.id, - parsed_args.host, - parsed_args.force_host_copy, - parsed_args.lock_volume, + host=parsed_args.host, + force_host_copy=parsed_args.force_host_copy, + lock_volume=parsed_args.lock_volume, ) class SetVolume(command.Command): _description = _("Set volume properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume', @@ -747,9 +800,13 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume = utils.find_resource(volume_client.volumes, parsed_args.volume) + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False + ) result = 0 if parsed_args.retype_policy: @@ -765,7 +822,7 @@ def take_action(self, parsed_args): try: if parsed_args.size <= volume.size: msg = ( - _("New size must be greater than %s GB") % volume.size + _("New size must be greater than %d GB") % volume.size ) raise exceptions.CommandError(msg) if volume.status != 'available': @@ -777,24 +834,22 @@ def take_action(self, parsed_args): % volume.status ) raise exceptions.CommandError(msg) - volume_client.volumes.extend(volume.id, parsed_args.size) + volume_client.extend_volume(volume, parsed_args.size) except Exception as e: LOG.error(_("Failed to set volume size: %s"), e) result += 1 if parsed_args.no_property: try: - volume_client.volumes.delete_metadata( - volume.id, volume.metadata.keys() - ) + volume_client.delete_volume_metadata(volume) except Exception as e: LOG.error(_("Failed to clean volume properties: %s"), e) result += 1 if parsed_args.properties: try: - volume_client.volumes.set_metadata( - volume.id, parsed_args.properties + volume_client.set_volume_metadata( + volume, **parsed_args.properties ) except Exception as e: LOG.error(_("Failed to set volume properties: %s"), e) @@ -802,8 +857,8 @@ def take_action(self, parsed_args): if parsed_args.image_properties: try: - volume_client.volumes.set_image_metadata( - volume.id, parsed_args.image_properties + volume_client.set_volume_image_metadata( + volume, **parsed_args.image_properties ) except Exception as e: LOG.error(_("Failed to set image properties: %s"), e) @@ -811,15 +866,17 @@ def take_action(self, parsed_args): if parsed_args.state: try: - volume_client.volumes.reset_state(volume.id, parsed_args.state) + volume_client.reset_volume_status( + volume, status=parsed_args.state + ) except Exception as e: LOG.error(_("Failed to set volume state: %s"), e) result += 1 if parsed_args.attached: try: - volume_client.volumes.reset_state( - volume.id, state=None, attach_status="attached" + volume_client.reset_volume_status( + volume, attach_status="attached" ) except Exception as e: LOG.error(_("Failed to set volume attach-status: %s"), e) @@ -827,8 +884,8 @@ def take_action(self, parsed_args): if parsed_args.detached: try: - volume_client.volumes.reset_state( - volume.id, state=None, attach_status="detached" + volume_client.reset_volume_status( + volume, attach_status="detached" ) except Exception as e: LOG.error(_("Failed to set volume attach-status: %s"), e) @@ -836,8 +893,8 @@ def take_action(self, parsed_args): if parsed_args.bootable is not None: try: - volume_client.volumes.set_bootable( - volume.id, parsed_args.bootable + volume_client.set_volume_bootable_status( + volume, parsed_args.bootable ) except Exception as e: LOG.error(_("Failed to set volume bootable property: %s"), e) @@ -845,8 +902,8 @@ def take_action(self, parsed_args): if parsed_args.read_only is not None: try: - volume_client.volumes.update_readonly_flag( - volume.id, parsed_args.read_only + volume_client.set_volume_readonly( + volume, parsed_args.read_only ) except Exception as e: LOG.error( @@ -856,6 +913,7 @@ def take_action(self, parsed_args): result += 1 policy = parsed_args.migration_policy or parsed_args.retype_policy + if parsed_args.type: # get the migration policy migration_policy = 'never' @@ -863,11 +921,11 @@ def take_action(self, parsed_args): migration_policy = policy try: # find the volume type - volume_type = utils.find_resource( - volume_client.volume_types, parsed_args.type + volume_type = volume_client.find_type( + parsed_args.type, ignore_missing=False ) # reset to the new volume type - volume_client.volumes.retype( + volume_client.retype_volume( volume.id, volume_type.id, migration_policy ) except Exception as e: @@ -876,12 +934,12 @@ def take_action(self, parsed_args): elif policy: # If the "--migration-policy" is specified without "--type" LOG.warning( - _("'%s' option will not work without '--type' option") - % ( + _("'%s' option will not work without '--type' option"), + ( '--migration-policy' if parsed_args.migration_policy else '--retype-policy' - ) + ), ) kwargs = {} @@ -891,7 +949,7 @@ def take_action(self, parsed_args): kwargs['display_description'] = parsed_args.description if kwargs: try: - volume_client.volumes.update(volume.id, **kwargs) + volume_client.update_volume(volume, **kwargs) except Exception as e: LOG.error( _( @@ -911,7 +969,7 @@ def take_action(self, parsed_args): class ShowVolume(command.ShowOne): _description = _("Display volume details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume', @@ -920,31 +978,26 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume = utils.find_resource(volume_client.volumes, parsed_args.volume) - - # Special mapping for columns to make the output easier to read: - # 'metadata' --> 'properties' - # 'volume_type' --> 'type' - volume._info.update( - { - 'properties': format_columns.DictColumn( - volume._info.pop('metadata') - ), - 'type': volume._info.pop('volume_type'), - }, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' ) - # Remove key links from being displayed - volume._info.pop("links", None) - return zip(*sorted(volume._info.items())) + volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False + ) + + data = _format_volume(volume) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class UnsetVolume(command.Command): _description = _("Unset volume properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume', @@ -973,15 +1026,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume = utils.find_resource(volume_client.volumes, parsed_args.volume) + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False + ) result = 0 if parsed_args.properties: try: - volume_client.volumes.delete_metadata( - volume.id, parsed_args.properties + volume_client.delete_volume_metadata( + volume.id, keys=parsed_args.properties ) except Exception as e: LOG.error(_("Failed to unset volume properties: %s"), e) @@ -989,8 +1046,8 @@ def take_action(self, parsed_args): if parsed_args.image_properties: try: - volume_client.volumes.delete_image_metadata( - volume.id, parsed_args.image_properties + volume_client.delete_volume_image_metadata( + volume.id, keys=parsed_args.image_properties ) except Exception as e: LOG.error(_("Failed to unset image properties: %s"), e) diff --git a/openstackclient/volume/v2/volume_backend.py b/openstackclient/volume/v2/volume_backend.py index 2fbbed64a3..34e46564ef 100644 --- a/openstackclient/volume/v2/volume_backend.py +++ b/openstackclient/volume/v2/volume_backend.py @@ -14,17 +14,22 @@ """Storage backend action implementations""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack import utils as sdk_utils from osc_lib.cli import format_columns -from osc_lib.command import command from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ class ShowCapability(command.Lister): _description = _("Show capability command") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "host", @@ -33,8 +38,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) columns = [ 'Title', @@ -71,7 +80,7 @@ def take_action(self, parsed_args): class ListPool(command.Lister): _description = _("List pool command") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--long", @@ -83,8 +92,12 @@ def get_parser(self, prog_name): # able to pass in --filters with a = pair to filter on. return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) if parsed_args.long: columns = [ diff --git a/openstackclient/volume/v2/volume_backup.py b/openstackclient/volume/v2/volume_backup.py index f6746eec85..4bd3b413a2 100644 --- a/openstackclient/volume/v2/volume_backup.py +++ b/openstackclient/volume/v2/volume_backup.py @@ -14,21 +14,25 @@ """Volume v2 Backup action implementations""" +import argparse import functools import logging +from collections.abc import Iterable, Sequence +from typing import Any from cliff import columns as cliff_columns -from osc_lib.command import command +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -class VolumeIdColumn(cliff_columns.FormattableColumn): +class VolumeIdColumn(cliff_columns.FormattableColumn[str]): """Formattable column for volume ID column. Unlike the parent FormattableColumn class, the initializer of the @@ -39,11 +43,13 @@ class takes volume_cache as the second argument. ``functools.partial(VolumeIdColumn, volume_cache)``. """ - def __init__(self, value, volume_cache=None): + def __init__( + self, value: str, volume_cache: dict[str, Any] | None = None + ) -> None: super().__init__(value) self._volume_cache = volume_cache or {} - def human_readable(self): + def human_readable(self) -> str: """Return a volume name if available :rtype: either the volume ID or name @@ -58,7 +64,7 @@ def human_readable(self): class CreateVolumeBackup(command.ShowOne): _description = _("Create new volume backup") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "volume", @@ -102,8 +108,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) volume_id = volume_client.find_volume( parsed_args.volume, @@ -139,7 +149,7 @@ def take_action(self, parsed_args): class DeleteVolumeBackup(command.Command): _description = _("Delete volume backup(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "backups", @@ -155,8 +165,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) result = 0 for backup in parsed_args.backups: @@ -175,8 +187,8 @@ def take_action(self, parsed_args): _( "Failed to delete backup with " "name or ID '%(backup)s': %(e)s" - ) - % {'backup': backup, 'e': e} + ), + {'backup': backup, 'e': e}, ) if result > 0: @@ -191,7 +203,7 @@ def take_action(self, parsed_args): class ListVolumeBackup(command.Lister): _description = _("List volume backups") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--long", @@ -237,8 +249,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) columns: tuple[str, ...] = ( 'id', @@ -306,6 +322,7 @@ def take_action(self, parsed_args): all_tenants=parsed_args.all_projects, marker=marker_backup_id, limit=parsed_args.limit, + max_items=parsed_args.max_items, ) return ( @@ -324,7 +341,7 @@ def take_action(self, parsed_args): class RestoreVolumeBackup(command.ShowOne): _description = _("Restore volume backup") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "backup", @@ -350,8 +367,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) backup = volume_client.find_backup( parsed_args.backup, @@ -385,7 +406,7 @@ def take_action(self, parsed_args): restore = volume_client.restore_backup( backup.id, - volume_id=volume_id, + volume=volume_id, name=volume_name, ) @@ -396,7 +417,7 @@ def take_action(self, parsed_args): class SetVolumeBackup(command.Command): _description = _("Set volume backup properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "backup", @@ -416,14 +437,22 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - backup = utils.find_resource(volume_client.backups, parsed_args.backup) + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + + backup = volume_client.find_backup( + parsed_args.backup, + ignore_missing=False, + ) result = 0 if parsed_args.state: try: - volume_client.backups.reset_state(backup.id, parsed_args.state) + volume_client.reset_backup_status( + backup, status=parsed_args.state + ) except Exception as e: LOG.error(_("Failed to set backup state: %s"), e) result += 1 @@ -436,7 +465,7 @@ def take_action(self, parsed_args): class ShowVolumeBackup(command.ShowOne): _description = _("Display volume backup details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "backup", @@ -445,9 +474,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume - backup = volume_client.find_backup(parsed_args.backup) + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + backup = volume_client.find_backup( + parsed_args.backup, ignore_missing=False + ) columns: tuple[str, ...] = ( "availability_zone", "container", diff --git a/openstackclient/volume/v2/volume_host.py b/openstackclient/volume/v2/volume_host.py index 2b0df0aa2e..73fb852d31 100644 --- a/openstackclient/volume/v2/volume_host.py +++ b/openstackclient/volume/v2/volume_host.py @@ -14,15 +14,19 @@ """Volume v2 host action implementations""" -from osc_lib.command import command +import argparse +from openstack.block_storage.v2 import service as _service +from openstack import utils as sdk_utils + +from openstackclient import command from openstackclient.i18n import _ class FailoverVolumeHost(command.Command): _description = _("Failover volume host to different backend") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "host", metavar="", help=_("Name of volume host") @@ -38,17 +42,20 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - service_client = self.app.client_manager.volume - service_client.services.failover_host( - parsed_args.host, parsed_args.volume_backend + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + service = _service.Service(host=parsed_args.host) + volume_client.failover_service( + service, backend_id=parsed_args.volume_backend ) class SetVolumeHost(command.Command): _description = _("Set volume host properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "host", metavar="", help=_("Name of volume host") @@ -66,9 +73,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - service_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + service = _service.Service(host=parsed_args.host) if parsed_args.enable: - service_client.services.thaw_host(parsed_args.host) + volume_client.thaw_service(service) if parsed_args.disable: - service_client.services.freeze_host(parsed_args.host) + volume_client.freeze_service(service) diff --git a/openstackclient/volume/v2/volume_snapshot.py b/openstackclient/volume/v2/volume_snapshot.py index e8d5484a27..7ef9b16775 100644 --- a/openstackclient/volume/v2/volume_snapshot.py +++ b/openstackclient/volume/v2/volume_snapshot.py @@ -14,17 +14,21 @@ """Volume v2 snapshot action implementations""" -import copy +import argparse import functools import logging +from collections.abc import Iterable, Sequence +from typing import Any from cliff import columns as cliff_columns +from openstack.block_storage.v2 import snapshot as _snapshot +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -33,7 +37,7 @@ LOG = logging.getLogger(__name__) -class VolumeIdColumn(cliff_columns.FormattableColumn): +class VolumeIdColumn(cliff_columns.FormattableColumn[str]): """Formattable column for volume ID column. Unlike the parent FormattableColumn class, the initializer of the @@ -44,11 +48,13 @@ class takes volume_cache as the second argument. ``functools.partial(VolumeIdColumn, volume_cache)``. """ - def __init__(self, value, volume_cache=None): + def __init__( + self, value: str, volume_cache: dict[str, Any] | None = None + ) -> None: super().__init__(value) self._volume_cache = volume_cache or {} - def human_readable(self): + def human_readable(self) -> str: """Return a volume name if available :rtype: either the volume ID or name @@ -60,10 +66,46 @@ def human_readable(self): return volume +def _format_snapshot(snapshot: _snapshot.Snapshot) -> dict[str, object]: + # Some columns returned by openstacksdk should not be shown because they're + # either irrelevant or duplicates + ignored_columns = { + # computed columns + 'location', + # create-only columns + 'consumes_quota', + 'force', + 'group_snapshot_id', + # ignored columns + 'os-extended-snapshot-attributes:progress', + 'os-extended-snapshot-attributes:project_id', + 'updated_at', + 'user_id', + # unnecessary columns + 'links', + } + + info = snapshot.to_dict(original_names=True) + data = {} + for key, value in info.items(): + if key in ignored_columns: + continue + + data[key] = value + + data.update( + { + 'properties': format_columns.DictColumn(data.pop('metadata')), + } + ) + + return data + + class CreateVolumeSnapshot(command.ShowOne): _description = _("Create new volume snapshot") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "snapshot_name", @@ -94,6 +136,7 @@ def get_parser(self, prog_name): "--property", metavar="", action=parseractions.KeyValueAction, + dest="properties", help=_( "Set a property to this snapshot " "(repeat option to set multiple properties)" @@ -112,12 +155,18 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + volume = parsed_args.volume if not parsed_args.volume: volume = parsed_args.snapshot_name - volume_id = utils.find_resource(volume_client.volumes, volume).id + volume_id = volume_client.find_volume(volume, ignore_missing=False).id + if parsed_args.remote_source: # Create a new snapshot from an existing remote snapshot source if parsed_args.force: @@ -127,36 +176,33 @@ def take_action(self, parsed_args): "volume snapshot" ) LOG.warning(msg) - snapshot = volume_client.volume_snapshots.manage( + + snapshot = volume_client.manage_snapshot( volume_id=volume_id, ref=parsed_args.remote_source, name=parsed_args.snapshot_name, description=parsed_args.description, - metadata=parsed_args.property, + metadata=parsed_args.properties, ) else: # create a new snapshot from scratch - snapshot = volume_client.volume_snapshots.create( - volume_id, + snapshot = volume_client.create_snapshot( + volume_id=volume_id, force=parsed_args.force, name=parsed_args.snapshot_name, description=parsed_args.description, - metadata=parsed_args.property, + metadata=parsed_args.properties, ) - snapshot._info.update( - { - 'properties': format_columns.DictColumn( - snapshot._info.pop('metadata') - ) - } - ) - return zip(*sorted(snapshot._info.items())) + + data = _format_snapshot(snapshot) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class DeleteVolumeSnapshot(command.Command): _description = _("Delete volume snapshot(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "snapshots", @@ -174,17 +220,20 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) result = 0 - for i in parsed_args.snapshots: + for snapshot in parsed_args.snapshots: try: - snapshot_id = utils.find_resource( - volume_client.volume_snapshots, i + snapshot_id = volume_client.find_snapshot( + snapshot, ignore_missing=False ).id - volume_client.volume_snapshots.delete( - snapshot_id, parsed_args.force + volume_client.delete_snapshot( + snapshot_id, + force=parsed_args.force, ) except Exception as e: result += 1 @@ -192,8 +241,8 @@ def take_action(self, parsed_args): _( "Failed to delete snapshot with " "name or ID '%(snapshot)s': %(e)s" - ) - % {'snapshot': i, 'e': e} + ), + {'snapshot': snapshot, 'e': e}, ) if result > 0: @@ -208,7 +257,7 @@ def take_action(self, parsed_args): class ListVolumeSnapshot(command.Lister): _description = _("List volume snapshots") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--all-projects', @@ -259,32 +308,45 @@ def get_parser(self, prog_name): pagination.add_marker_pagination_option_to_parser(parser) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - identity_client = self.app.client_manager.identity - + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + # we don't narrow types here since we want to handle both v2.0 and v3 + identity_client = self.app.client_manager.sdk_connection.identity + + columns: tuple[str, ...] = ( + 'id', + 'name', + 'description', + 'status', + 'size', + ) + column_headers: tuple[str, ...] = ( + 'ID', + 'Name', + 'Description', + 'Status', + 'Size', + ) if parsed_args.long: - columns = [ - 'ID', - 'Name', - 'Description', - 'Status', - 'Size', + columns += ( + 'created_at', + 'volume_id', + 'metadata', + ) + column_headers += ( 'Created At', - 'Volume ID', - 'Metadata', - ] - column_headers = copy.deepcopy(columns) - column_headers[6] = 'Volume' - column_headers[7] = 'Properties' - else: - columns = ['ID', 'Name', 'Description', 'Status', 'Size'] - column_headers = copy.deepcopy(columns) + 'Volume', + 'Properties', + ) # Cache the volume list volume_cache = {} try: - for s in volume_client.volumes.list(): + for s in volume_client.volumes(): volume_cache[s.id] = s except Exception: # noqa: S110 # Just forget it if there's any trouble @@ -295,35 +357,32 @@ def take_action(self, parsed_args): volume_id = None if parsed_args.volume: - volume_id = utils.find_resource( - volume_client.volumes, parsed_args.volume + volume_id = volume_client.find_volume( + parsed_args.volume, ignore_missing=False ).id project_id = None if parsed_args.project: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) # set value of 'all_tenants' when using project option all_projects = ( True if parsed_args.project else parsed_args.all_projects ) - search_opts = { - 'all_tenants': all_projects, - 'project_id': project_id, - 'name': parsed_args.name, - 'status': parsed_args.status, - 'volume_id': volume_id, - } - - data = volume_client.volume_snapshots.list( - search_opts=search_opts, + data = volume_client.snapshots( marker=parsed_args.marker, limit=parsed_args.limit, + max_items=parsed_args.max_items, + all_projects=all_projects, + project_id=project_id, + name=parsed_args.name, + status=parsed_args.status, + volume_id=volume_id, ) return ( column_headers, @@ -332,8 +391,8 @@ def take_action(self, parsed_args): s, columns, formatters={ - 'Metadata': format_columns.DictColumn, - 'Volume ID': _VolumeIdColumn, + 'metadata': format_columns.DictColumn, + 'volume_id': _VolumeIdColumn, }, ) for s in data @@ -344,7 +403,7 @@ def take_action(self, parsed_args): class SetVolumeSnapshot(command.Command): _description = _("Set volume snapshot properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'snapshot', @@ -374,6 +433,7 @@ def get_parser(self, prog_name): '--property', metavar='', action=parseractions.KeyValueAction, + dest='properties', help=_( 'Property to add/change for this snapshot ' '(repeat option to set multiple properties)' @@ -399,28 +459,29 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - snapshot = utils.find_resource( - volume_client.volume_snapshots, parsed_args.snapshot + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + + snapshot = volume_client.find_snapshot( + parsed_args.snapshot, ignore_missing=False ) result = 0 if parsed_args.no_property: try: - key_list = snapshot.metadata.keys() - volume_client.volume_snapshots.delete_metadata( - snapshot.id, - list(key_list), + volume_client.delete_snapshot_metadata( + snapshot.id, keys=list(snapshot.metadata) ) except Exception as e: LOG.error(_("Failed to clean snapshot properties: %s"), e) result += 1 - if parsed_args.property: + if parsed_args.properties: try: - volume_client.volume_snapshots.set_metadata( - snapshot.id, parsed_args.property + volume_client.set_snapshot_metadata( + snapshot.id, **parsed_args.properties ) except Exception as e: LOG.error(_("Failed to set snapshot property: %s"), e) @@ -428,7 +489,7 @@ def take_action(self, parsed_args): if parsed_args.state: try: - volume_client.volume_snapshots.reset_state( + volume_client.reset_snapshot_status( snapshot.id, parsed_args.state ) except Exception as e: @@ -442,7 +503,7 @@ def take_action(self, parsed_args): kwargs['description'] = parsed_args.description if kwargs: try: - volume_client.volume_snapshots.update(snapshot.id, **kwargs) + volume_client.update_snapshot(snapshot.id, **kwargs) except Exception as e: LOG.error( _("Failed to update snapshot name or description: %s"), @@ -459,7 +520,7 @@ def take_action(self, parsed_args): class ShowVolumeSnapshot(command.ShowOne): _description = _("Display volume snapshot details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "snapshot", @@ -468,25 +529,26 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - snapshot = utils.find_resource( - volume_client.volume_snapshots, parsed_args.snapshot + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' ) - snapshot._info.update( - { - 'properties': format_columns.DictColumn( - snapshot._info.pop('metadata') - ) - } + + snapshot = volume_client.find_snapshot( + parsed_args.snapshot, ignore_missing=False ) - return zip(*sorted(snapshot._info.items())) + + data = _format_snapshot(snapshot) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class UnsetVolumeSnapshot(command.Command): _description = _("Unset volume snapshot properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'snapshot', @@ -498,6 +560,7 @@ def get_parser(self, prog_name): metavar='', action='append', default=[], + dest='properties', help=_( 'Property to remove from snapshot ' '(repeat option to remove multiple properties)' @@ -505,14 +568,16 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - snapshot = utils.find_resource( - volume_client.volume_snapshots, parsed_args.snapshot + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + + snapshot = volume_client.find_snapshot( + parsed_args.snapshot, ignore_missing=False ) - if parsed_args.property: - volume_client.volume_snapshots.delete_metadata( - snapshot.id, - parsed_args.property, + if parsed_args.properties: + volume_client.delete_snapshot_metadata( + snapshot.id, keys=parsed_args.properties ) diff --git a/openstackclient/volume/v2/volume_transfer_request.py b/openstackclient/volume/v2/volume_transfer_request.py index 3bd91d0522..83be03e215 100644 --- a/openstackclient/volume/v2/volume_transfer_request.py +++ b/openstackclient/volume/v2/volume_transfer_request.py @@ -14,12 +14,17 @@ """Volume v2 transfer action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -from osc_lib.command import command +from openstack import exceptions as sdk_exceptions +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -29,7 +34,7 @@ class AcceptTransferRequest(command.ShowOne): _description = _("Accept volume transfer request.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'transfer_request', @@ -44,31 +49,33 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) try: - transfer_request_id = utils.find_resource( - volume_client.transfers, parsed_args.transfer_request + transfer_request_id = volume_client.find_transfer( + parsed_args.transfer_request, ignore_missing=False ).id - except exceptions.CommandError: + except sdk_exceptions.ResourceNotFound: # Non-admin users will fail to lookup name -> ID so we just # move on and attempt with the user-supplied information transfer_request_id = parsed_args.transfer_request - transfer_accept = volume_client.transfers.accept( - transfer_request_id, - parsed_args.auth_key, + result = volume_client.accept_transfer( + transfer_request_id, parsed_args.auth_key ) - transfer_accept._info.pop("links", None) - - return zip(*sorted(transfer_accept._info.items())) + columns = ('id', 'name', 'volume_id') + data = (result.id, result.name, result.volume_id) + return columns, data class CreateTransferRequest(command.ShowOne): _description = _("Create volume transfer request.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--name', @@ -82,26 +89,34 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - - volume_id = utils.find_resource( - volume_client.volumes, - parsed_args.volume, - ).id - volume_transfer_request = volume_client.transfers.create( - volume_id, - parsed_args.name, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' ) - volume_transfer_request._info.pop("links", None) - - return zip(*sorted(volume_transfer_request._info.items())) + volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False + ) + result = volume_client.create_transfer( + volume_id=volume.id, + name=parsed_args.name, + ) + columns = ('auth_key', 'created_at', 'id', 'name', 'volume_id') + data = ( + result.auth_key, + result.created_at, + result.id, + result.name, + result.volume_id, + ) + return columns, data class DeleteTransferRequest(command.Command): _description = _("Delete volume transfer request(s).") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'transfer_request', @@ -111,25 +126,23 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) result = 0 for t in parsed_args.transfer_request: try: - transfer_request_id = utils.find_resource( - volume_client.transfers, - t, - ).id - volume_client.transfers.delete(transfer_request_id) + volume_client.delete_transfer(t, ignore_missing=False) except Exception as e: result += 1 LOG.error( _( "Failed to delete volume transfer request " "with name or ID '%(transfer)s': %(e)s" - ) - % {'transfer': t, 'e': e} + ), + {'transfer': t, 'e': e}, ) if result > 0: @@ -144,7 +157,7 @@ def take_action(self, parsed_args): class ListTransferRequest(command.Lister): _description = _("Lists all volume transfer requests.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--all-projects', @@ -155,30 +168,29 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - columns = ['ID', 'Name', 'Volume ID'] - column_headers = ['ID', 'Name', 'Volume'] - - volume_client = self.app.client_manager.volume - - volume_transfer_result = volume_client.transfers.list( - detailed=True, - search_opts={'all_tenants': parsed_args.all_projects}, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' ) + column_headers = ('ID', 'Name', 'Volume') + columns = ('id', 'name', 'volume_id') + results = volume_client.transfers( + details=True, + all_projects=parsed_args.all_projects, + ) return ( column_headers, - ( - utils.get_item_properties(s, columns) - for s in volume_transfer_result - ), + (utils.get_item_properties(s, columns) for s in results), ) class ShowTransferRequest(command.ShowOne): _description = _("Show volume transfer request details.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'transfer_request', @@ -187,12 +199,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume_transfer_request = utils.find_resource( - volume_client.transfers, - parsed_args.transfer_request, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' ) - volume_transfer_request._info.pop("links", None) - - return zip(*sorted(volume_transfer_request._info.items())) + result = volume_client.find_transfer( + parsed_args.transfer_request, ignore_missing=False + ) + columns = ('created_at', 'id', 'name', 'volume_id') + data = (result.created_at, result.id, result.name, result.volume_id) + return columns, data diff --git a/openstackclient/volume/v2/volume_type.py b/openstackclient/volume/v2/volume_type.py index 9e2ac20cbf..1e60d15e65 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -14,16 +14,21 @@ """Volume v2 Type action implementations""" +import argparse +from collections.abc import MutableMapping import functools import logging +from collections.abc import Iterable, Sequence +from typing import Any from cliff import columns as cliff_columns +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -31,7 +36,41 @@ LOG = logging.getLogger(__name__) -class EncryptionInfoColumn(cliff_columns.FormattableColumn): +def _format_type(volume_type: Any) -> dict[str, Any]: + ignored_columns = {'location'} + info = volume_type.to_dict() + data: dict[str, Any] = {} + for key, value in info.items(): + if key in ignored_columns: + continue + data[key] = value + + data['properties'] = format_columns.DictColumn(data.pop('extra_specs', {})) + + return data + + +def _format_encryption(encryption: Any) -> dict[str, Any]: + ignored_columns = { + 'id', + 'location', + 'name', + 'deleted', + 'created_at', + 'updated_at', + 'deleted_at', + 'volume_type_id', + } + info = encryption.to_dict() + data: dict[str, Any] = {} + for key, value in info.items(): + if key in ignored_columns: + continue + data[key] = value + return data + + +class EncryptionInfoColumn(cliff_columns.FormattableColumn[Any]): """Formattable column for encryption info column. Unlike the parent FormattableColumn class, the initializer of the @@ -42,57 +81,66 @@ class takes encryption_data as the second argument. ``functools.partial(EncryptionInfoColumn encryption_data)``. """ - def __init__(self, value, encryption_data=None): + def __init__( + self, value: Any, encryption_data: dict[str, Any] | None = None + ) -> None: super().__init__(value) self._encryption_data = encryption_data or {} - def _get_encryption_info(self): + def _get_encryption_info(self) -> Any: type_id = self._value return self._encryption_data.get(type_id) - def human_readable(self): + def human_readable(self) -> str: encryption_info = self._get_encryption_info() if encryption_info: return utils.format_dict(encryption_info) else: return '-' - def machine_readable(self): + def machine_readable(self) -> Any: return self._get_encryption_info() -def _create_encryption_type(volume_client, volume_type, parsed_args): +def _create_encryption_type( + volume_client: Any, volume_type: Any, parsed_args: argparse.Namespace +) -> Any: if not parsed_args.encryption_provider: msg = _( "'--encryption-provider' should be specified while " "creating a new encryption type" ) raise exceptions.CommandError(msg) + # set the default of control location while creating control_location = 'front-end' if parsed_args.encryption_control_location: control_location = parsed_args.encryption_control_location - body = { - 'provider': parsed_args.encryption_provider, - 'cipher': parsed_args.encryption_cipher, - 'key_size': parsed_args.encryption_key_size, - 'control_location': control_location, - } - encryption = volume_client.volume_encryption_types.create( - volume_type, body + + return volume_client.create_type_encryption( + volume_type, + provider=parsed_args.encryption_provider, + cipher=parsed_args.encryption_cipher, + key_size=parsed_args.encryption_key_size, + control_location=control_location, ) - return encryption -def _set_encryption_type(volume_client, volume_type, parsed_args): +def _set_encryption_type( + volume_client: Any, volume_type: Any, parsed_args: argparse.Namespace +) -> None: # update the existing encryption type - body = {} + kwargs = {} for attr in ['provider', 'cipher', 'key_size', 'control_location']: info = getattr(parsed_args, 'encryption_' + attr, None) if info is not None: - body[attr] = info + kwargs[attr] = info try: - volume_client.volume_encryption_types.update(volume_type, body) + volume_client.update_type_encryption( + encryption=None, + volume_type=volume_type, + **kwargs, + ) except Exception as e: if type(e).__name__ == 'NotFound': # create new encryption type @@ -108,7 +156,7 @@ def _set_encryption_type(volume_client, volume_type, parsed_args): class CreateVolumeType(command.ShowOne): _description = _("Create new volume type") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "name", @@ -171,7 +219,8 @@ def get_parser(self, prog_name): default=False, help=_( "Enabled replication for this volume type " - "(this is an alias for '--property replication_enabled= True') " + "(this is an alias for " + "'--property replication_enabled= True') " "(requires driver support)" ), ) @@ -181,7 +230,8 @@ def get_parser(self, prog_name): dest='availability_zones', help=_( "Set an availability zone for this volume type " - "(this is an alias for '--property RESKEY:availability_zones:') " + "(this is an alias for " + "'--property RESKEY:availability_zones:') " "(repeat option to set multiple availability zones)" ), ) @@ -240,9 +290,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + # we don't narrow types here since we want to handle both v2.0 and v3 + identity_client = self.app.client_manager.sdk_connection.identity if parsed_args.project and parsed_args.is_public is not False: msg = _("--project is only allowed with --private") @@ -253,28 +308,27 @@ def take_action(self, parsed_args): if parsed_args.is_public is not None: kwargs['is_public'] = parsed_args.is_public - volume_type = volume_client.volume_types.create( - parsed_args.name, + volume_type = volume_client.create_type( + name=parsed_args.name, description=parsed_args.description, **kwargs, ) - volume_type._info.pop('extra_specs') + + info = _format_type(volume_type) if parsed_args.project: try: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id - volume_client.volume_type_access.add_project_access( - volume_type.id, project_id ) + volume_client.add_type_access(volume_type.id, project_id) except Exception as e: msg = _( "Failed to add project %(project)s access to type: %(e)s" ) - LOG.error(msg % {'project': parsed_args.project, 'e': e}) + LOG.error(msg, {'project': parsed_args.project, 'e': e}) properties = {} if parsed_args.properties: @@ -290,10 +344,10 @@ def take_action(self, parsed_args): parsed_args.availability_zones ) if properties: - result = volume_type.set_keys(properties) - volume_type._info.update( - {'properties': format_columns.DictColumn(result)} + result = volume_client.update_type_extra_specs( + volume_type.id, **properties ) + info['properties'] = format_columns.DictColumn(result.extra_specs) if ( parsed_args.encryption_provider @@ -315,20 +369,17 @@ def take_action(self, parsed_args): e, ) # add encryption info in result - encryption._info.pop("volume_type_id", None) - volume_type._info.update( - {'encryption': format_columns.DictColumn(encryption._info)} - ) - - volume_type._info.pop("os-volume-type-access:is_public", None) + encryption_info = _format_encryption(encryption) + info['encryption'] = format_columns.DictColumn(encryption_info) - return zip(*sorted(volume_type._info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteVolumeType(command.Command): _description = _("Delete volume type(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "volume_types", @@ -338,25 +389,26 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) result = 0 for volume_type in parsed_args.volume_types: try: - vol_type = utils.find_resource( - volume_client.volume_types, volume_type + vol_type = volume_client.find_type( + volume_type, ignore_missing=False ) - - volume_client.volume_types.delete(vol_type) + volume_client.delete_type(vol_type) except Exception as e: result += 1 LOG.error( _( "Failed to delete volume type with " "name or ID '%(volume_type)s': %(e)s" - ) - % {'volume_type': volume_type, 'e': e} + ), + {'volume_type': volume_type, 'e': e}, ) if result > 0: @@ -370,7 +422,7 @@ def take_action(self, parsed_args): class ListVolumeType(command.Lister): _description = _("List volume types") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -389,14 +441,14 @@ def get_parser(self, prog_name): "--public", action="store_true", dest="is_public", - default=None, + default='none', # cinder expects the string "none" help=_("List only public types"), ) public_group.add_argument( "--private", action="store_false", dest="is_public", - default=None, + default='none', # cinder expects the string "none" help=_("List only private types (admin only)"), ) parser.add_argument( @@ -409,8 +461,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) if parsed_args.long: columns = [ @@ -430,30 +486,28 @@ def take_action(self, parsed_args): column_headers = ['ID', 'Name', 'Is Public'] if parsed_args.default: - data = [volume_client.volume_types.default()] + data = [volume_client.get_type('default')] else: - data = volume_client.volume_types.list( - is_public=parsed_args.is_public, + data = list( + volume_client.types( + is_public=parsed_args.is_public, + ) ) - formatters = {'Extra Specs': format_columns.DictColumn} + formatters: MutableMapping[str, Any] = { + 'Extra Specs': format_columns.DictColumn + } if parsed_args.encryption_type: encryption = {} - for d in volume_client.volume_encryption_types.list(): - volume_type_id = d._info['volume_type_id'] - # remove some redundant information - del_key = [ - 'deleted', - 'created_at', - 'updated_at', - 'deleted_at', - 'volume_type_id', - ] - for key in del_key: - d._info.pop(key, None) - # save the encryption information with their volume type ID - encryption[volume_type_id] = d._info + for d in data: + e = volume_client.get_type_encryption(d.id) + if not e.volume_type_id: + continue + + encryption_info = _format_encryption(e) + encryption[e.volume_type_id] = encryption_info + # We need to get volume type ID, then show encryption # information according to the ID, so use "id" to keep # difference to the real "ID" column. @@ -481,7 +535,7 @@ def take_action(self, parsed_args): class SetVolumeType(command.Command): _description = _("Set volume type properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume_type', @@ -534,7 +588,8 @@ def get_parser(self, prog_name): default=False, help=_( "Enabled replication for this volume type " - "(this is an alias for '--property replication_enabled= True') " + "(this is an alias for " + "'--property replication_enabled= True') " "(requires driver support)" ), ) @@ -544,7 +599,8 @@ def get_parser(self, prog_name): dest='availability_zones', help=_( "Set an availability zone for this volume type " - "(this is an alias for '--property RESKEY:availability_zones:') " + "(this is an alias for " + "'--property RESKEY:availability_zones:') " "(repeat option to set multiple availability zones)" ), ) @@ -618,13 +674,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + # we don't narrow types here since we want to handle both v2.0 and v3 + identity_client = self.app.client_manager.sdk_connection.identity - volume_type = utils.find_resource( - volume_client.volume_types, - parsed_args.volume_type, + volume_type = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False ) result = 0 @@ -641,7 +699,7 @@ def take_action(self, parsed_args): if kwargs: try: - volume_client.volume_types.update(volume_type.id, **kwargs) + volume_client.update_type(volume_type.id, **kwargs) except Exception as e: LOG.error( _("Failed to update volume type name or description: %s"), @@ -664,23 +722,21 @@ def take_action(self, parsed_args): ) if properties: try: - volume_type.set_keys(properties) + volume_client.update_type_extra_specs( + volume_type.id, **properties + ) except Exception as e: LOG.error(_("Failed to set volume type properties: %s"), e) result += 1 if parsed_args.project: - project_info = None try: - project_info = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, ) - - volume_client.volume_type_access.add_project_access( - volume_type.id, project_info.id - ) + volume_client.add_type_access(volume_type.id, project_id) except Exception as e: LOG.error( _("Failed to set volume type access to project: %s"), e @@ -714,7 +770,7 @@ def take_action(self, parsed_args): class ShowVolumeType(command.ShowOne): _description = _("Display volume type details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "volume_type", @@ -731,19 +787,23 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume_type = utils.find_resource( - volume_client.volume_types, parsed_args.volume_type + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' ) - properties = format_columns.DictColumn( - volume_type._info.pop('extra_specs', {}) + + volume_type = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False ) - volume_type._info.update({'properties': properties}) + + info = _format_type(volume_type) + access_project_ids = None if not volume_type.is_public: try: - volume_type_access = volume_client.volume_type_access.list( + volume_type_access = volume_client.get_type_access( volume_type.id ) project_ids = [ @@ -758,18 +818,15 @@ def take_action(self, parsed_args): 'Failed to get access project list for volume type ' '%(type)s: %(e)s' ) - LOG.error(msg % {'type': volume_type.id, 'e': e}) - volume_type._info.update({'access_project_ids': access_project_ids}) + LOG.error(msg, {'type': volume_type.id, 'e': e}) + info['access_project_ids'] = access_project_ids + if parsed_args.encryption_type: # show encryption type information for this volume type try: - encryption = volume_client.volume_encryption_types.get( - volume_type.id - ) - encryption._info.pop("volume_type_id", None) - volume_type._info.update( - {'encryption': format_columns.DictColumn(encryption._info)} - ) + encryption = volume_client.get_type_encryption(volume_type.id) + encryption_info = _format_encryption(encryption) + info['encryption'] = format_columns.DictColumn(encryption_info) except Exception as e: LOG.error( _( @@ -778,14 +835,14 @@ def take_action(self, parsed_args): ), e, ) - volume_type._info.pop("os-volume-type-access:is_public", None) - return zip(*sorted(volume_type._info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class UnsetVolumeType(command.Command): _description = _("Unset volume type properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume_type', @@ -820,35 +877,35 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) + # we don't narrow types here since we want to handle both v2.0 and v3 + identity_client = self.app.client_manager.sdk_connection.identity - volume_type = utils.find_resource( - volume_client.volume_types, - parsed_args.volume_type, + volume_type = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False ) result = 0 if parsed_args.properties: try: - volume_type.unset_keys(parsed_args.properties) + volume_client.delete_type_extra_specs( + volume_type.id, parsed_args.properties + ) except Exception as e: LOG.error(_("Failed to unset volume type properties: %s"), e) result += 1 if parsed_args.project: - project_info = None try: - project_info = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, ) - - volume_client.volume_type_access.remove_project_access( - volume_type.id, project_info.id - ) + volume_client.remove_type_access(volume_type.id, project_id) except Exception as e: LOG.error( _("Failed to remove volume type access from project: %s"), @@ -857,7 +914,7 @@ def take_action(self, parsed_args): result += 1 if parsed_args.encryption_type: try: - volume_client.volume_encryption_types.delete(volume_type) + volume_client.delete_type_encryption(None, volume_type.id) except Exception as e: LOG.error( _( diff --git a/openstackclient/volume/v3/backup_record.py b/openstackclient/volume/v3/backup_record.py new file mode 100644 index 0000000000..b0297f42b7 --- /dev/null +++ b/openstackclient/volume/v3/backup_record.py @@ -0,0 +1,100 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Volume v3 Backup action implementations""" + +import argparse +import logging +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack import utils as sdk_utils + +from openstackclient import command +from openstackclient.i18n import _ + +LOG = logging.getLogger(__name__) + + +class ExportBackupRecord(command.ShowOne): + _description = _( + """Export volume backup details. + +Backup information can be imported into a new service instance to be able to +restore.""" + ) + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "backup", + metavar="", + help=_("Backup to export (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + backup = volume_client.find_backup( + parsed_args.backup, ignore_missing=False + ) + backup_data = volume_client.export_backup(backup)['backup-record'] + + # We only want to show "friendly" display names, but also want to keep + # json structure compatibility with cinderclient + if parsed_args.formatter == 'table': + backup_data['Backup Service'] = backup_data.pop('backup_service') + backup_data['Metadata'] = backup_data.pop('backup_url') + + col_headers, col_data = zip(*sorted(backup_data.items())) + return col_headers, col_data + + +class ImportBackupRecord(command.ShowOne): + _description = _( + """Import volume backup details. + +Exported backup details contain the metadata necessary to restore to a new or +rebuilt service instance""" + ) + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "backup_service", + metavar="", + help=_("Backup service containing the backup."), + ) + parser.add_argument( + "backup_metadata", + metavar="", + help=_("Encoded backup metadata from export."), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + backup_data = volume_client.import_backup( + parsed_args.backup_service, parsed_args.backup_metadata + ) + backup_data.pop('links', None) + col_headers, col_data = zip(*sorted(backup_data.items())) + return col_headers, col_data diff --git a/openstackclient/volume/v3/block_storage_cleanup.py b/openstackclient/volume/v3/block_storage_cleanup.py index 361440c994..00c354fb15 100644 --- a/openstackclient/volume/v3/block_storage_cleanup.py +++ b/openstackclient/volume/v3/block_storage_cleanup.py @@ -10,14 +10,20 @@ # License for the specific language governing permissions and limitations # under the License. -from cinderclient import api_versions -from osc_lib.command import command +import argparse +from collections.abc import Iterable +from typing import Any + +from openstack import utils as sdk_utils from osc_lib import exceptions +from openstackclient import command from openstackclient.i18n import _ -def _format_cleanup_response(cleaning, unavailable): +def _format_cleanup_response( + cleaning: Any, unavailable: Any +) -> tuple[tuple[str, ...], list[tuple[Any, ...]]]: column_headers = ( 'ID', 'Cluster Name', @@ -27,15 +33,21 @@ def _format_cleanup_response(cleaning, unavailable): ) combined_data = [] for obj in cleaning: - details = (obj.id, obj.cluster_name, obj.host, obj.binary, 'Cleaning') + details = ( + obj['id'], + obj['cluster_name'], + obj['host'], + obj['binary'], + 'Cleaning', + ) combined_data.append(details) for obj in unavailable: details = ( - obj.id, - obj.cluster_name, - obj.host, - obj.binary, + obj['id'], + obj['cluster_name'], + obj['host'], + obj['binary'], 'Unavailable', ) combined_data.append(details) @@ -49,7 +61,7 @@ class BlockStorageCleanup(command.Lister): This command requires ``--os-volume-api-version`` 3.24 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--cluster', @@ -127,10 +139,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.24'): + if not sdk_utils.supports_microversion(volume_client, '3.24'): msg = _( "--os-volume-api-version 3.24 or greater is required to " "support the 'block storage cleanup' command" @@ -149,5 +165,7 @@ def take_action(self, parsed_args): } filters = {k: v for k, v in filters.items() if v is not None} - cleaning, unavailable = volume_client.workers.clean(**filters) - return _format_cleanup_response(cleaning, unavailable) + result = volume_client.cleanup_service_workers(**filters) + return _format_cleanup_response( + result['cleaning'], result['unavailable'] + ) diff --git a/openstackclient/volume/v3/block_storage_cluster.py b/openstackclient/volume/v3/block_storage_cluster.py index 8cf01b2478..7188e31922 100644 --- a/openstackclient/volume/v3/block_storage_cluster.py +++ b/openstackclient/volume/v3/block_storage_cluster.py @@ -10,15 +10,22 @@ # License for the specific language governing permissions and limitations # under the License. -from cinderclient import api_versions -from osc_lib.command import command +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack.block_storage.v3 import cluster as _cluster +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ -def _format_cluster(cluster, detailed=False): +def _format_cluster( + cluster: Any, detailed: bool = False +) -> tuple[Sequence[str], Iterable[Any]]: columns: tuple[str, ...] = ( 'name', 'binary', @@ -73,7 +80,7 @@ class ListBlockStorageCluster(command.Lister): This command requires ``--os-volume-api-version`` 3.7 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--cluster', @@ -137,10 +144,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.7'): + if not sdk_utils.supports_microversion(volume_client, '3.7'): msg = _( "--os-volume-api-version 3.7 or greater is required to " "support the 'block storage cluster list' command" @@ -158,14 +169,14 @@ def take_action(self, parsed_args): 'Updated At', ) - data = volume_client.clusters.list( + data = volume_client.clusters( name=parsed_args.cluster, binary=parsed_args.binary, is_up=parsed_args.is_up, disabled=parsed_args.is_disabled, num_hosts=parsed_args.num_hosts, num_down_hosts=parsed_args.num_down_hosts, - detailed=parsed_args.long, + details=parsed_args.long, ) return ( @@ -180,7 +191,7 @@ class SetBlockStorageCluster(command.Command): This command requires ``--os-volume-api-version`` 3.7 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'cluster', @@ -221,10 +232,14 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.7'): + if not sdk_utils.supports_microversion(volume_client, '3.7'): msg = _( "--os-volume-api-version 3.7 or greater is required to " "support the 'block storage cluster set' command" @@ -235,13 +250,19 @@ def take_action(self, parsed_args): msg = _("Cannot specify --disable-reason without --disable") raise exceptions.CommandError(msg) - cluster = volume_client.clusters.update( - parsed_args.cluster, - parsed_args.binary, - disabled=parsed_args.disabled, - disabled_reason=parsed_args.disabled_reason, + cluster_obj = _cluster.Cluster( + name=parsed_args.cluster, + binary=parsed_args.binary, ) + if parsed_args.disabled: + cluster = volume_client.disable_cluster( + cluster_obj, + reason=parsed_args.disabled_reason, + ) + else: + cluster = volume_client.enable_cluster(cluster_obj) + return _format_cluster(cluster, detailed=True) @@ -251,7 +272,7 @@ class ShowBlockStorageCluster(command.ShowOne): This command requires ``--os-volume-api-version`` 3.7 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'cluster', @@ -265,19 +286,20 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.7'): + if not sdk_utils.supports_microversion(volume_client, '3.7'): msg = _( "--os-volume-api-version 3.7 or greater is required to " "support the 'block storage cluster show' command" ) raise exceptions.CommandError(msg) - cluster = volume_client.clusters.show( - parsed_args.cluster, - binary=parsed_args.binary, - ) + cluster = volume_client.get_cluster(parsed_args.cluster) return _format_cluster(cluster, detailed=True) diff --git a/openstackclient/volume/v3/block_storage_log_level.py b/openstackclient/volume/v3/block_storage_log_level.py index 878cf4087a..095cbeb1f1 100644 --- a/openstackclient/volume/v3/block_storage_log_level.py +++ b/openstackclient/volume/v3/block_storage_log_level.py @@ -14,11 +14,13 @@ """Block Storage Service action implementations""" -from cinderclient import api_versions -from osc_lib.command import command +import argparse +from typing import Any + +from openstack import utils as sdk_utils from osc_lib import exceptions -from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -28,12 +30,12 @@ class BlockStorageLogLevelList(command.Lister): Supported by --os-volume-api-version 3.32 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "--host", metavar="", - default="", + default=None, help=_( "List block storage service log level of specified host " "(name only)" @@ -42,9 +44,9 @@ def get_parser(self, prog_name): parser.add_argument( "--service", metavar="", - default="", + default=None, choices=( - '', + None, '*', 'cinder-api', 'cinder-volume', @@ -59,13 +61,17 @@ def get_parser(self, prog_name): parser.add_argument( "--log-prefix", metavar="", - default="", + default=None, help="Prefix for the log, e.g. 'sqlalchemy'", ) return parser - def take_action(self, parsed_args): - service_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[list[str], list[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) columns = [ "Binary", "Host", @@ -73,29 +79,24 @@ def take_action(self, parsed_args): "Level", ] - if service_client.api_version < api_versions.APIVersion('3.32'): + if not sdk_utils.supports_microversion(volume_client, '3.32'): msg = _( "--os-volume-api-version 3.32 or greater is required to " "support the 'block storage log level list' command" ) raise exceptions.CommandError(msg) - data = service_client.services.get_log_levels( + data = [] + for entry in volume_client.get_service_log_levels( binary=parsed_args.service, server=parsed_args.host, prefix=parsed_args.log_prefix, - ) + ): + entry_levels = sorted(entry.levels.items(), key=lambda x: x[0]) + for prefix, level in entry_levels: + data.append((entry.binary, entry.host, prefix, level)) - return ( - columns, - ( - utils.get_item_properties( - s, - columns, - ) - for s in data - ), - ) + return (columns, data) class BlockStorageLogLevelSet(command.Command): @@ -104,19 +105,19 @@ class BlockStorageLogLevelSet(command.Command): Supported by --os-volume-api-version 3.32 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "level", metavar="", choices=('INFO', 'WARNING', 'ERROR', 'DEBUG'), type=str.upper, - help=_("Desired log level."), + help=_("Desired log level"), ) parser.add_argument( "--host", metavar="", - default="", + default=None, help=_( "Set block storage service log level of specified host " "(name only)" @@ -125,9 +126,9 @@ def get_parser(self, prog_name): parser.add_argument( "--service", metavar="", - default="", + default=None, choices=( - '', + None, '*', 'cinder-api', 'cinder-volume', @@ -142,22 +143,24 @@ def get_parser(self, prog_name): parser.add_argument( "--log-prefix", metavar="", - default="", + default=None, help="Prefix for the log, e.g. 'sqlalchemy'", ) return parser - def take_action(self, parsed_args): - service_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if service_client.api_version < api_versions.APIVersion('3.32'): + if not sdk_utils.supports_microversion(volume_client, '3.32'): msg = _( "--os-volume-api-version 3.32 or greater is required to " "support the 'block storage log level set' command" ) raise exceptions.CommandError(msg) - service_client.services.set_log_levels( + volume_client.set_service_log_levels( level=parsed_args.level, binary=parsed_args.service, server=parsed_args.host, diff --git a/openstackclient/volume/v3/block_storage_manage.py b/openstackclient/volume/v3/block_storage_manage.py index 17c699b992..87720ed2e6 100644 --- a/openstackclient/volume/v3/block_storage_manage.py +++ b/openstackclient/volume/v3/block_storage_manage.py @@ -14,12 +14,14 @@ """Block Storage Volume/Snapshot Management implementations""" import argparse +from collections.abc import Iterable +from typing import Any -from cinderclient import api_versions -from osc_lib.command import command +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -32,7 +34,7 @@ class BlockStorageManageVolumes(command.Lister): Supported by --os-volume-api-version 3.8 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) host_group = parser.add_mutually_exclusive_group() host_group.add_argument( @@ -104,8 +106,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[list[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if parsed_args.host is None and parsed_args.cluster is None: msg = _( @@ -114,7 +120,7 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - if volume_client.api_version < api_versions.APIVersion('3.8'): + if not sdk_utils.supports_microversion(volume_client, '3.8'): msg = _( "--os-volume-api-version 3.8 or greater is required to " "support the 'block storage volume manageable list' command" @@ -122,7 +128,7 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) if parsed_args.cluster: - if volume_client.api_version < api_versions.APIVersion('3.17'): + if not sdk_utils.supports_microversion(volume_client, '3.17'): msg = _( "--os-volume-api-version 3.17 or greater is required to " "support the '--cluster' option" @@ -168,15 +174,21 @@ def take_action(self, parsed_args): ] ) - data = volume_client.volumes.list_manageable( - host=parsed_args.host, - detailed=detailed, - marker=parsed_args.marker, - limit=parsed_args.limit, - offset=parsed_args.offset, - sort=parsed_args.sort, - cluster=parsed_args.cluster, - ) + query: dict[str, Any] = {} + if parsed_args.host: + query['host'] = parsed_args.host + if parsed_args.cluster: + query['cluster'] = parsed_args.cluster + if parsed_args.marker: + query['marker'] = parsed_args.marker + if parsed_args.limit: + query['limit'] = parsed_args.limit + if parsed_args.offset: + query['offset'] = parsed_args.offset + if parsed_args.sort: + query['sort'] = parsed_args.sort + + data = volume_client.manageable_volumes(details=detailed, **query) return ( columns, @@ -196,7 +208,7 @@ class BlockStorageManageSnapshots(command.Lister): Supported by --os-volume-api-version 3.8 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) host_group = parser.add_mutually_exclusive_group() host_group.add_argument( @@ -268,8 +280,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[list[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if parsed_args.host is None and parsed_args.cluster is None: msg = _( @@ -279,7 +295,7 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - if volume_client.api_version < api_versions.APIVersion('3.8'): + if not sdk_utils.supports_microversion(volume_client, '3.8'): msg = _( "--os-volume-api-version 3.8 or greater is required to " "support the 'block storage volume snapshot manageable list' " @@ -288,7 +304,7 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) if parsed_args.cluster: - if volume_client.api_version < api_versions.APIVersion('3.17'): + if not sdk_utils.supports_microversion(volume_client, '3.17'): msg = _( "--os-volume-api-version 3.17 or greater is required to " "support the '--cluster' option" @@ -335,15 +351,21 @@ def take_action(self, parsed_args): ] ) - data = volume_client.volume_snapshots.list_manageable( - host=parsed_args.host, - detailed=detailed, - marker=parsed_args.marker, - limit=parsed_args.limit, - offset=parsed_args.offset, - sort=parsed_args.sort, - cluster=parsed_args.cluster, - ) + query: dict[str, Any] = {} + if parsed_args.host: + query['host'] = parsed_args.host + if parsed_args.cluster: + query['cluster'] = parsed_args.cluster + if parsed_args.marker: + query['marker'] = parsed_args.marker + if parsed_args.limit: + query['limit'] = parsed_args.limit + if parsed_args.offset: + query['offset'] = parsed_args.offset + if parsed_args.sort: + query['sort'] = parsed_args.sort + + data = volume_client.manageable_snapshots(details=detailed, **query) return ( columns, diff --git a/openstackclient/volume/v3/block_storage_resource_filter.py b/openstackclient/volume/v3/block_storage_resource_filter.py index 494d9cbe16..e760f5cd51 100644 --- a/openstackclient/volume/v3/block_storage_resource_filter.py +++ b/openstackclient/volume/v3/block_storage_resource_filter.py @@ -12,20 +12,28 @@ """Volume V3 Resource Filters implementations""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from openstack import utils as sdk_utils from osc_lib.cli import format_columns -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ class ListBlockStorageResourceFilter(command.Lister): _description = _('List block storage resource filters') - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.33'): msg = _( @@ -58,7 +66,7 @@ def take_action(self, parsed_args): class ShowBlockStorageResourceFilter(command.ShowOne): _description = _('Show filters for a block storage resource type') - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'resource', @@ -68,8 +76,12 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.33'): msg = _( diff --git a/openstackclient/volume/v3/consistency_group.py b/openstackclient/volume/v3/consistency_group.py new file mode 100644 index 0000000000..b2fa4a7ad9 --- /dev/null +++ b/openstackclient/volume/v3/consistency_group.py @@ -0,0 +1,440 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Volume v3 consistency group action implementations""" + +import argparse +import logging +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack.block_storage import v3 as block_storage_v3 +from openstack.block_storage.v3 import consistency_group as _consistency_group +from openstack import utils as sdk_utils +from osc_lib.cli import format_columns +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient import command +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +def _find_volumes( + parsed_args_volumes: list[str], volume_client: block_storage_v3.Proxy +) -> tuple[int, str]: + result = 0 + uuid = '' + for volume in parsed_args_volumes: + try: + volume_id = volume_client.find_volume( + volume, ignore_missing=False + ).id + uuid += volume_id + ',' + except Exception as e: + result += 1 + LOG.error( + _("Failed to find volume with name or ID '%(volume)s':%(e)s"), + {'volume': volume, 'e': e}, + ) + + return result, uuid + + +def _format_consistency_group( + consistency_group: _consistency_group.ConsistencyGroup, +) -> tuple[Sequence[str], Iterable[Any]]: + columns = ( + 'availability_zone', + 'created_at', + 'description', + 'id', + 'name', + 'status', + 'volume_types', + ) + return columns, utils.get_item_properties(consistency_group, columns) + + +class AddVolumeToConsistencyGroup(command.Command): + _description = _("Add volume(s) to consistency group") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'consistency_group', + metavar="", + help=_('Consistency group to contain (name or ID)'), + ) + parser.add_argument( + 'volumes', + metavar='', + nargs='+', + help=_( + 'Volume(s) to add to (name or ID) ' + '(repeat option to add multiple volumes)' + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + result, add_uuid = _find_volumes(parsed_args.volumes, volume_client) + + if result > 0: + total = len(parsed_args.volumes) + LOG.error( + _("%(result)s of %(total)s volumes failed to add."), + {'result': result, 'total': total}, + ) + + if add_uuid: + add_uuid = add_uuid.rstrip(',') + consistency_group = volume_client.find_consistency_group( + parsed_args.consistency_group, ignore_missing=False + ) + volume_client.update_consistency_group( + consistency_group, add_volumes=add_uuid + ) + + +class CreateConsistencyGroup(command.ShowOne): + _description = _("Create new consistency group.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "name", + metavar="", + nargs="?", + help=_("Name of new consistency group (default to None)"), + ) + exclusive_group = parser.add_mutually_exclusive_group(required=True) + exclusive_group.add_argument( + "--volume-type", + metavar="", + help=_("Volume type of this consistency group (name or ID)"), + ) + exclusive_group.add_argument( + "--source", + metavar="", + help=_("Existing consistency group (name or ID)"), + ) + # NOTE(stephenfin): Legacy alias + exclusive_group.add_argument( + "--consistency-group-source", + metavar="", + dest='source', + help=argparse.SUPPRESS, + ) + exclusive_group.add_argument( + "--snapshot", + metavar="", + help=_("Existing consistency group snapshot (name or ID)"), + ) + # NOTE(stephenfin): Legacy alias + exclusive_group.add_argument( + "--consistency-group-snapshot", + metavar="", + dest='snapshot', + help=argparse.SUPPRESS, + ) + parser.add_argument( + "--description", + metavar="", + help=_("Description of this consistency group"), + ) + parser.add_argument( + "--availability-zone", + metavar="", + help=_( + "Availability zone for this consistency group " + "(not available if creating consistency group " + "from source)" + ), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + if parsed_args.volume_type: + volume_type_id = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False + ).id + consistency_group = volume_client.create_consistency_group( + volume_types=volume_type_id, + name=parsed_args.name, + description=parsed_args.description, + availability_zone=parsed_args.availability_zone, + ) + else: + if parsed_args.availability_zone: + msg = _( + "'--availability-zone' option will not work " + "if creating consistency group from source" + ) + LOG.warning(msg) + + consistency_group_id = None + consistency_group_snapshot_id = None + if parsed_args.source: + consistency_group_id = volume_client.find_consistency_group( + parsed_args.source, ignore_missing=False + ).id + elif parsed_args.snapshot: + consistency_group_snapshot_id = ( + volume_client.find_consistency_group_snapshot( + parsed_args.snapshot, ignore_missing=False + ).id + ) + + consistency_group = ( + volume_client.create_consistency_group_from_source( + consistency_group_snapshot=consistency_group_snapshot_id, + consistency_group=consistency_group_id, + name=parsed_args.name, + description=parsed_args.description, + ) + ) + + return _format_consistency_group(consistency_group) + + +class DeleteConsistencyGroup(command.Command): + _description = _("Delete consistency group(s).") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'consistency_groups', + metavar='', + nargs="+", + help=_('Consistency group(s) to delete (name or ID)'), + ) + parser.add_argument( + '--force', + action='store_true', + default=False, + help=_("Allow delete in state other than error or available"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + result = 0 + + for i in parsed_args.consistency_groups: + try: + consistency_group = volume_client.find_consistency_group( + i, ignore_missing=False + ) + volume_client.delete_consistency_group( + consistency_group, force=parsed_args.force + ) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete consistency group with " + "name or ID '%(consistency_group)s':%(e)s" + ), + {'consistency_group': i, 'e': e}, + ) + + if result > 0: + total = len(parsed_args.consistency_groups) + msg = _( + "%(result)s of %(total)s consistency groups failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListConsistencyGroup(command.Lister): + _description = _("List consistency groups.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--all-projects', + action="store_true", + help=_( + 'Show details for all projects. Admin only. ' + '(defaults to False)' + ), + ) + parser.add_argument( + '--long', + action="store_true", + help=_('List additional fields in output'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + + if parsed_args.long: + column_headers = [ + 'ID', + 'Status', + 'Availability Zone', + 'Name', + 'Description', + 'Volume Types', + ] + columns = [ + 'id', + 'status', + 'availability_zone', + 'name', + 'description', + 'volume_types', + ] + else: + column_headers = ['ID', 'Status', 'Name'] + columns = ['id', 'status', 'name'] + + consistency_groups = volume_client.consistency_groups( + all_tenants=parsed_args.all_projects, + ) + + return ( + column_headers, + ( + utils.get_item_properties( + s, + columns, + formatters={'volume_types': format_columns.ListColumn}, + ) + for s in consistency_groups + ), + ) + + +class RemoveVolumeFromConsistencyGroup(command.Command): + _description = _("Remove volume(s) from consistency group") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'consistency_group', + metavar="", + help=_('Consistency group containing (name or ID)'), + ) + parser.add_argument( + 'volumes', + metavar='', + nargs='+', + help=_( + 'Volume(s) to remove from (name or ID) ' + '(repeat option to remove multiple volumes)' + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + result, remove_uuid = _find_volumes(parsed_args.volumes, volume_client) + + if result > 0: + total = len(parsed_args.volumes) + LOG.error( + _("%(result)s of %(total)s volumes failed to remove."), + {'result': result, 'total': total}, + ) + + if remove_uuid: + remove_uuid = remove_uuid.rstrip(',') + consistency_group = volume_client.find_consistency_group( + parsed_args.consistency_group, ignore_missing=False + ) + volume_client.update_consistency_group( + consistency_group, remove_volumes=remove_uuid + ) + + +class SetConsistencyGroup(command.Command): + _description = _("Set consistency group properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'consistency_group', + metavar='', + help=_('Consistency group to modify (name or ID)'), + ) + parser.add_argument( + '--name', + metavar='', + help=_('New consistency group name'), + ) + parser.add_argument( + '--description', + metavar='', + help=_('New consistency group description'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + kwargs = {} + if parsed_args.name: + kwargs['name'] = parsed_args.name + if parsed_args.description: + kwargs['description'] = parsed_args.description + if kwargs: + consistency_group = volume_client.find_consistency_group( + parsed_args.consistency_group, ignore_missing=False + ) + volume_client.update_consistency_group(consistency_group, **kwargs) + + +class ShowConsistencyGroup(command.ShowOne): + _description = _("Display consistency group details.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "consistency_group", + metavar="", + help=_("Consistency group to display (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + consistency_group = volume_client.find_consistency_group( + parsed_args.consistency_group, ignore_missing=False + ) + return _format_consistency_group(consistency_group) diff --git a/openstackclient/volume/v3/consistency_group_snapshot.py b/openstackclient/volume/v3/consistency_group_snapshot.py new file mode 100644 index 0000000000..baeab172e2 --- /dev/null +++ b/openstackclient/volume/v3/consistency_group_snapshot.py @@ -0,0 +1,263 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Volume v3 consistency group snapshot action implementations""" + +import argparse +import logging +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack.block_storage.v3 import ( + consistency_group_snapshot as _cg_snapshot, +) +from openstack import utils as sdk_utils +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient import command +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +def _format_consistency_group_snapshot( + consistency_group_snapshot: _cg_snapshot.ConsistencyGroupSnapshot, +) -> tuple[Sequence[str], Iterable[Any]]: + columns = ( + 'consistencygroup_id', + 'created_at', + 'description', + 'id', + 'name', + 'status', + ) + return columns, utils.get_item_properties( + consistency_group_snapshot, columns + ) + + +class CreateConsistencyGroupSnapshot(command.ShowOne): + _description = _("Create new consistency group snapshot.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "snapshot_name", + metavar="", + nargs="?", + help=_("Name of new consistency group snapshot (default to None)"), + ) + parser.add_argument( + "--consistency-group", + metavar="", + help=_( + "Consistency group to snapshot (name or ID) " + "(default to be the same as )" + ), + ) + parser.add_argument( + "--description", + metavar="", + help=_("Description of this consistency group snapshot"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + consistency_group = parsed_args.consistency_group + if not parsed_args.consistency_group: + # If "--consistency-group" not specified, then consistency_group + # will be the same as the new consistency group snapshot name + consistency_group = parsed_args.snapshot_name + consistency_group_id = volume_client.find_consistency_group( + consistency_group, ignore_missing=False + ).id + consistency_group_snapshot = ( + volume_client.create_consistency_group_snapshot( + consistencygroup_id=consistency_group_id, + name=parsed_args.snapshot_name, + description=parsed_args.description, + ) + ) + + return _format_consistency_group_snapshot(consistency_group_snapshot) + + +class DeleteConsistencyGroupSnapshot(command.Command): + _description = _("Delete consistency group snapshot(s).") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "consistency_group_snapshot", + metavar="", + nargs="+", + help=_("Consistency group snapshot(s) to delete (name or ID)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + result = 0 + + for snapshot in parsed_args.consistency_group_snapshot: + try: + consistency_group_snapshot = ( + volume_client.find_consistency_group_snapshot( + snapshot, ignore_missing=False + ) + ) + volume_client.delete_consistency_group_snapshot( + consistency_group_snapshot + ) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete consistency group snapshot " + "with name or ID '%(snapshot)s': %(e)s" + ), + {'snapshot': snapshot, 'e': e}, + ) + + if result > 0: + total = len(parsed_args.consistency_group_snapshot) + msg = _( + "%(result)s of %(total)s consistency group snapshots " + "failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class ListConsistencyGroupSnapshot(command.Lister): + _description = _("List consistency group snapshots.") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--all-projects', + action="store_true", + help=_( + 'Show detail for all projects (admin only) (defaults to False)' + ), + ) + parser.add_argument( + '--long', + action="store_true", + help=_('List additional fields in output'), + ) + parser.add_argument( + '--status', + metavar="", + choices=[ + 'available', + 'error', + 'creating', + 'deleting', + 'error_deleting', + ], + help=_( + 'Filters results by a status ("available", "error", ' + '"creating", "deleting" or "error_deleting")' + ), + ) + parser.add_argument( + '--consistency-group', + metavar="", + help=_('Filters results by a consistency group (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + + if parsed_args.long: + column_headers = [ + 'ID', + 'Status', + 'ConsistencyGroup ID', + 'Name', + 'Description', + 'Created At', + ] + columns = [ + 'id', + 'status', + 'consistencygroup_id', + 'name', + 'description', + 'created_at', + ] + else: + column_headers = ['ID', 'Status', 'Name'] + columns = ['id', 'status', 'name'] + + consistency_group_id = None + if parsed_args.consistency_group: + consistency_group_id = volume_client.find_consistency_group( + parsed_args.consistency_group, ignore_missing=False + ).id + + consistency_group_snapshots = ( + volume_client.consistency_group_snapshots( + all_tenants=parsed_args.all_projects, + status=parsed_args.status, + consistencygroup_id=consistency_group_id, + ) + ) + + return ( + column_headers, + ( + utils.get_item_properties(s, columns) + for s in consistency_group_snapshots + ), + ) + + +class ShowConsistencyGroupSnapshot(command.ShowOne): + _description = _("Display consistency group snapshot details") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "consistency_group_snapshot", + metavar="", + help=_("Consistency group snapshot to display (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + consistency_group_snapshot = ( + volume_client.find_consistency_group_snapshot( + parsed_args.consistency_group_snapshot, ignore_missing=False + ) + ) + return _format_consistency_group_snapshot(consistency_group_snapshot) diff --git a/openstackclient/volume/v3/qos_specs.py b/openstackclient/volume/v3/qos_specs.py new file mode 100644 index 0000000000..f50fbb2009 --- /dev/null +++ b/openstackclient/volume/v3/qos_specs.py @@ -0,0 +1,396 @@ +# Copyright 2015 iWeb Technologies Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Volume v3 QoS action implementations""" + +import argparse +import logging +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack import utils as sdk_utils +from osc_lib.cli import format_columns +from osc_lib.cli import parseractions +from osc_lib import exceptions +from osc_lib import utils + +from openstackclient import command +from openstackclient.i18n import _ + + +LOG = logging.getLogger(__name__) + + +class AssociateQos(command.Command): + _description = _("Associate a QoS specification to a volume type") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'qos_spec', + metavar='', + help=_('QoS specification to modify (name or ID)'), + ) + parser.add_argument( + 'volume_type', + metavar='', + help=_('Volume type to associate the QoS (name or ID)'), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + qos_spec = volume_client.find_qos_spec( + parsed_args.qos_spec, ignore_missing=False + ) + volume_type = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False + ) + volume_client.associate_qos_spec(qos_spec.id, volume_type.id) + + +class CreateQos(command.ShowOne): + _description = _("Create new QoS specification") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'name', + metavar='', + help=_('New QoS specification name'), + ) + consumer_choices = ['front-end', 'back-end', 'both'] + parser.add_argument( + '--consumer', + metavar='', + choices=consumer_choices, + default='both', + help=( + _( + 'Consumer of the QoS. Valid consumers: %s ' + "(defaults to 'both')" + ) + % utils.format_list(consumer_choices) + ), + ) + parser.add_argument( + '--property', + metavar='', + dest='properties', + action=parseractions.KeyValueAction, + help=_( + 'Set a QoS specification property ' + '(repeat option to set multiple properties)' + ), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + attrs: dict[str, Any] = {'consumer': parsed_args.consumer} + if parsed_args.properties: + attrs.update(parsed_args.properties) + qos_spec = volume_client.create_qos_spec( + name=parsed_args.name, **attrs + ) + columns = ('consumer', 'id', 'name', 'properties') + data = ( + qos_spec.consumer, + qos_spec.id, + qos_spec.name, + format_columns.DictColumn(qos_spec.specs), + ) + return columns, data + + +class DeleteQos(command.Command): + _description = _("Delete QoS specification") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'qos_specs', + metavar='', + nargs="+", + help=_('QoS specification(s) to delete (name or ID)'), + ) + parser.add_argument( + '--force', + action='store_true', + default=False, + help=_("Allow to delete in-use QoS specification(s)"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + result = 0 + + for i in parsed_args.qos_specs: + try: + qos_spec = volume_client.find_qos_spec(i, ignore_missing=False) + volume_client.delete_qos_spec( + qos_spec.id, ignore_missing=False, force=parsed_args.force + ) + except Exception as e: + result += 1 + LOG.error( + _( + "Failed to delete QoS specification with " + "name or ID '%(qos)s': %(e)s" + ), + {'qos': i, 'e': e}, + ) + + if result > 0: + total = len(parsed_args.qos_specs) + msg = _( + "%(result)s of %(total)s QoS specifications failed to delete." + ) % {'result': result, 'total': total} + raise exceptions.CommandError(msg) + + +class DisassociateQos(command.Command): + _description = _("Disassociate a QoS specification from a volume type") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'qos_spec', + metavar='', + help=_('QoS specification to modify (name or ID)'), + ) + volume_type_group = parser.add_mutually_exclusive_group() + volume_type_group.add_argument( + '--volume-type', + metavar='', + help=_('Volume type to disassociate the QoS from (name or ID)'), + ) + volume_type_group.add_argument( + '--all', + action='store_true', + default=False, + help=_('Disassociate the QoS from every volume type'), + ) + + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + qos_spec = volume_client.find_qos_spec( + parsed_args.qos_spec, ignore_missing=False + ) + + if parsed_args.volume_type: + volume_type = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False + ) + volume_client.disassociate_qos_spec(qos_spec.id, volume_type.id) + elif parsed_args.all: + volume_client.disassociate_all_qos_spec(qos_spec.id) + + +class ListQos(command.Lister): + _description = _("List QoS specifications") + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + qos_specs_list = list(volume_client.qos_specs()) + + display_columns = ( + 'ID', + 'Name', + 'Consumer', + 'Associations', + 'Properties', + ) + + data = [] + for qos in qos_specs_list: + qos_associations = volume_client.qos_spec_associations(qos) + associations = [a.name for a in qos_associations] + data.append( + ( + qos.id, + qos.name, + qos.consumer, + format_columns.ListColumn(associations), + format_columns.DictColumn(qos.specs), + ) + ) + + return display_columns, iter(data) + + +class SetQos(command.Command): + _description = _("Set QoS specification properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'qos_spec', + metavar='', + help=_('QoS specification to modify (name or ID)'), + ) + parser.add_argument( + '--no-property', + dest='no_property', + action='store_true', + help=_( + 'Remove all properties from ' + '(specify both --no-property and --property to remove the ' + 'current properties before setting new properties)' + ), + ) + parser.add_argument( + '--property', + metavar='', + dest='properties', + action=parseractions.KeyValueAction, + help=_( + 'Property to add or modify for this QoS specification ' + '(repeat option to set multiple properties)' + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + qos_spec = volume_client.find_qos_spec( + parsed_args.qos_spec, ignore_missing=False + ) + + result = 0 + if parsed_args.no_property: + try: + key_list = list(qos_spec.specs.keys()) + volume_client.delete_qos_spec_metadata(qos_spec.id, key_list) + except Exception as e: + LOG.error(_("Failed to clean qos properties: %s"), e) + result += 1 + + if parsed_args.properties: + try: + volume_client.update_qos_spec( + qos_spec.id, **parsed_args.properties + ) + except Exception as e: + LOG.error(_("Failed to set qos property: %s"), e) + result += 1 + + if result > 0: + raise exceptions.CommandError( + _("One or more of the set operations failed") + ) + + +class ShowQos(command.ShowOne): + _description = _("Display QoS specification details") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'qos_spec', + metavar='', + help=_('QoS specification to display (name or ID)'), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + qos_spec = volume_client.find_qos_spec( + parsed_args.qos_spec, ignore_missing=False + ) + qos_associations = list(volume_client.qos_spec_associations(qos_spec)) + associations = [a.name for a in qos_associations] + if associations: + columns: tuple[str, ...] = ( + 'associations', + 'consumer', + 'id', + 'name', + 'properties', + ) + data: tuple[Any, ...] = ( + format_columns.ListColumn(associations), + qos_spec.consumer, + qos_spec.id, + qos_spec.name, + format_columns.DictColumn(qos_spec.specs), + ) + else: + columns = ('consumer', 'id', 'name', 'properties') + data = ( + qos_spec.consumer, + qos_spec.id, + qos_spec.name, + format_columns.DictColumn(qos_spec.specs), + ) + return columns, data + + +class UnsetQos(command.Command): + _description = _("Unset QoS specification properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'qos_spec', + metavar='', + help=_('QoS specification to modify (name or ID)'), + ) + parser.add_argument( + '--property', + metavar='', + action='append', + dest='properties', + default=[], + help=_( + 'Property to remove from the QoS specification. ' + '(repeat option to unset multiple properties)' + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + qos_spec = volume_client.find_qos_spec( + parsed_args.qos_spec, ignore_missing=False + ) + if parsed_args.properties: + volume_client.delete_qos_spec_metadata( + qos_spec.id, parsed_args.properties + ) diff --git a/openstackclient/volume/v3/service.py b/openstackclient/volume/v3/service.py index fe4db4c113..d85d42d8a1 100644 --- a/openstackclient/volume/v3/service.py +++ b/openstackclient/volume/v3/service.py @@ -14,37 +14,80 @@ """Service action implementations""" -from cinderclient import api_versions +import argparse +from collections.abc import Iterable +from typing import Any + +from openstack import utils as sdk_utils +from osc_lib import exceptions from osc_lib import utils -from openstackclient.volume.v2 import service as service_v2 +from openstackclient import command +from openstackclient.i18n import _ + +class ListService(command.Lister): + _description = _("List service command") -class ListService(service_v2.ListService): - def take_action(self, parsed_args): - service_client = self.app.client_manager.volume + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "--host", + metavar="", + help=_("List services on specified host (name only)"), + ) + parser.add_argument( + "--service", + metavar="", + help=_("List only specified service (name only)"), + ) + parser.add_argument( + "--long", + action="store_true", + default=False, + help=_("List additional fields in output"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - columns = [ + columns: tuple[str, ...] = ( + "binary", + "host", + "availability_zone", + "status", + "state", + "updated_at", + ) + column_names: tuple[str, ...] = ( "Binary", "Host", "Zone", "Status", "State", "Updated At", - ] + ) - if service_client.api_version >= api_versions.APIVersion('3.7'): - columns.append("Cluster") - if service_client.api_version >= api_versions.APIVersion('3.49'): - columns.append("Backend State") + if sdk_utils.supports_microversion(volume_client, '3.7'): + columns += ("cluster",) + column_names += ("Cluster",) + if sdk_utils.supports_microversion(volume_client, '3.49'): + columns += ("backend_state",) + column_names += ("Backend State",) if parsed_args.long: - columns.append("Disabled Reason") + columns += ("disabled_reason",) + column_names += ("Disabled Reason",) - data = service_client.services.list( - parsed_args.host, parsed_args.service + data = volume_client.services( + host=parsed_args.host, binary=parsed_args.service ) return ( - columns, + column_names, ( utils.get_item_properties( s, @@ -53,3 +96,61 @@ def take_action(self, parsed_args): for s in data ), ) + + +class SetService(command.Command): + _description = _("Set volume service properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "host", + metavar="", + help=_("Name of host"), + ) + parser.add_argument( + "service", + metavar="", + help=_("Name of service (Binary name)"), + ) + enabled_group = parser.add_mutually_exclusive_group() + enabled_group.add_argument( + "--enable", action="store_true", help=_("Enable volume service") + ) + enabled_group.add_argument( + "--disable", action="store_true", help=_("Disable volume service") + ) + parser.add_argument( + "--disable-reason", + metavar="", + help=_( + "Reason for disabling the service " + "(should be used with --disable option)" + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + if parsed_args.disable_reason and not parsed_args.disable: + msg = _( + "Cannot specify option --disable-reason without " + "--disable specified." + ) + raise exceptions.CommandError(msg) + + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + + service = volume_client.find_service( + parsed_args.service, ignore_missing=False, host=parsed_args.host + ) + + if parsed_args.enable: + service.enable(volume_client) + + if parsed_args.disable: + service.disable( + volume_client, + reason=parsed_args.disable_reason, + ) diff --git a/openstackclient/volume/v3/volume.py b/openstackclient/volume/v3/volume.py index 64aa5f076f..71cbb4fe00 100644 --- a/openstackclient/volume/v3/volume.py +++ b/openstackclient/volume/v3/volume.py @@ -15,23 +15,25 @@ """Volume V3 Volume action implementations""" import argparse -import copy +from collections.abc import Iterable, Sequence import functools import logging +from typing import Any from cliff import columns as cliff_columns +from openstack.block_storage.v3 import volume as _volume from openstack import exceptions as sdk_exceptions from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient.api import volume_v3 +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common -from openstackclient.volume.v2 import volume as volume_v2 LOG = logging.getLogger(__name__) @@ -42,14 +44,20 @@ class KeyValueHintAction(argparse.Action): APPEND_KEYS = ('same_host', 'different_host') - def __init__(self, *args, **kwargs): + def __init__(self, *args: Any, **kwargs: Any) -> None: self._key_value_action = parseractions.KeyValueAction(*args, **kwargs) self._key_value_append_action = parseractions.KeyValueAppendAction( *args, **kwargs ) super().__init__(*args, **kwargs) - def __call__(self, parser, namespace, values, option_string=None): + def __call__( + self, + parser: argparse.ArgumentParser, + namespace: argparse.Namespace, + values: Any, + option_string: str | None = None, + ) -> None: if values.startswith(self.APPEND_KEYS): self._key_value_append_action( parser, namespace, values, option_string=option_string @@ -60,7 +68,7 @@ def __call__(self, parser, namespace, values, option_string=None): ) -class AttachmentsColumn(cliff_columns.FormattableColumn): +class AttachmentsColumn(cliff_columns.FormattableColumn[list[Any]]): """Formattable column for attachments column. Unlike the parent FormattableColumn class, the initializer of the @@ -71,11 +79,13 @@ class takes server_cache as the second argument. ``functools.partial(AttachmentsColumn, server_cache)``. """ - def __init__(self, value, server_cache=None): + def __init__( + self, value: list[Any], server_cache: dict[str, Any] | None = None + ) -> None: super().__init__(value) self._server_cache = server_cache or {} - def human_readable(self): + def human_readable(self) -> str: """Return a formatted string of a volume's attached instances :rtype: a string of formatted instances @@ -91,11 +101,57 @@ def human_readable(self): return msg -class CreateVolume(volume_v2.CreateVolume): +def _format_volume(volume: _volume.Volume) -> dict[str, object]: + # Some columns returned by openstacksdk should not be shown because they're + # either irrelevant or duplicates + ignored_columns = { + # computed columns + 'location', + # create-only columns + 'OS-SCH-HNT:scheduler_hints', + 'imageRef', + # removed columns + 'os-volume-replication:driver_data', + 'os-volume-replication:extended_status', + # unnecessary columns + 'links', + } + optional_columns = { + # only present if part of a consistency group + 'consistencygroup_id', + # only present if the volume is encrypted + 'encryption_key_id', + # only present if there are image properties associated + 'volume_image_metadata', + } + + info = volume.to_dict(original_names=True) + data = {} + for key, value in info.items(): + if key in ignored_columns: + continue + + if key in optional_columns: + if info[key] is None: + continue + + data[key] = value + + data.update( + { + 'properties': format_columns.DictColumn(data.pop('metadata')), + 'type': data.pop('volume_type'), + } + ) + + return data + + +class CreateVolume(command.ShowOne): _description = _("Create new volume") @staticmethod - def _check_size_arg(args): + def _check_size_arg(args: argparse.Namespace) -> None: """Check whether --size option is required or not. Require size parameter in case if any of the following is not @@ -116,9 +172,49 @@ def _check_size_arg(args): ) raise exceptions.CommandError(msg) - def get_parser(self, prog_name): - parser, source_group = self._get_parser(prog_name) - + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "name", + metavar="", + nargs="?", + help=_("Volume name"), + ) + parser.add_argument( + "--size", + metavar="", + type=int, + help=_( + "Volume size in GB (required unless --snapshot or " + "--source specified)" + ), + ) + parser.add_argument( + "--type", + metavar="", + help=_("Set the type of volume"), + ) + source_group = parser.add_mutually_exclusive_group() + source_group.add_argument( + "--image", + metavar="", + help=_("Use as source of volume (name or ID)"), + ) + source_group.add_argument( + "--snapshot", + metavar="", + help=_("Use as source of volume (name or ID)"), + ) + source_group.add_argument( + "--source", + metavar="", + help=_("Volume to clone (name or ID)"), + ) + source_group.add_argument( + "--source-replicated", + metavar="", + help=argparse.SUPPRESS, + ) source_group.add_argument( "--backup", metavar="", @@ -138,6 +234,72 @@ def get_parser(self, prog_name): "--remote-source source-id=test_id')" ), ) + parser.add_argument( + "--description", + metavar="", + help=_("Volume description"), + ) + parser.add_argument( + "--availability-zone", + metavar="", + help=_("Create volume in "), + ) + parser.add_argument( + "--consistency-group", + metavar="consistency-group>", + help=_("Consistency group where the new volume belongs to"), + ) + parser.add_argument( + "--property", + metavar="", + action=parseractions.KeyValueAction, + dest="properties", + help=_( + "Set a property to this volume " + "(repeat option to set multiple properties)" + ), + ) + parser.add_argument( + "--hint", + metavar="", + action=KeyValueHintAction, + help=_( + "Arbitrary scheduler hint key-value pairs to help creating " + "a volume. Repeat the option to set multiple hints. " + "'same_host' and 'different_host' get values appended when " + "repeated, all other keys take the last given value" + ), + ) + bootable_group = parser.add_mutually_exclusive_group() + bootable_group.add_argument( + "--bootable", + action="store_true", + dest="bootable", + default=None, + help=_("Mark volume as bootable"), + ) + bootable_group.add_argument( + "--non-bootable", + action="store_false", + dest="bootable", + default=None, + help=_("Mark volume as non-bootable (default)"), + ) + readonly_group = parser.add_mutually_exclusive_group() + readonly_group.add_argument( + "--read-only", + action="store_true", + dest="read_only", + default=None, + help=_("Set volume to read-only access mode"), + ) + readonly_group.add_argument( + "--read-write", + action="store_false", + dest="read_only", + default=None, + help=_("Set volume to read-write access mode (default)"), + ) parser.add_argument( "--host", metavar="", @@ -159,16 +321,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - CreateVolume._check_size_arg(parsed_args) + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + self._check_size_arg(parsed_args) # size is validated in the above call to # _check_size_arg where we check that size # should be passed if we are not creating a # volume from snapshot, backup or source volume size = parsed_args.size - volume_client_sdk = self.app.client_manager.sdk_connection.volume - volume_client = self.app.client_manager.volume + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) image_client = self.app.client_manager.image if ( @@ -180,8 +345,8 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - if parsed_args.backup and not ( - volume_client.api_version.matches('3.47') + if parsed_args.backup and not sdk_utils.supports_microversion( + volume_client, '3.47' ): msg = _( "--os-volume-api-version 3.47 or greater is required " @@ -203,9 +368,7 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) if parsed_args.cluster: - if not sdk_utils.supports_microversion( - volume_client_sdk, '3.16' - ): + if not sdk_utils.supports_microversion(volume_client, '3.16'): msg = _( "--os-volume-api-version 3.16 or greater is required " "to support the cluster parameter." @@ -223,7 +386,7 @@ def take_action(self, parsed_args): "manage a volume." ) raise exceptions.CommandError(msg) - volume = volume_client_sdk.manage_volume( + volume = volume_client.manage_volume( host=parsed_args.host, cluster=parsed_args.cluster, ref=parsed_args.remote_source, @@ -234,21 +397,23 @@ def take_action(self, parsed_args): metadata=parsed_args.properties, bootable=parsed_args.bootable, ) - return zip(*sorted(volume.items())) + data = _format_volume(volume) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data source_volume = None if parsed_args.source: - source_volume_obj = utils.find_resource( - volume_client.volumes, parsed_args.source + source_volume_obj = volume_client.find_volume( + parsed_args.source, ignore_missing=False ) source_volume = source_volume_obj.id size = max(size or 0, source_volume_obj.size) consistency_group = None if parsed_args.consistency_group: - consistency_group = utils.find_resource( - volume_client.consistencygroups, parsed_args.consistency_group - ).id + consistency_group = volume_v3.find_consistency_group( + volume_client, parsed_args.consistency_group + )['id'] image = None if parsed_args.image: @@ -258,8 +423,8 @@ def take_action(self, parsed_args): snapshot = None if parsed_args.snapshot: - snapshot_obj = utils.find_resource( - volume_client.volume_snapshots, parsed_args.snapshot + snapshot_obj = volume_client.find_snapshot( + parsed_args.snapshot, ignore_missing=False ) snapshot = snapshot_obj.id # Cinder requires a value for size when creating a volume @@ -272,14 +437,14 @@ def take_action(self, parsed_args): backup = None if parsed_args.backup: - backup_obj = utils.find_resource( - volume_client.backups, parsed_args.backup + backup_obj = volume_client.find_backup( + parsed_args.backup, ignore_missing=False ) backup = backup_obj.id # As above size = max(size or 0, backup_obj.size) - volume = volume_client.volumes.create( + volume = volume_client.create_volume( size=size, snapshot_id=snapshot, name=parsed_args.name, @@ -287,9 +452,9 @@ def take_action(self, parsed_args): volume_type=parsed_args.type, availability_zone=parsed_args.availability_zone, metadata=parsed_args.properties, - imageRef=image, - source_volid=source_volume, - consistencygroup_id=consistency_group, + image_id=image, + source_volume_id=source_volume, + consistency_group_id=consistency_group, scheduler_hints=parsed_args.hint, backup_id=backup, ) @@ -297,14 +462,14 @@ def take_action(self, parsed_args): if parsed_args.bootable is not None: try: if utils.wait_for_status( - volume_client.volumes.get, + volume_client.get_volume, volume.id, success_status=['available'], error_status=['error'], sleep_time=1, ): - volume_client.volumes.set_bootable( - volume.id, parsed_args.bootable + volume_client.set_volume_bootable_status( + volume, parsed_args.bootable ) else: msg = _( @@ -317,14 +482,14 @@ def take_action(self, parsed_args): if parsed_args.read_only is not None: try: if utils.wait_for_status( - volume_client.volumes.get, + volume_client.get_volume, volume.id, success_status=['available'], error_status=['error'], sleep_time=1, ): - volume_client.volumes.update_readonly_flag( - volume.id, parsed_args.read_only + volume_client.set_volume_readonly( + volume, parsed_args.read_only ) else: msg = _( @@ -338,24 +503,45 @@ def take_action(self, parsed_args): e, ) - # Remove key links from being displayed - volume._info.update( - { - 'properties': format_columns.DictColumn( - volume._info.pop('metadata') - ), - 'type': volume._info.pop('volume_type'), - } - ) - volume._info.pop("links", None) - return zip(*sorted(volume._info.items())) + data = _format_volume(volume) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data -class DeleteVolume(volume_v2.DeleteVolume): +class DeleteVolume(command.Command): _description = _("Delete volume(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) + parser.add_argument( + "volumes", + metavar="", + nargs="+", + help=_("Volume(s) to delete (name or ID)"), + ) + group = parser.add_mutually_exclusive_group() + group.add_argument( + "--force", + action="store_true", + help=_( + "Attempt forced removal of volume(s), regardless of state " + "(defaults to False)" + ), + ) + group.add_argument( + "--cascade", + action="store_true", + help=_( + "Remove any snapshots along with volume(s) (defaults to False)" + ), + ) + group.add_argument( + # now called "cascade", accept old arg for compatibility + "--purge", + action="store_true", + help=argparse.SUPPRESS, + dest='cascade', + ) parser.add_argument( '--remote', action='store_true', @@ -363,28 +549,31 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume_client_sdk = self.app.client_manager.sdk_connection.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) result = 0 - if parsed_args.remote and (parsed_args.force or parsed_args.purge): + if parsed_args.remote and (parsed_args.force or parsed_args.cascade): msg = _( - "The --force and --purge options are not " + "The --force and --cascade options are not " "supported with the --remote parameter." ) raise exceptions.CommandError(msg) - for i in parsed_args.volumes: + for volume in parsed_args.volumes: try: - volume_obj = utils.find_resource(volume_client.volumes, i) + volume_obj = volume_client.find_volume( + volume, ignore_missing=False + ) if parsed_args.remote: - volume_client_sdk.unmanage_volume(volume_obj.id) - elif parsed_args.force: - volume_client.volumes.force_delete(volume_obj.id) + volume_client.unmanage_volume(volume_obj.id) else: - volume_client.volumes.delete( - volume_obj.id, cascade=parsed_args.purge + volume_client.delete_volume( + volume_obj.id, + force=parsed_args.force, + cascade=parsed_args.cascade, ) except Exception as e: result += 1 @@ -393,7 +582,7 @@ def take_action(self, parsed_args): "Failed to delete volume with " "name or ID '%(volume)s': %(e)s" ), - {'volume': i, 'e': e}, + {'volume': volume, 'e': e}, ) if result > 0: @@ -408,7 +597,7 @@ def take_action(self, parsed_args): class ListVolume(command.Lister): _description = _("List volumes") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--project', @@ -432,6 +621,16 @@ def get_parser(self, prog_name): metavar='', help=_('Filter results by status'), ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + dest='properties', + help=_( + 'Filter by a property on the volume list ' + '(repeat option to filter by multiple properties) ' + ), + ) parser.add_argument( '--all-projects', action='store_true', @@ -444,69 +643,70 @@ def get_parser(self, prog_name): default=False, help=_('List additional fields in output'), ) - pagination.add_marker_pagination_option_to_parser(parser) + pagination.add_marker_pagination_option_to_parser( + parser, include_max_items=False + ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + columns: tuple[str, ...] = ('id', 'name', 'status', 'size') + column_headers: tuple[str, ...] = ('ID', 'Name', 'Status', 'Size') if parsed_args.long: - columns = [ - 'ID', - 'Name', - 'Status', - 'Size', - 'Volume Type', - 'Bootable', - 'Attachments', - 'Metadata', - ] - column_headers = copy.deepcopy(columns) - column_headers[4] = 'Type' - column_headers[6] = 'Attached to' - column_headers[7] = 'Properties' - else: - columns = [ - 'ID', - 'Name', - 'Status', - 'Size', - 'Attachments', - ] - column_headers = copy.deepcopy(columns) - column_headers[4] = 'Attached to' - - project_id = None + columns += ('volume_type', 'is_bootable') + column_headers += ('Type', 'Bootable') + columns += ('attachments',) + column_headers += ('Attached to',) + if parsed_args.long: + columns += ('metadata',) + column_headers += ('Properties',) + + kwargs = {} + + if parsed_args.name: + kwargs['name'] = parsed_args.name + + if parsed_args.status: + kwargs['status'] = parsed_args.status + + if parsed_args.limit: + kwargs['limit'] = parsed_args.limit + + if parsed_args.marker: + kwargs['marker'] = parsed_args.marker + if parsed_args.project: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) + kwargs['project_id'] = project_id user_id = None if parsed_args.user: - user_id = identity_common.find_user( + user_id = identity_common.find_user_id_sdk( identity_client, parsed_args.user, parsed_args.user_domain - ).id + ) + kwargs['user_id'] = user_id + + if parsed_args.properties: + # yes, cinder really does expect a stringified dict... + kwargs['properties'] = str(parsed_args.properties) # set value of 'all_tenants' when using project option all_projects = bool(parsed_args.project) or parsed_args.all_projects + kwargs['all_projects'] = all_projects - search_opts = { - 'all_tenants': all_projects, - 'project_id': project_id, - 'user_id': user_id, - 'name': parsed_args.name, - 'status': parsed_args.status, - } - - data = volume_client.volumes.list( - search_opts=search_opts, - marker=parsed_args.marker, - limit=parsed_args.limit, - ) + data = list(volume_client.volumes(**kwargs)) do_server_list = False @@ -522,17 +722,14 @@ def take_action(self, parsed_args): compute_client = self.app.client_manager.compute for s in compute_client.servers(): server_cache[s.id] = s - except sdk_exceptions.SDKException: # noqa: S110 + except sdk_exceptions.SDKException: # Just forget it if there's any trouble pass + AttachmentsColumnWithCache = functools.partial( AttachmentsColumn, server_cache=server_cache ) - column_headers = utils.backward_compat_col_lister( - column_headers, parsed_args.columns, {'Display Name': 'Name'} - ) - return ( column_headers, ( @@ -540,8 +737,8 @@ def take_action(self, parsed_args): s, columns, formatters={ - 'Metadata': format_columns.DictColumn, - 'Attachments': AttachmentsColumnWithCache, + 'metadata': format_columns.DictColumn, + 'attachments': AttachmentsColumnWithCache, }, ) for s in data @@ -552,21 +749,29 @@ def take_action(self, parsed_args): class MigrateVolume(command.Command): _description = _("Migrate volume to a new host") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume', metavar="", help=_("Volume to migrate (name or ID)"), ) - parser.add_argument( + destination_group = parser.add_mutually_exclusive_group(required=True) + destination_group.add_argument( '--host', metavar="", - required=True, help=_( "Destination host (takes the form: host@backend-name#pool)" ), ) + destination_group.add_argument( + '--cluster', + metavar="", + help=_( + "Destination cluster to migrate the volume to " + "(requires --os-volume-api-version 3.16 or higher)" + ), + ) parser.add_argument( '--force-host-copy', action="store_true", @@ -586,21 +791,36 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume = utils.find_resource(volume_client.volumes, parsed_args.volume) - volume_client.volumes.migrate_volume( + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False + ) + + if parsed_args.cluster and not sdk_utils.supports_microversion( + volume_client, '3.16' + ): + msg = _( + "--os-volume-api-version 3.16 or greater is required to " + "support the volume migration with cluster" + ) + raise exceptions.CommandError(msg) + + volume_client.migrate_volume( volume.id, - parsed_args.host, - parsed_args.force_host_copy, - parsed_args.lock_volume, + host=parsed_args.host, + force_host_copy=parsed_args.force_host_copy, + lock_volume=parsed_args.lock_volume, + cluster=parsed_args.cluster, ) class SetVolume(command.Command): _description = _("Set volume properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume', @@ -753,9 +973,13 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume = utils.find_resource(volume_client.volumes, parsed_args.volume) + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False + ) result = 0 if parsed_args.retype_policy: @@ -785,31 +1009,31 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) if ( volume.status == 'in-use' - and not volume_client.api_version.matches('3.42') + and not sdk_utils.supports_microversion( + volume_client, '3.42' + ) ): msg = _( "--os-volume-api-version 3.42 or greater is " "required to extend in-use volumes." ) raise exceptions.CommandError(msg) - volume_client.volumes.extend(volume.id, parsed_args.size) + volume_client.extend_volume(volume, parsed_args.size) except Exception as e: LOG.error(_("Failed to set volume size: %s"), e) result += 1 if parsed_args.no_property: try: - volume_client.volumes.delete_metadata( - volume.id, volume.metadata.keys() - ) + volume_client.delete_volume_metadata(volume) except Exception as e: LOG.error(_("Failed to clean volume properties: %s"), e) result += 1 if parsed_args.properties: try: - volume_client.volumes.set_metadata( - volume.id, parsed_args.properties + volume_client.set_volume_metadata( + volume, **parsed_args.properties ) except Exception as e: LOG.error(_("Failed to set volume properties: %s"), e) @@ -817,8 +1041,8 @@ def take_action(self, parsed_args): if parsed_args.image_properties: try: - volume_client.volumes.set_image_metadata( - volume.id, parsed_args.image_properties + volume_client.set_volume_image_metadata( + volume, **parsed_args.image_properties ) except Exception as e: LOG.error(_("Failed to set image properties: %s"), e) @@ -826,15 +1050,17 @@ def take_action(self, parsed_args): if parsed_args.state: try: - volume_client.volumes.reset_state(volume.id, parsed_args.state) + volume_client.reset_volume_status( + volume, status=parsed_args.state + ) except Exception as e: LOG.error(_("Failed to set volume state: %s"), e) result += 1 if parsed_args.attached: try: - volume_client.volumes.reset_state( - volume.id, state=None, attach_status="attached" + volume_client.reset_volume_status( + volume, attach_status="attached" ) except Exception as e: LOG.error(_("Failed to set volume attach-status: %s"), e) @@ -842,8 +1068,8 @@ def take_action(self, parsed_args): if parsed_args.detached: try: - volume_client.volumes.reset_state( - volume.id, state=None, attach_status="detached" + volume_client.reset_volume_status( + volume, attach_status="detached" ) except Exception as e: LOG.error(_("Failed to set volume attach-status: %s"), e) @@ -851,8 +1077,8 @@ def take_action(self, parsed_args): if parsed_args.bootable is not None: try: - volume_client.volumes.set_bootable( - volume.id, parsed_args.bootable + volume_client.set_volume_bootable_status( + volume, parsed_args.bootable ) except Exception as e: LOG.error(_("Failed to set volume bootable property: %s"), e) @@ -860,8 +1086,8 @@ def take_action(self, parsed_args): if parsed_args.read_only is not None: try: - volume_client.volumes.update_readonly_flag( - volume.id, parsed_args.read_only + volume_client.set_volume_readonly( + volume, parsed_args.read_only ) except Exception as e: LOG.error( @@ -878,12 +1104,12 @@ def take_action(self, parsed_args): migration_policy = policy try: # find the volume type - volume_type = utils.find_resource( - volume_client.volume_types, parsed_args.type + volume_type = volume_client.find_type( + parsed_args.type, ignore_missing=False ) # reset to the new volume type - volume_client.volumes.retype( - volume.id, volume_type.id, migration_policy + volume_client.retype_volume( + volume, volume_type, migration_policy ) except Exception as e: LOG.error(_("Failed to set volume type: %s"), e) @@ -891,22 +1117,22 @@ def take_action(self, parsed_args): elif policy: # If the "--migration-policy" is specified without "--type" LOG.warning( - _("'%s' option will not work without '--type' option") - % ( + _("'%s' option will not work without '--type' option"), + ( '--migration-policy' if parsed_args.migration_policy else '--retype-policy' - ) + ), ) kwargs = {} if parsed_args.name: - kwargs['display_name'] = parsed_args.name + kwargs['name'] = parsed_args.name if parsed_args.description: - kwargs['display_description'] = parsed_args.description + kwargs['description'] = parsed_args.description if kwargs: try: - volume_client.volumes.update(volume.id, **kwargs) + volume_client.update_volume(volume, **kwargs) except Exception as e: LOG.error( _( @@ -926,7 +1152,7 @@ def take_action(self, parsed_args): class ShowVolume(command.ShowOne): _description = _("Display volume details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume', @@ -935,31 +1161,25 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume = utils.find_resource(volume_client.volumes, parsed_args.volume) - - # Special mapping for columns to make the output easier to read: - # 'metadata' --> 'properties' - # 'volume_type' --> 'type' - volume._info.update( - { - 'properties': format_columns.DictColumn( - volume._info.pop('metadata') - ), - 'type': volume._info.pop('volume_type'), - }, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False ) - # Remove key links from being displayed - volume._info.pop("links", None) - return zip(*sorted(volume._info.items())) + data = _format_volume(volume) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data class UnsetVolume(command.Command): _description = _("Unset volume properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume', @@ -988,15 +1208,19 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume = utils.find_resource(volume_client.volumes, parsed_args.volume) + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False + ) result = 0 if parsed_args.properties: try: - volume_client.volumes.delete_metadata( - volume.id, parsed_args.properties + volume_client.delete_volume_metadata( + volume.id, keys=parsed_args.properties ) except Exception as e: LOG.error(_("Failed to unset volume properties: %s"), e) @@ -1004,8 +1228,8 @@ def take_action(self, parsed_args): if parsed_args.image_properties: try: - volume_client.volumes.delete_image_metadata( - volume.id, parsed_args.image_properties + volume_client.delete_volume_image_metadata( + volume.id, keys=parsed_args.image_properties ) except Exception as e: LOG.error(_("Failed to unset image properties: %s"), e) @@ -1020,7 +1244,7 @@ def take_action(self, parsed_args): class VolumeSummary(command.ShowOne): _description = _("Show a summary of all volumes in this deployment.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--all-projects', @@ -1030,8 +1254,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.12'): msg = _( @@ -1070,7 +1298,7 @@ def take_action(self, parsed_args): class VolumeRevertToSnapshot(command.Command): _description = _("Revert a volume to a snapshot.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'snapshot', @@ -1082,8 +1310,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.40'): msg = _( diff --git a/openstackclient/volume/v3/volume_attachment.py b/openstackclient/volume/v3/volume_attachment.py index 5773a121cc..9ae70c3937 100644 --- a/openstackclient/volume/v3/volume_attachment.py +++ b/openstackclient/volume/v3/volume_attachment.py @@ -10,14 +10,18 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +from openstack.block_storage.v3 import attachment as _attachment from openstack import utils as sdk_utils from osc_lib.cli import format_columns -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import envvars from openstackclient.common import pagination from openstackclient.i18n import _ @@ -31,7 +35,9 @@ ) -def _format_attachment(attachment): +def _format_attachment( + attachment: _attachment.Attachment, +) -> tuple[tuple[str, ...], Iterable[Any]]: columns = ( 'id', 'volume_id', @@ -56,12 +62,12 @@ def _format_attachment(attachment): # VolumeAttachmentManager.create returns a dict while everything else # returns a VolumeAttachment object if isinstance(attachment, dict): - data = [] + data: tuple[Any, ...] = () for column in columns: if column == 'connection_info': - data.append(format_columns.DictColumn(attachment[column])) + data += (format_columns.DictColumn(attachment[column]),) continue - data.append(attachment[column]) + data += (attachment[column],) else: data = utils.get_item_properties( attachment, @@ -87,7 +93,7 @@ class CreateVolumeAttachment(command.ShowOne): add volume' command should be preferred. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume', @@ -170,8 +176,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) compute_client = self.app.client_manager.compute if not sdk_utils.supports_microversion(volume_client, '3.27'): @@ -248,7 +258,7 @@ class DeleteVolumeAttachment(command.Command): remove' command should be preferred. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'attachment', @@ -257,8 +267,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.27'): msg = _( @@ -279,7 +291,7 @@ class SetVolumeAttachment(command.ShowOne): connected to. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'attachment', @@ -331,8 +343,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.27'): msg = _( @@ -362,7 +378,7 @@ def take_action(self, parsed_args): class CompleteVolumeAttachment(command.Command): """Complete an attachment for a volume.""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'attachment', @@ -371,8 +387,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.44'): msg = _( @@ -387,7 +405,7 @@ def take_action(self, parsed_args): class ListVolumeAttachment(command.Lister): """Lists all volume attachments.""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--project', @@ -431,9 +449,15 @@ def get_parser(self, prog_name): # ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.27'): msg = _( @@ -444,11 +468,11 @@ def take_action(self, parsed_args): project_id = None if parsed_args.project: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) search_opts = { 'all_tenants': True if project_id else parsed_args.all_projects, @@ -458,13 +482,14 @@ def take_action(self, parsed_args): } # Update search option with `filters` # if AppendFilters.filters: - # search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) + # search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) # noqa: E501 # TODO(stephenfin): Implement sorting attachments = volume_client.attachments( search_opts=search_opts, marker=parsed_args.marker, limit=parsed_args.limit, + max_items=parsed_args.max_items, ) column_headers = ( @@ -489,7 +514,7 @@ def take_action(self, parsed_args): class ShowVolumeAttachment(command.ShowOne): """Show detailed information for a volume attachment.""" - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'attachment', @@ -498,8 +523,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.27'): msg = _( diff --git a/openstackclient/volume/v3/volume_backend.py b/openstackclient/volume/v3/volume_backend.py new file mode 100644 index 0000000000..76c0291ccb --- /dev/null +++ b/openstackclient/volume/v3/volume_backend.py @@ -0,0 +1,130 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. +# + +"""Storage backend action implementations""" + +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + +from openstack import utils as sdk_utils +from osc_lib.cli import format_columns +from osc_lib import utils + +from openstackclient import command +from openstackclient.i18n import _ + + +class ShowCapability(command.Lister): + _description = _("Show capability command") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "host", + metavar="", + help=_("List capabilities of specified host (host@backend-name)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + + columns = [ + 'Title', + 'Key', + 'Type', + 'Description', + ] + + data = volume_client.get_capabilities(parsed_args.host) + + # The get capabilities API is... interesting. We only want the names of + # the capabilities that can set for a backend through extra specs, so + # we need to extract out that part of the mess that is returned. + print_data = [] + keys = data.properties + for key in keys: + # Stuff the key into the details to make it easier to output + capability_data = data.properties[key] + capability_data['key'] = key + print_data.append(capability_data) + + return ( + columns, + ( + utils.get_dict_properties( + s, + columns, + ) + for s in print_data + ), + ) + + +class ListPool(command.Lister): + _description = _("List pool command") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "--long", + action="store_true", + default=False, + help=_("Show detailed information about pools."), + ) + # TODO(smcginnis): Starting with Cinder microversion 3.33, user is also + # able to pass in --filters with a = pair to filter on. + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + + if parsed_args.long: + columns = [ + 'name', + 'capabilities', + ] + + headers = [ + 'Name', + 'Capabilities', + ] + else: + columns = [ + 'Name', + ] + headers = columns + + data = volume_client.backend_pools(detailed=parsed_args.long) + formatters = {'capabilities': format_columns.DictColumn} + return ( + headers, + ( + utils.get_item_properties( + s, + columns, + formatters=formatters, + ) + for s in data + ), + ) diff --git a/openstackclient/volume/v3/volume_backup.py b/openstackclient/volume/v3/volume_backup.py index 9892eadb06..135838196d 100644 --- a/openstackclient/volume/v3/volume_backup.py +++ b/openstackclient/volume/v3/volume_backup.py @@ -14,25 +14,27 @@ """Volume v3 Backup action implementations""" +import argparse +from collections.abc import Iterable, Sequence import copy import functools import logging +from typing import Any -from cinderclient import api_versions from cliff import columns as cliff_columns from openstack import utils as sdk_utils from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -class VolumeIdColumn(cliff_columns.FormattableColumn): +class VolumeIdColumn(cliff_columns.FormattableColumn[str]): """Formattable column for volume ID column. Unlike the parent FormattableColumn class, the initializer of the @@ -43,11 +45,13 @@ class takes volume_cache as the second argument. ``functools.partial(VolumeIdColumn, volume_cache)``. """ - def __init__(self, value, volume_cache=None): + def __init__( + self, value: str, volume_cache: dict[str, Any] | None = None + ) -> None: super().__init__(value) self._volume_cache = volume_cache or {} - def human_readable(self): + def human_readable(self) -> str: """Return a volume name if available :rtype: either the volume ID or name @@ -62,7 +66,7 @@ def human_readable(self): class CreateVolumeBackup(command.ShowOne): _description = _("Create new volume backup") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "volume", @@ -126,8 +130,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) volume_id = volume_client.find_volume( parsed_args.volume, @@ -183,7 +191,7 @@ def take_action(self, parsed_args): class DeleteVolumeBackup(command.Command): _description = _("Delete volume backup(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "backups", @@ -199,8 +207,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) result = 0 for backup in parsed_args.backups: @@ -219,8 +229,8 @@ def take_action(self, parsed_args): _( "Failed to delete backup with " "name or ID '%(backup)s': %(e)s" - ) - % {'backup': backup, 'e': e} + ), + {'backup': backup, 'e': e}, ) if result > 0: @@ -235,8 +245,13 @@ def take_action(self, parsed_args): class ListVolumeBackup(command.Lister): _description = _("List volume backups") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) + parser.add_argument( + '--project', + metavar='', + help=_('Filter results by project (name or ID) (admin only)'), + ) parser.add_argument( "--long", action="store_true", @@ -295,8 +310,15 @@ def get_parser(self, prog_name): # ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) columns: tuple[str, ...] = ( 'id', @@ -333,6 +355,14 @@ def take_action(self, parsed_args): VolumeIdColumn, volume_cache=volume_cache ) + all_tenants = parsed_args.all_projects + project_id = None + if parsed_args.project: + all_tenants = True + project_id = identity_client.find_project( + parsed_args.project, ignore_missing=False + ).id + filter_volume_id = None if parsed_args.volume: try: @@ -361,9 +391,11 @@ def take_action(self, parsed_args): name=parsed_args.name, status=parsed_args.status, volume_id=filter_volume_id, - all_tenants=parsed_args.all_projects, + all_tenants=all_tenants, marker=marker_backup_id, limit=parsed_args.limit, + max_items=parsed_args.max_items, + project_id=project_id, ) return ( @@ -382,7 +414,7 @@ def take_action(self, parsed_args): class RestoreVolumeBackup(command.ShowOne): _description = _("Restore volume backup") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "backup", @@ -408,8 +440,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) columns: tuple[str, ...] = ( 'id', @@ -442,9 +478,7 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg % parsed_args.volume) restore = volume_client.restore_backup( - backup.id, - volume_id=volume_id, - name=volume_name, + backup.id, volume=volume_id, name=volume_name ) data = utils.get_dict_properties(restore, columns) @@ -454,7 +488,7 @@ def take_action(self, parsed_args): class SetVolumeBackup(command.Command): _description = _("Set volume backup properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "backup", @@ -511,14 +545,22 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - backup = utils.find_resource(volume_client.backups, parsed_args.backup) + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + + backup = volume_client.find_backup( + parsed_args.backup, + ignore_missing=False, + ) result = 0 if parsed_args.state: try: - volume_client.backups.reset_state(backup.id, parsed_args.state) + volume_client.reset_backup_status( + backup, status=parsed_args.state + ) except Exception as e: LOG.error(_("Failed to set backup state: %s"), e) result += 1 @@ -526,7 +568,7 @@ def take_action(self, parsed_args): kwargs = {} if parsed_args.name: - if volume_client.api_version < api_versions.APIVersion('3.9'): + if not sdk_utils.supports_microversion(volume_client, '3.9'): msg = _( '--os-volume-api-version 3.9 or greater is required to ' 'support the --name option' @@ -536,7 +578,7 @@ def take_action(self, parsed_args): kwargs['name'] = parsed_args.name if parsed_args.description: - if volume_client.api_version < api_versions.APIVersion('3.9'): + if not sdk_utils.supports_microversion(volume_client, '3.9'): msg = _( '--os-volume-api-version 3.9 or greater is required to ' 'support the --description option' @@ -546,7 +588,7 @@ def take_action(self, parsed_args): kwargs['description'] = parsed_args.description if parsed_args.no_property: - if volume_client.api_version < api_versions.APIVersion('3.43'): + if not sdk_utils.supports_microversion(volume_client, '3.43'): msg = _( '--os-volume-api-version 3.43 or greater is required to ' 'support the --no-property option' @@ -554,14 +596,14 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) if parsed_args.properties: - if volume_client.api_version < api_versions.APIVersion('3.43'): + if not sdk_utils.supports_microversion(volume_client, '3.43'): msg = _( '--os-volume-api-version 3.43 or greater is required to ' 'support the --property option' ) raise exceptions.CommandError(msg) - if volume_client.api_version >= api_versions.APIVersion('3.43'): + if sdk_utils.supports_microversion(volume_client, '3.43'): metadata = copy.deepcopy(backup.metadata) if parsed_args.no_property: @@ -572,7 +614,7 @@ def take_action(self, parsed_args): if kwargs: try: - volume_client.backups.update(backup.id, **kwargs) + volume_client.update_backup(backup, **kwargs) except Exception as e: LOG.error("Failed to update backup: %s", e) result += 1 @@ -588,7 +630,7 @@ class UnsetVolumeBackup(command.Command): This command requires ``--os-volume-api-version`` 3.43 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'backup', @@ -607,17 +649,21 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.43'): + if not sdk_utils.supports_microversion(volume_client, '3.43'): msg = _( '--os-volume-api-version 3.43 or greater is required to ' 'support the --property option' ) raise exceptions.CommandError(msg) - backup = utils.find_resource(volume_client.backups, parsed_args.backup) + backup = volume_client.find_backup( + parsed_args.backup, ignore_missing=False + ) metadata = copy.deepcopy(backup.metadata) for key in parsed_args.properties: @@ -632,17 +678,13 @@ def take_action(self, parsed_args): del metadata[key] - kwargs = { - 'metadata': metadata, - } - - volume_client.backups.update(backup.id, **kwargs) + volume_client.delete_backup_metadata(backup, keys=list(metadata)) class ShowVolumeBackup(command.ShowOne): _description = _("Display volume backup details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "backup", @@ -651,9 +693,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume - backup = volume_client.find_backup(parsed_args.backup) + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + backup = volume_client.find_backup( + parsed_args.backup, ignore_missing=False + ) columns: tuple[str, ...] = ( "availability_zone", "container", diff --git a/openstackclient/volume/v3/volume_group.py b/openstackclient/volume/v3/volume_group.py index f943b80302..b3c9989dce 100644 --- a/openstackclient/volume/v3/volume_group.py +++ b/openstackclient/volume/v3/volume_group.py @@ -11,17 +11,20 @@ # under the License. import argparse +from collections.abc import Iterable, Sequence +from typing import Any -from cinderclient import api_versions -from osc_lib.command import command +from openstack.block_storage.v3 import group as _group +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import envvars from openstackclient.i18n import _ -def _format_group(group): +def _format_group(group: _group.Group) -> tuple[Sequence[str], Iterable[Any]]: columns = ( 'id', 'status', @@ -79,7 +82,7 @@ class CreateVolumeGroup(command.ShowOne): This command requires ``--os-volume-api-version`` 3.13 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) # This is a bit complicated. We accept two patterns: a legacy pattern # @@ -168,8 +171,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if parsed_args.volume_group_type_legacy: msg = _( @@ -187,7 +194,7 @@ def take_action(self, parsed_args): volume_types.extend(parsed_args.volume_types_legacy) if volume_group_type: - if volume_client.api_version < api_versions.APIVersion('3.13'): + if not sdk_utils.supports_microversion(volume_client, '3.13'): msg = _( "--os-volume-api-version 3.13 or greater is required to " "support the 'volume group create' command" @@ -200,32 +207,27 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - volume_group_type_id = utils.find_resource( - volume_client.group_types, - volume_group_type, + volume_group_type_id = volume_client.find_group_type( + volume_group_type, ignore_missing=False ).id - volume_types_ids = [] - for volume_type in volume_types: - volume_types_ids.append( - utils.find_resource( - volume_client.volume_types, - volume_type, - ).id - ) - - group = volume_client.groups.create( - volume_group_type_id, - ','.join(volume_types_ids), - parsed_args.name, - parsed_args.description, + volume_types_ids = [ + volume_client.find_type(vt, ignore_missing=False).id + for vt in volume_types + ] + + group = volume_client.create_group( + group_type=volume_group_type_id, + volume_types=volume_types_ids, + name=parsed_args.name, + description=parsed_args.description, availability_zone=parsed_args.availability_zone, ) - group = volume_client.groups.get(group.id) + group = volume_client.get_group(group.id) return _format_group(group) else: - if volume_client.api_version < api_versions.APIVersion('3.14'): + if not sdk_utils.supports_microversion(volume_client, '3.14'): msg = _( "--os-volume-api-version 3.14 or greater is required to " "support the 'volume group create " @@ -250,23 +252,24 @@ def take_action(self, parsed_args): ) self.log.warning(msg) - source_group = None + source_group_id = None if parsed_args.source_group: - source_group = utils.find_resource( - volume_client.groups, parsed_args.source_group - ) - group_snapshot = None + source_group_id = volume_client.find_group( + parsed_args.source_group, ignore_missing=False + ).id + group_snapshot_id = None if parsed_args.group_snapshot: - group_snapshot = utils.find_resource( - volume_client.group_snapshots, parsed_args.group_snapshot - ) - group = volume_client.groups.create_from_src( - group_snapshot.id if group_snapshot else None, - source_group.id if source_group else None, - parsed_args.name, - parsed_args.description, + group_snapshot_id = volume_client.find_group_snapshot( + parsed_args.group_snapshot, ignore_missing=False + ).id + + group = volume_client.create_group_from_source( + group_snapshot_id=group_snapshot_id, + source_group_id=source_group_id, + name=parsed_args.name, + description=parsed_args.description, ) - group = volume_client.groups.get(group.id) + group = volume_client.get_group(group.id) return _format_group(group) @@ -276,7 +279,7 @@ class DeleteVolumeGroup(command.Command): This command requires ``--os-volume-api-version`` 3.13 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group', @@ -294,22 +297,24 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.13'): + if not sdk_utils.supports_microversion(volume_client, '3.13'): msg = _( "--os-volume-api-version 3.13 or greater is required to " "support the 'volume group delete' command" ) raise exceptions.CommandError(msg) - group = utils.find_resource( - volume_client.groups, + group = volume_client.find_group( parsed_args.group, + ignore_missing=False, ) - volume_client.groups.delete(group.id, delete_volumes=parsed_args.force) + volume_client.delete_group(group, delete_volumes=parsed_args.force) class SetVolumeGroup(command.ShowOne): @@ -318,7 +323,7 @@ class SetVolumeGroup(command.ShowOne): This command requires ``--os-volume-api-version`` 3.13 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group', @@ -335,7 +340,8 @@ def get_parser(self, prog_name): metavar='', help=_('New description for group.'), ) - parser.add_argument( + type_group = parser.add_mutually_exclusive_group() + type_group.add_argument( '--enable-replication', action='store_true', dest='enable_replication', @@ -345,7 +351,7 @@ def get_parser(self, prog_name): '(supported by --os-volume-api-version 3.38 or above)' ), ) - parser.add_argument( + type_group.add_argument( '--disable-replication', action='store_false', dest='enable_replication', @@ -356,23 +362,27 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.13'): + if not sdk_utils.supports_microversion(volume_client, '3.13'): msg = _( "--os-volume-api-version 3.13 or greater is required to " "support the 'volume group set' command" ) raise exceptions.CommandError(msg) - group = utils.find_resource( - volume_client.groups, + group = volume_client.find_group( parsed_args.group, + ignore_missing=False, ) if parsed_args.enable_replication is not None: - if volume_client.api_version < api_versions.APIVersion('3.38'): + if not sdk_utils.supports_microversion(volume_client, '3.38'): msg = _( "--os-volume-api-version 3.38 or greater is required to " "support the '--enable-replication' or " @@ -381,9 +391,9 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) if parsed_args.enable_replication: - volume_client.groups.enable_replication(group.id) + volume_client.enable_group_replication(group) else: - volume_client.groups.disable_replication(group.id) + volume_client.disable_group_replication(group) kwargs = {} @@ -394,7 +404,7 @@ def take_action(self, parsed_args): kwargs['description'] = parsed_args.description if kwargs: - group = volume_client.groups.update(group.id, **kwargs) + group = volume_client.update_group(group, **kwargs) return _format_group(group) @@ -405,7 +415,7 @@ class ListVolumeGroup(command.Lister): This command requires ``--os-volume-api-version`` 3.13 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--all-projects', @@ -430,21 +440,23 @@ def get_parser(self, prog_name): # ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.13'): + if not sdk_utils.supports_microversion(volume_client, '3.13'): msg = _( "--os-volume-api-version 3.13 or greater is required to " "support the 'volume group list' command" ) raise exceptions.CommandError(msg) - search_opts = { - 'all_tenants': parsed_args.all_projects, - } - - groups = volume_client.groups.list(search_opts=search_opts) + groups = list( + volume_client.groups(all_projects=parsed_args.all_projects) + ) column_headers = ( 'ID', @@ -469,7 +481,7 @@ class ShowVolumeGroup(command.ShowOne): This command requires ``--os-volume-api-version`` 3.13 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group', @@ -517,50 +529,46 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.13'): + if not sdk_utils.supports_microversion(volume_client, '3.13'): msg = _( "--os-volume-api-version 3.13 or greater is required to " "support the 'volume group show' command" ) raise exceptions.CommandError(msg) - kwargs = {} - if parsed_args.show_volumes is not None: - if volume_client.api_version < api_versions.APIVersion('3.25'): + if not sdk_utils.supports_microversion(volume_client, '3.25'): msg = _( "--os-volume-api-version 3.25 or greater is required to " "support the '--(no-)volumes' option" ) raise exceptions.CommandError(msg) - kwargs['list_volume'] = parsed_args.show_volumes - if parsed_args.show_replication_targets is not None: - if volume_client.api_version < api_versions.APIVersion('3.38'): + if not sdk_utils.supports_microversion(volume_client, '3.38'): msg = _( "--os-volume-api-version 3.38 or greater is required to " "support the '--(no-)replication-targets' option" ) raise exceptions.CommandError(msg) - group = utils.find_resource( - volume_client.groups, + group = volume_client.find_group( parsed_args.group, + ignore_missing=False, ) - group = volume_client.groups.show(group.id, **kwargs) - - if parsed_args.show_replication_targets: - replication_targets = ( - volume_client.groups.list_replication_targets(group.id) + if parsed_args.show_volumes is not None: + group = volume_client.get_group( + group.id, list_volume=parsed_args.show_volumes ) - group.replication_targets = replication_targets - # TODO(stephenfin): Show replication targets return _format_group(group) @@ -571,7 +579,7 @@ class FailoverVolumeGroup(command.Command): This command requires ``--os-volume-api-version`` 3.38 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group', @@ -599,23 +607,25 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.38'): + if not sdk_utils.supports_microversion(volume_client, '3.38'): msg = _( "--os-volume-api-version 3.38 or greater is required to " "support the 'volume group failover' command" ) raise exceptions.CommandError(msg) - group = utils.find_resource( - volume_client.groups, + group = volume_client.find_group( parsed_args.group, + ignore_missing=False, ) - volume_client.groups.failover_replication( - group.id, - allow_attached_volume=parsed_args.allow_attached_volume, + volume_client.failover_group_replication( + group, + allowed_attached_volume=parsed_args.allow_attached_volume, secondary_backend_id=parsed_args.secondary_backend_id, ) diff --git a/openstackclient/volume/v3/volume_group_snapshot.py b/openstackclient/volume/v3/volume_group_snapshot.py index 5a760ac9bf..7e11f989b8 100644 --- a/openstackclient/volume/v3/volume_group_snapshot.py +++ b/openstackclient/volume/v3/volume_group_snapshot.py @@ -10,20 +10,26 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +from openstack.block_storage.v3 import group_snapshot as _group_snapshot from openstack import utils as sdk_utils -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import envvars from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -def _format_group_snapshot(snapshot): +def _format_group_snapshot( + snapshot: _group_snapshot.GroupSnapshot, +) -> tuple[tuple[str, ...], Iterable[Any]]: columns = ( 'id', 'status', @@ -43,10 +49,7 @@ def _format_group_snapshot(snapshot): return ( column_headers, - utils.get_item_properties( - snapshot, - columns, - ), + utils.get_item_properties(snapshot, columns), ) @@ -56,7 +59,7 @@ class CreateVolumeGroupSnapshot(command.ShowOne): This command requires ``--os-volume-api-version`` 3.13 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume_group', @@ -75,8 +78,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.14'): msg = _( @@ -106,7 +113,7 @@ class DeleteVolumeGroupSnapshot(command.Command): This command requires ``--os-volume-api-version`` 3.14 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'snapshot', @@ -115,8 +122,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.14'): msg = _( @@ -140,7 +149,7 @@ class ListVolumeGroupSnapshot(command.Lister): This command requires ``--os-volume-api-version`` 3.14 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--all-projects', @@ -165,8 +174,12 @@ def get_parser(self, prog_name): # ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.14'): msg = _( @@ -202,7 +215,7 @@ class ShowVolumeGroupSnapshot(command.ShowOne): This command requires ``--os-volume-api-version`` 3.14 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'snapshot', @@ -211,8 +224,12 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.sdk_connection.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if not sdk_utils.supports_microversion(volume_client, '3.14'): msg = _( diff --git a/openstackclient/volume/v3/volume_group_type.py b/openstackclient/volume/v3/volume_group_type.py index f1ef617409..4bb394daaa 100644 --- a/openstackclient/volume/v3/volume_group_type.py +++ b/openstackclient/volume/v3/volume_group_type.py @@ -10,21 +10,27 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from cinderclient import api_versions +from openstack.block_storage.v3 import group_type as _group_type +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ LOG = logging.getLogger(__name__) -def _format_group_type(group): +def _format_group_type( + group_type: _group_type.GroupType, +) -> tuple[Sequence[str], Iterable[Any]]: columns = ( 'id', 'name', @@ -40,16 +46,12 @@ def _format_group_type(group): 'Properties', ) - # TODO(stephenfin): Consider using a formatter for volume_types since it's - # a list return ( column_headers, utils.get_item_properties( - group, + group_type, columns, - formatters={ - 'group_specs': format_columns.DictColumn, - }, + formatters={'group_specs': format_columns.DictColumn}, ), ) @@ -60,7 +62,7 @@ class CreateVolumeGroupType(command.ShowOne): This command requires ``--os-volume-api-version`` 3.11 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'name', @@ -90,18 +92,24 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.11'): + if not sdk_utils.supports_microversion(volume_client, '3.11'): msg = _( "--os-volume-api-version 3.11 or greater is required to " "support the 'volume group type create' command" ) raise exceptions.CommandError(msg) - group_type = volume_client.group_types.create( - parsed_args.name, parsed_args.description, parsed_args.is_public + group_type = volume_client.create_group_type( + name=parsed_args.name, + description=parsed_args.description, + is_public=parsed_args.is_public, ) return _format_group_type(group_type) @@ -113,7 +121,7 @@ class DeleteVolumeGroupType(command.Command): This command requires ``--os-volume-api-version`` 3.11 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group_type', @@ -122,22 +130,24 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.11'): + if not sdk_utils.supports_microversion(volume_client, '3.11'): msg = _( "--os-volume-api-version 3.11 or greater is required to " "support the 'volume group type delete' command" ) raise exceptions.CommandError(msg) - group_type = utils.find_resource( - volume_client.group_types, + group_type = volume_client.find_group_type( parsed_args.group_type, + ignore_missing=False, ) - volume_client.group_types.delete(group_type.id) + volume_client.delete_group_type(group_type) class SetVolumeGroupType(command.ShowOne): @@ -146,7 +156,7 @@ class SetVolumeGroupType(command.ShowOne): This command requires ``--os-volume-api-version`` 3.11 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group_type', @@ -199,19 +209,23 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.11'): + if not sdk_utils.supports_microversion(volume_client, '3.11'): msg = _( "--os-volume-api-version 3.11 or greater is required to " "support the 'volume group type set' command" ) raise exceptions.CommandError(msg) - group_type = utils.find_resource( - volume_client.group_types, + group_type = volume_client.find_group_type( parsed_args.group_type, + ignore_missing=False, ) kwargs = {} @@ -228,7 +242,7 @@ def take_action(self, parsed_args): if kwargs: try: - group_type = volume_client.group_types.update( + group_type = volume_client.update_group_type( group_type.id, **kwargs ) except Exception as e: @@ -237,22 +251,31 @@ def take_action(self, parsed_args): if parsed_args.no_property: try: - keys = group_type.get_keys().keys() - group_type.unset_keys(keys) + for key in group_type.group_specs: + volume_client.delete_group_type_group_specs_property( + group_type.id, key + ) except Exception as e: LOG.error(_("Failed to clear group type properties: %s"), e) errors += 1 if parsed_args.properties: try: - group_type.set_keys(parsed_args.properties) + volume_client.create_group_type_group_specs( + group_type.id, parsed_args.properties + ) except Exception as e: LOG.error(_("Failed to set group type properties: %s"), e) errors += 1 if errors > 0: msg = _("Command Failed: One or more of the operations failed") - raise exceptions.CommandError() + raise exceptions.CommandError(msg) + + group_type = volume_client.find_group_type( + parsed_args.group_type, + ignore_missing=False, + ) return _format_group_type(group_type) @@ -263,7 +286,7 @@ class UnsetVolumeGroupType(command.ShowOne): This command requires ``--os-volume-api-version`` 3.11 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group_type', @@ -282,26 +305,28 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.11'): + if not sdk_utils.supports_microversion(volume_client, '3.11'): msg = _( "--os-volume-api-version 3.11 or greater is required to " "support the 'volume group type unset' command" ) raise exceptions.CommandError(msg) - group_type = utils.find_resource( - volume_client.group_types, - parsed_args.group_type, - ) - - group_type.unset_keys(parsed_args.properties) + for key in parsed_args.properties or []: + volume_client.delete_group_type_group_specs_property( + parsed_args.group_type, key + ) - group_type = utils.find_resource( - volume_client.group_types, + group_type = volume_client.find_group_type( parsed_args.group_type, + ignore_missing=False, ) return _format_group_type(group_type) @@ -313,7 +338,7 @@ class ListVolumeGroupType(command.Lister): This command requires ``--os-volume-api-version`` 3.11 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--default', @@ -338,10 +363,14 @@ def get_parser(self, prog_name): # ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.11'): + if not sdk_utils.supports_microversion(volume_client, '3.11'): msg = _( "--os-volume-api-version 3.11 or greater is required to " "support the 'volume group type list' command" @@ -349,26 +378,33 @@ def take_action(self, parsed_args): raise exceptions.CommandError(msg) if parsed_args.show_default: - group_types = [volume_client.group_types.default()] + group_types = [volume_client.get_group_type('default')] else: - group_types = volume_client.group_types.list() + group_types = list(volume_client.group_types()) column_headers = ( 'ID', 'Name', 'Is Public', - 'Properties', + 'Group Specs', ) columns = ( - 'id', - 'name', - 'is_public', - 'group_specs', + 'ID', + 'Name', + 'Is Public', + 'Group Specs', ) return ( column_headers, - (utils.get_item_properties(a, columns) for a in group_types), + ( + utils.get_item_properties( + a, + columns, + formatters={'Group Specs': format_columns.DictColumn}, + ) + for a in group_types + ), ) @@ -378,7 +414,7 @@ class ShowVolumeGroupType(command.ShowOne): This command requires ``--os-volume-api-version`` 3.11 or greater. """ - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'group_type', @@ -387,19 +423,23 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.11'): + if not sdk_utils.supports_microversion(volume_client, '3.11'): msg = _( "--os-volume-api-version 3.11 or greater is required to " "support the 'volume group type show' command" ) raise exceptions.CommandError(msg) - group_type = utils.find_resource( - volume_client.group_types, - parsed_args.group, + group_type = volume_client.find_group_type( + parsed_args.group_type, + ignore_missing=False, ) return _format_group_type(group_type) diff --git a/openstackclient/volume/v3/volume_host.py b/openstackclient/volume/v3/volume_host.py new file mode 100644 index 0000000000..d05643bd2d --- /dev/null +++ b/openstackclient/volume/v3/volume_host.py @@ -0,0 +1,83 @@ +# +# Licensed under the Apache License, Version 2.0 (the "License"); you may +# not use this file except in compliance with the License. You may obtain +# a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT +# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the +# License for the specific language governing permissions and limitations +# under the License. + +"""Volume v3 host action implementations""" + +import argparse + +from openstack.block_storage.v3 import service as _service +from openstack import utils as sdk_utils + +from openstackclient import command +from openstackclient.i18n import _ + + +class FailoverVolumeHost(command.Command): + _description = _("Failover volume host to different backend") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "host", metavar="", help=_("Name of volume host") + ) + parser.add_argument( + "--volume-backend", + metavar="", + required=True, + help=_( + "The ID of the volume backend replication " + "target where the host will failover to (required)" + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + service = _service.Service(host=parsed_args.host) + volume_client.failover_service( + service, backend_id=parsed_args.volume_backend + ) + + +class SetVolumeHost(command.Command): + _description = _("Set volume host properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "host", metavar="", help=_("Name of volume host") + ) + enabled_group = parser.add_mutually_exclusive_group() + enabled_group.add_argument( + "--disable", + action="store_true", + help=_("Freeze and disable the specified volume host"), + ) + enabled_group.add_argument( + "--enable", + action="store_true", + help=_("Thaw and enable the specified volume host"), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + service = _service.Service(host=parsed_args.host) + if parsed_args.enable: + volume_client.thaw_service(service) + if parsed_args.disable: + volume_client.freeze_service(service) diff --git a/openstackclient/volume/v3/volume_message.py b/openstackclient/volume/v3/volume_message.py index 0fc724a586..03a6ca5d23 100644 --- a/openstackclient/volume/v3/volume_message.py +++ b/openstackclient/volume/v3/volume_message.py @@ -14,22 +14,56 @@ """Volume V3 Messages implementations""" -import logging as LOG +import argparse +from collections.abc import Iterable, Sequence +import logging +from typing import Any -from cinderclient import api_versions -from osc_lib.command import command +from openstack.block_storage.v3 import message as _message +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.common import pagination from openstackclient.i18n import _ from openstackclient.identity import common as identity_common +LOG = logging.getLogger(__name__) + + +def _format_message( + message: _message.Message, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: + column_headers = ( + 'Created At', + 'Event ID', + 'Guaranteed Until', + 'ID', + 'Message Level', + 'Request ID', + 'Resource Type', + 'Resource UUID', + 'User Message', + ) + columns = ( + 'created_at', + 'event_id', + 'guaranteed_until', + 'id', + 'message_level', + 'request_id', + 'resource_type', + 'resource_uuid', + 'user_message', + ) + return column_headers, utils.get_item_properties(message, columns) + class DeleteMessage(command.Command): _description = _('Delete a volume failure message') - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'message_ids', @@ -40,10 +74,12 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.3'): + if not sdk_utils.supports_microversion(volume_client, '3.3'): msg = _( "--os-volume-api-version 3.3 or greater is required to " "support the 'volume message delete' command" @@ -53,7 +89,7 @@ def take_action(self, parsed_args): errors = 0 for message_id in parsed_args.message_ids: try: - volume_client.messages.delete(message_id) + volume_client.delete_message(message_id, ignore_missing=False) except Exception: LOG.error(_('Failed to delete message: %s'), message_id) errors += 1 @@ -70,7 +106,7 @@ def take_action(self, parsed_args): class ListMessages(command.Lister): _description = _('List volume failure messages') - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( @@ -79,15 +115,23 @@ def get_parser(self, prog_name): help=_('Filter results by project (name or ID) (admin only)'), ) identity_common.add_project_domain_option_to_parser(parser) - pagination.add_marker_pagination_option_to_parser(parser) + pagination.add_marker_pagination_option_to_parser( + parser, include_max_items=False + ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - identity_client = self.app.client_manager.identity + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.3'): + if not sdk_utils.supports_microversion(volume_client, '3.3'): msg = _( "--os-volume-api-version 3.3 or greater is required to " "support the 'volume message list' command" @@ -105,34 +149,42 @@ def take_action(self, parsed_args): 'Created At', 'Guaranteed Until', ) + columns = ( + 'id', + 'event_id', + 'resource_type', + 'resource_uuid', + 'message_level', + 'user_message', + 'request_id', + 'created_at', + 'guaranteed_until', + ) project_id = None if parsed_args.project: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id + ) - search_opts = { - 'project_id': project_id, - } - data = volume_client.messages.list( - search_opts=search_opts, + data = volume_client.messages( + project_id=project_id, marker=parsed_args.marker, limit=parsed_args.limit, ) return ( column_headers, - (utils.get_item_properties(s, column_headers) for s in data), + (utils.get_item_properties(s, columns) for s in data), ) class ShowMessage(command.ShowOne): _description = _('Show a volume failure message') - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'message_id', @@ -142,16 +194,20 @@ def get_parser(self, prog_name): return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) - if volume_client.api_version < api_versions.APIVersion('3.3'): + if not sdk_utils.supports_microversion(volume_client, '3.3'): msg = _( "--os-volume-api-version 3.3 or greater is required to " "support the 'volume message show' command" ) raise exceptions.CommandError(msg) - message = volume_client.messages.get(parsed_args.message_id) + message = volume_client.get_message(parsed_args.message_id) - return zip(*sorted(message._info.items())) + return _format_message(message) diff --git a/openstackclient/volume/v3/volume_snapshot.py b/openstackclient/volume/v3/volume_snapshot.py index bb7f5a660e..e1d68ed693 100644 --- a/openstackclient/volume/v3/volume_snapshot.py +++ b/openstackclient/volume/v3/volume_snapshot.py @@ -14,21 +14,206 @@ """Volume v3 snapshot action implementations""" +import argparse +from collections.abc import Iterable, Sequence +import functools import logging +from typing import Any -from osc_lib.command import command +from cliff import columns as cliff_columns +from openstack.block_storage.v3 import snapshot as _snapshot +from openstack import utils as sdk_utils +from osc_lib.cli import format_columns +from osc_lib.cli import parseractions from osc_lib import exceptions from osc_lib import utils +from openstackclient import command +from openstackclient.common import pagination from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) +class VolumeIdColumn(cliff_columns.FormattableColumn[str]): + """Formattable column for volume ID column. + + Unlike the parent FormattableColumn class, the initializer of the + class takes volume_cache as the second argument. + osc_lib.utils.get_item_properties instantiate cliff FormattableColumn + object with a single parameter "column value", so you need to pass + a partially initialized class like + ``functools.partial(VolumeIdColumn, volume_cache)``. + """ + + def __init__( + self, value: str, volume_cache: dict[str, Any] | None = None + ) -> None: + super().__init__(value) + self._volume_cache = volume_cache or {} + + def human_readable(self) -> str: + """Return a volume name if available + + :rtype: either the volume ID or name + """ + volume_id = self._value + volume = volume_id + if volume_id in self._volume_cache.keys(): + volume = self._volume_cache[volume_id].name + return volume + + +def _format_snapshot(snapshot: _snapshot.Snapshot) -> dict[str, object]: + # Some columns returned by openstacksdk should not be shown because they're + # either irrelevant or duplicates + ignored_columns = { + # computed columns + 'location', + # create-only columns + 'consumes_quota', + 'force', + 'group_snapshot_id', + # ignored columns + 'os-extended-snapshot-attributes:progress', + 'os-extended-snapshot-attributes:project_id', + 'updated_at', + 'user_id', + # unnecessary columns + 'links', + } + + info = snapshot.to_dict(original_names=True) + data = {} + for key, value in info.items(): + if key in ignored_columns: + continue + + data[key] = value + + data.update( + { + 'properties': format_columns.DictColumn(data.pop('metadata')), + } + ) + + return data + + +class CreateVolumeSnapshot(command.ShowOne): + _description = _("Create new volume snapshot") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "snapshot_name", + metavar="", + help=_("Name of the new snapshot"), + ) + parser.add_argument( + "--volume", + metavar="", + help=_( + "Volume to snapshot (name or ID) (default is )" + ), + ) + parser.add_argument( + "--description", + metavar="", + help=_("Description of the snapshot"), + ) + parser.add_argument( + "--force", + action="store_true", + default=False, + help=_( + "Allow snapshot of in-use (attached) volume. " + "Only needed for microversions prior to 3.66; " + "ignored for 3.66+" + ), + ) + parser.add_argument( + "--property", + metavar="", + dest='properties', + action=parseractions.KeyValueAction, + help=_( + "Set a property to this snapshot " + "(repeat option to set multiple properties)" + ), + ) + parser.add_argument( + "--remote-source", + metavar="", + action=parseractions.KeyValueAction, + help=_( + "The attribute(s) of the existing remote volume snapshot " + "(admin required) (repeat option to specify multiple " + "attributes) e.g.: '--remote-source source-name=test_name " + "--remote-source source-id=test_id'" + ), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + + volume = parsed_args.volume + if not parsed_args.volume: + volume = parsed_args.snapshot_name + volume_id = volume_client.find_volume(volume, ignore_missing=False).id + + if parsed_args.remote_source: + # Create a new snapshot from an existing remote snapshot source + if parsed_args.force: + msg = _( + "'--force' option will not work when you create " + "new volume snapshot from an existing remote " + "volume snapshot" + ) + LOG.warning(msg) + + snapshot = volume_client.manage_snapshot( + volume_id=volume_id, + ref=parsed_args.remote_source, + name=parsed_args.snapshot_name, + description=parsed_args.description, + metadata=parsed_args.properties, + ) + else: + # Create a new snapshot from scratch + # only for microversion < 3.66, pass force parameter + # for backward compatibility + if not sdk_utils.supports_microversion(volume_client, '3.66'): + snapshot = volume_client.create_snapshot( + volume_id=volume_id, + force=parsed_args.force, + name=parsed_args.snapshot_name, + description=parsed_args.description, + metadata=parsed_args.properties, + ) + else: + snapshot = volume_client.create_snapshot( + volume_id=volume_id, + name=parsed_args.snapshot_name, + description=parsed_args.description, + metadata=parsed_args.properties, + ) + + data = _format_snapshot(snapshot) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data + + class DeleteVolumeSnapshot(command.Command): _description = _("Delete volume snapshot(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "snapshots", @@ -54,10 +239,10 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume_client_sdk = self.app.client_manager.sdk_connection.volume - + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) result = 0 if parsed_args.remote: @@ -68,16 +253,16 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - for i in parsed_args.snapshots: + for snapshot in parsed_args.snapshots: try: - snapshot_id = utils.find_resource( - volume_client.volume_snapshots, i + snapshot_id = volume_client.find_snapshot( + snapshot, ignore_missing=False ).id if parsed_args.remote: - volume_client_sdk.unmanage_snapshot(snapshot_id) + volume_client.unmanage_snapshot(snapshot_id) else: - volume_client.volume_snapshots.delete( - snapshot_id, parsed_args.force + volume_client.delete_snapshot( + snapshot_id, force=parsed_args.force ) except Exception as e: result += 1 @@ -85,8 +270,8 @@ def take_action(self, parsed_args): _( "Failed to delete snapshot with " "name or ID '%(snapshot)s': %(e)s" - ) - % {'snapshot': i, 'e': e} + ), + {'snapshot': snapshot, 'e': e}, ) if result > 0: @@ -96,3 +281,333 @@ def take_action(self, parsed_args): 'total': total, } raise exceptions.CommandError(msg) + + +class ListVolumeSnapshot(command.Lister): + _description = _("List volume snapshots") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + '--all-projects', + action='store_true', + default=False, + help=_('Include all projects (admin only)'), + ) + parser.add_argument( + '--project', + metavar='', + help=_('Filter results by project (name or ID) (admin only)'), + ) + identity_common.add_project_domain_option_to_parser(parser) + parser.add_argument( + '--long', + action='store_true', + default=False, + help=_('List additional fields in output'), + ) + parser.add_argument( + '--name', + metavar='', + default=None, + help=_('Filters results by a name.'), + ) + parser.add_argument( + '--status', + metavar='', + choices=[ + 'available', + 'error', + 'creating', + 'deleting', + 'error_deleting', + ], + help=_( + "Filters results by a status. " + "('available', 'error', 'creating', 'deleting'" + " or 'error_deleting')" + ), + ) + parser.add_argument( + '--volume', + metavar='', + default=None, + help=_('Filters results by a volume (name or ID).'), + ) + pagination.add_marker_pagination_option_to_parser(parser) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[tuple[str, ...], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + + columns: tuple[str, ...] = ( + 'id', + 'name', + 'description', + 'status', + 'size', + ) + column_headers: tuple[str, ...] = ( + 'ID', + 'Name', + 'Description', + 'Status', + 'Size', + ) + if parsed_args.long: + columns += ( + 'created_at', + 'volume_id', + 'metadata', + ) + column_headers += ( + 'Created At', + 'Volume', + 'Properties', + ) + + # Cache the volume list + volume_cache = {} + try: + for s in volume_client.volumes(): + volume_cache[s.id] = s + except Exception: # noqa: S110 + # Just forget it if there's any trouble + pass + _VolumeIdColumn = functools.partial( + VolumeIdColumn, volume_cache=volume_cache + ) + + volume_id = None + if parsed_args.volume: + volume_id = volume_client.find_volume( + parsed_args.volume, ignore_missing=False + ).id + + project_id = None + if parsed_args.project: + project_id = identity_common.find_project_id_sdk( + identity_client, + parsed_args.project, + parsed_args.project_domain, + ) + + # set value of 'all_tenants' when using project option + all_projects = ( + True if parsed_args.project else parsed_args.all_projects + ) + + data = volume_client.snapshots( + marker=parsed_args.marker, + limit=parsed_args.limit, + max_items=parsed_args.max_items, + all_projects=all_projects, + project_id=project_id, + name=parsed_args.name, + status=parsed_args.status, + volume_id=volume_id, + ) + return ( + column_headers, + ( + utils.get_item_properties( + s, + columns, + formatters={ + 'metadata': format_columns.DictColumn, + 'volume_id': _VolumeIdColumn, + }, + ) + for s in data + ), + ) + + +class SetVolumeSnapshot(command.Command): + _description = _("Set volume snapshot properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='', + help=_('Snapshot to modify (name or ID)'), + ) + parser.add_argument( + '--name', metavar='', help=_('New snapshot name') + ) + parser.add_argument( + '--description', + metavar='', + help=_('New snapshot description'), + ) + parser.add_argument( + "--no-property", + dest="no_property", + action="store_true", + help=_( + "Remove all properties from " + "(specify both --no-property and --property to " + "remove the current properties before setting " + "new properties.)" + ), + ) + parser.add_argument( + '--property', + metavar='', + action=parseractions.KeyValueAction, + dest='properties', + help=_( + 'Property to add/change for this snapshot ' + '(repeat option to set multiple properties)' + ), + ) + parser.add_argument( + '--state', + metavar='', + choices=[ + 'available', + 'error', + 'creating', + 'deleting', + 'error_deleting', + ], + help=_( + 'New snapshot state. ("available", "error", "creating", ' + '"deleting", or "error_deleting") (admin only) ' + '(This option simply changes the state of the snapshot ' + 'in the database with no regard to actual status, ' + 'exercise caution when using)' + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + + snapshot = volume_client.find_snapshot( + parsed_args.snapshot, ignore_missing=False + ) + + result = 0 + if parsed_args.no_property: + try: + volume_client.delete_snapshot_metadata( + snapshot.id, keys=list(snapshot.metadata) + ) + except Exception as e: + LOG.error(_("Failed to clean snapshot properties: %s"), e) + result += 1 + + if parsed_args.properties: + try: + volume_client.set_snapshot_metadata( + snapshot.id, **parsed_args.properties + ) + except Exception as e: + LOG.error(_("Failed to set snapshot property: %s"), e) + result += 1 + + if parsed_args.state: + try: + volume_client.reset_snapshot_status( + snapshot.id, parsed_args.state + ) + except Exception as e: + LOG.error(_("Failed to set snapshot state: %s"), e) + result += 1 + + kwargs = {} + if parsed_args.name: + kwargs['name'] = parsed_args.name + if parsed_args.description: + kwargs['description'] = parsed_args.description + if kwargs: + try: + volume_client.update_snapshot(snapshot.id, **kwargs) + except Exception as e: + LOG.error( + _("Failed to update snapshot name or description: %s"), + e, + ) + result += 1 + + if result > 0: + raise exceptions.CommandError( + _("One or more of the set operations failed") + ) + + +class ShowVolumeSnapshot(command.ShowOne): + _description = _("Display volume snapshot details") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + "snapshot", + metavar="", + help=_("Snapshot to display (name or ID)"), + ) + return parser + + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + + snapshot = volume_client.find_snapshot( + parsed_args.snapshot, ignore_missing=False + ) + + data = _format_snapshot(snapshot) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data + + +class UnsetVolumeSnapshot(command.Command): + _description = _("Unset volume snapshot properties") + + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: + parser = super().get_parser(prog_name) + parser.add_argument( + 'snapshot', + metavar='', + help=_('Snapshot to modify (name or ID)'), + ) + parser.add_argument( + '--property', + metavar='', + dest='properties', + action='append', + default=[], + help=_( + 'Property to remove from snapshot ' + '(repeat option to remove multiple properties)' + ), + ) + return parser + + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + + snapshot = volume_client.find_snapshot( + parsed_args.snapshot, ignore_missing=False + ) + + if parsed_args.properties: + volume_client.delete_snapshot_metadata( + snapshot.id, keys=parsed_args.properties + ) diff --git a/openstackclient/volume/v3/volume_transfer_request.py b/openstackclient/volume/v3/volume_transfer_request.py index b7add12280..ec353e87b6 100644 --- a/openstackclient/volume/v3/volume_transfer_request.py +++ b/openstackclient/volume/v3/volume_transfer_request.py @@ -14,13 +14,17 @@ """Volume v3 transfer action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any -from cinderclient import api_versions -from osc_lib.command import command +from openstack import exceptions as sdk_exceptions +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ @@ -30,7 +34,7 @@ class AcceptTransferRequest(command.ShowOne): _description = _("Accept volume transfer request.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'transfer_request', @@ -45,31 +49,33 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) try: - transfer_request_id = utils.find_resource( - volume_client.transfers, parsed_args.transfer_request + transfer_request_id = volume_client.find_transfer( + parsed_args.transfer_request, ignore_missing=False ).id - except exceptions.CommandError: + except sdk_exceptions.ResourceNotFound: # Non-admin users will fail to lookup name -> ID so we just # move on and attempt with the user-supplied information transfer_request_id = parsed_args.transfer_request - transfer_accept = volume_client.transfers.accept( - transfer_request_id, - parsed_args.auth_key, + result = volume_client.accept_transfer( + transfer_request_id, parsed_args.auth_key ) - transfer_accept._info.pop("links", None) - - return zip(*sorted(transfer_accept._info.items())) + columns = ('id', 'name', 'volume_id') + data = (result.id, result.name, result.volume_id) + return columns, data class CreateTransferRequest(command.ShowOne): _description = _("Create volume transfer request.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--name', @@ -102,13 +108,16 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - - kwargs = {} + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + attrs: dict[str, Any] = {} if parsed_args.snapshots is not None: - if volume_client.api_version < api_versions.APIVersion('3.55'): + if not sdk_utils.supports_microversion(volume_client, '3.55'): msg = _( "--os-volume-api-version 3.55 or greater is required to " "support the '--(no-)snapshots' option" @@ -117,26 +126,31 @@ def take_action(self, parsed_args): # unfortunately this option is negative so we have to reverse # things - kwargs['no_snapshots'] = not parsed_args.snapshots + attrs['no_snapshots'] = not parsed_args.snapshots - volume_id = utils.find_resource( - volume_client.volumes, - parsed_args.volume, - ).id - volume_transfer_request = volume_client.transfers.create( - volume_id, - parsed_args.name, - **kwargs, + volume = volume_client.find_volume( + parsed_args.volume, ignore_missing=False ) - volume_transfer_request._info.pop("links", None) - - return zip(*sorted(volume_transfer_request._info.items())) + result = volume_client.create_transfer( + volume_id=volume.id, + name=parsed_args.name, + **attrs, + ) + columns = ('auth_key', 'created_at', 'id', 'name', 'volume_id') + data = ( + result.auth_key, + result.created_at, + result.id, + result.name, + result.volume_id, + ) + return columns, data class DeleteTransferRequest(command.Command): _description = _("Delete volume transfer request(s).") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'transfer_request', @@ -146,25 +160,23 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) result = 0 for t in parsed_args.transfer_request: try: - transfer_request_id = utils.find_resource( - volume_client.transfers, - t, - ).id - volume_client.transfers.delete(transfer_request_id) + volume_client.delete_transfer(t, ignore_missing=False) except Exception as e: result += 1 LOG.error( _( "Failed to delete volume transfer request " "with name or ID '%(transfer)s': %(e)s" - ) - % {'transfer': t, 'e': e} + ), + {'transfer': t, 'e': e}, ) if result > 0: @@ -179,7 +191,7 @@ def take_action(self, parsed_args): class ListTransferRequest(command.Lister): _description = _("Lists all volume transfer requests.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--all-projects', @@ -190,30 +202,29 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - columns = ['ID', 'Name', 'Volume ID'] - column_headers = ['ID', 'Name', 'Volume'] - - volume_client = self.app.client_manager.volume - - volume_transfer_result = volume_client.transfers.list( - detailed=True, - search_opts={'all_tenants': parsed_args.all_projects}, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' ) + column_headers = ('ID', 'Name', 'Volume') + columns = ('id', 'name', 'volume_id') + results = volume_client.transfers( + details=True, + all_projects=parsed_args.all_projects, + ) return ( column_headers, - ( - utils.get_item_properties(s, columns) - for s in volume_transfer_result - ), + (utils.get_item_properties(s, columns) for s in results), ) class ShowTransferRequest(command.ShowOne): _description = _("Show volume transfer request details.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'transfer_request', @@ -222,12 +233,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume_transfer_request = utils.find_resource( - volume_client.transfers, - parsed_args.transfer_request, + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' ) - volume_transfer_request._info.pop("links", None) - - return zip(*sorted(volume_transfer_request._info.items())) + result = volume_client.find_transfer( + parsed_args.transfer_request, ignore_missing=False + ) + columns = ('created_at', 'id', 'name', 'volume_id') + data = (result.created_at, result.id, result.name, result.volume_id) + return columns, data diff --git a/openstackclient/volume/v3/volume_type.py b/openstackclient/volume/v3/volume_type.py index b5b328cb82..264ea14929 100644 --- a/openstackclient/volume/v3/volume_type.py +++ b/openstackclient/volume/v3/volume_type.py @@ -10,21 +10,26 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# """Volume v3 Type action implementations""" +import argparse +from collections.abc import Iterable, MutableMapping, Sequence import functools import logging +from typing import Any -from cinderclient import api_versions from cliff import columns as cliff_columns +from openstack.block_storage import v3 as _volume_v3 +from openstack.block_storage.v3 import type as _type +from openstack import exceptions as sdk_exceptions +from openstack import utils as sdk_utils from osc_lib.cli import format_columns from osc_lib.cli import parseractions -from osc_lib.command import command from osc_lib import exceptions from osc_lib import utils +from openstackclient import command from openstackclient.i18n import _ from openstackclient.identity import common as identity_common @@ -32,7 +37,41 @@ LOG = logging.getLogger(__name__) -class EncryptionInfoColumn(cliff_columns.FormattableColumn): +def _format_type(volume_type: _type.Type) -> dict[str, Any]: + ignored_columns = {'location'} + info = volume_type.to_dict() + data: dict[str, Any] = {} + for key, value in info.items(): + if key in ignored_columns: + continue + data[key] = value + + data['properties'] = format_columns.DictColumn(data.pop('extra_specs', {})) + + return data + + +def _format_encryption(encryption: _type.TypeEncryption) -> dict[str, Any]: + ignored_columns = { + 'id', + 'location', + 'name', + 'deleted', + 'created_at', + 'updated_at', + 'deleted_at', + 'volume_type_id', + } + info = encryption.to_dict() + data: dict[str, Any] = {} + for key, value in info.items(): + if key in ignored_columns or value is None: + continue + data[key] = value + return data + + +class EncryptionInfoColumn(cliff_columns.FormattableColumn[Any]): """Formattable column for encryption info column. Unlike the parent FormattableColumn class, the initializer of the @@ -43,73 +82,92 @@ class takes encryption_data as the second argument. ``functools.partial(EncryptionInfoColumn encryption_data)``. """ - def __init__(self, value, encryption_data=None): + def __init__( + self, value: Any, encryption_data: dict[str, Any] | None = None + ) -> None: super().__init__(value) self._encryption_data = encryption_data or {} - def _get_encryption_info(self): + def _get_encryption_info(self) -> Any: type_id = self._value return self._encryption_data.get(type_id) - def human_readable(self): + def human_readable(self) -> str: encryption_info = self._get_encryption_info() if encryption_info: return utils.format_dict(encryption_info) else: return '-' - def machine_readable(self): + def machine_readable(self) -> Any: return self._get_encryption_info() -def _create_encryption_type(volume_client, volume_type, parsed_args): +def _create_encryption_type( + volume_client: _volume_v3.Proxy, + volume_type: _type.Type, + parsed_args: argparse.Namespace, +) -> _type.TypeEncryption: if not parsed_args.encryption_provider: msg = _( "'--encryption-provider' should be specified while " "creating a new encryption type" ) raise exceptions.CommandError(msg) + # set the default of control location while creating control_location = 'front-end' if parsed_args.encryption_control_location: control_location = parsed_args.encryption_control_location - body = { - 'provider': parsed_args.encryption_provider, - 'cipher': parsed_args.encryption_cipher, - 'key_size': parsed_args.encryption_key_size, - 'control_location': control_location, - } - encryption = volume_client.volume_encryption_types.create( - volume_type, body + + encryption = volume_client.create_type_encryption( + volume_type, + provider=parsed_args.encryption_provider, + cipher=parsed_args.encryption_cipher, + key_size=parsed_args.encryption_key_size, + control_location=control_location, ) return encryption -def _set_encryption_type(volume_client, volume_type, parsed_args): - # update the existing encryption type - body = {} +def _set_encryption_type( + volume_client: _volume_v3.Proxy, + volume_type: _type.Type, + parsed_args: argparse.Namespace, +) -> None: + kwargs = {} for attr in ['provider', 'cipher', 'key_size', 'control_location']: info = getattr(parsed_args, 'encryption_' + attr, None) if info is not None: - body[attr] = info + kwargs[attr] = info + try: - volume_client.volume_encryption_types.update(volume_type, body) - except Exception as e: - if type(e).__name__ == 'NotFound': - # create new encryption type - LOG.warning( - _( - "No existing encryption type found, creating " - "new encryption type for this volume type ..." - ) + volume_client.update_type_encryption( + encryption=None, + volume_type=volume_type, + **kwargs, + ) + except (sdk_exceptions.NotFoundException, sdk_exceptions.InvalidRequest): + # No existing encryption: create a new one + if not parsed_args.encryption_provider: + msg = _( + "'--encryption-provider' should be specified while " + "creating a new encryption type" ) - _create_encryption_type(volume_client, volume_type, parsed_args) + raise exceptions.CommandError(msg) + LOG.warning( + _( + "No existing encryption type found, creating " + "new encryption type for this volume type ..." + ) + ) + _create_encryption_type(volume_client, volume_type, parsed_args) class CreateVolumeType(command.ShowOne): _description = _("Create new volume type") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "name", @@ -172,7 +230,8 @@ def get_parser(self, prog_name): default=False, help=_( "Enabled replication for this volume type " - "(this is an alias for '--property replication_enabled= True') " + "(this is an alias for " + "'--property replication_enabled= True') " "(requires driver support)" ), ) @@ -182,7 +241,8 @@ def get_parser(self, prog_name): dest='availability_zones', help=_( "Set an availability zone for this volume type " - "(this is an alias for '--property RESKEY:availability_zones:') " + "(this is an alias for " + "'--property RESKEY:availability_zones:') " "(repeat option to set multiple availability zones)" ), ) @@ -241,9 +301,15 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) if parsed_args.project and parsed_args.is_public is not False: msg = _("--project is only allowed with --private") @@ -254,28 +320,27 @@ def take_action(self, parsed_args): if parsed_args.is_public is not None: kwargs['is_public'] = parsed_args.is_public - volume_type = volume_client.volume_types.create( - parsed_args.name, + volume_type = volume_client.create_type( + name=parsed_args.name, description=parsed_args.description, **kwargs, ) - volume_type._info.pop('extra_specs') + + info = _format_type(volume_type) if parsed_args.project: try: - project_id = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, - ).id - volume_client.volume_type_access.add_project_access( - volume_type.id, project_id ) + volume_client.add_type_access(volume_type.id, project_id) except Exception as e: msg = _( "Failed to add project %(project)s access to type: %(e)s" ) - LOG.error(msg % {'project': parsed_args.project, 'e': e}) + LOG.error(msg, {'project': parsed_args.project, 'e': e}) properties = {} if parsed_args.properties: @@ -291,10 +356,10 @@ def take_action(self, parsed_args): parsed_args.availability_zones ) if properties: - result = volume_type.set_keys(properties) - volume_type._info.update( - {'properties': format_columns.DictColumn(result)} + result = volume_client.update_type_extra_specs( + volume_type.id, **properties ) + info['properties'] = format_columns.DictColumn(result.extra_specs) if ( parsed_args.encryption_provider @@ -316,20 +381,17 @@ def take_action(self, parsed_args): e, ) # add encryption info in result - encryption._info.pop("volume_type_id", None) - volume_type._info.update( - {'encryption': format_columns.DictColumn(encryption._info)} - ) + encryption_info = _format_encryption(encryption) + info['encryption'] = format_columns.DictColumn(encryption_info) - volume_type._info.pop("os-volume-type-access:is_public", None) - - return zip(*sorted(volume_type._info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class DeleteVolumeType(command.Command): _description = _("Delete volume type(s)") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "volume_types", @@ -339,25 +401,26 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) result = 0 for volume_type in parsed_args.volume_types: try: - vol_type = utils.find_resource( - volume_client.volume_types, volume_type + vol_type = volume_client.find_type( + volume_type, ignore_missing=False ) - - volume_client.volume_types.delete(vol_type) + volume_client.delete_type(vol_type) except Exception as e: result += 1 LOG.error( _( "Failed to delete volume type with " "name or ID '%(volume_type)s': %(e)s" - ) - % {'volume_type': volume_type, 'e': e} + ), + {'volume_type': volume_type, 'e': e}, ) if result > 0: @@ -371,7 +434,7 @@ def take_action(self, parsed_args): class ListVolumeType(command.Lister): _description = _("List volume types") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( '--long', @@ -390,14 +453,14 @@ def get_parser(self, prog_name): "--public", action="store_true", dest="is_public", - default=None, + default='none', # cinder expects the string "none" help=_("List only public types"), ) public_group.add_argument( "--private", action="store_false", dest="is_public", - default=None, + default='none', # cinder expects the string "none" help=_("List only private types (admin only)"), ) parser.add_argument( @@ -447,7 +510,8 @@ def get_parser(self, prog_name): default=False, help=_( "List only volume types with replication enabled " - "(this is an alias for '--property replication_enabled= True') " + "(this is an alias for " + "'--property replication_enabled= True') " "(supported by --os-volume-api-version 3.52 or above)" ), ) @@ -457,14 +521,19 @@ def get_parser(self, prog_name): dest='availability_zones', help=_( "List only volume types with this availability configured " - "(this is an alias for '--property RESKEY:availability_zones:') " + "(this is an alias for " + "'--property RESKEY:availability_zones:') " "(repeat option to filter on multiple availability zones)" ), ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) if parsed_args.long: columns = [ @@ -486,7 +555,7 @@ def take_action(self, parsed_args): column_headers = ['ID', 'Name', 'Is Public'] if parsed_args.default: - data = [volume_client.volume_types.default()] + data = [volume_client.get_type('default')] else: search_opts = {} properties = {} @@ -503,7 +572,7 @@ def take_action(self, parsed_args): parsed_args.availability_zones ) if properties: - if volume_client.api_version < api_versions.APIVersion('3.52'): + if not sdk_utils.supports_microversion(volume_client, '3.52'): msg = _( "--os-volume-api-version 3.52 or greater is required " "to use the '--property' option or any of the alias " @@ -513,29 +582,26 @@ def take_action(self, parsed_args): search_opts['extra_specs'] = properties - data = volume_client.volume_types.list( - search_opts=search_opts, - is_public=parsed_args.is_public, + data = list( + volume_client.types( + is_public=parsed_args.is_public, **properties + ) ) - formatters = {'Extra Specs': format_columns.DictColumn} + formatters: MutableMapping[str, Any] = { + 'Extra Specs': format_columns.DictColumn + } if parsed_args.encryption_type: encryption = {} - for d in volume_client.volume_encryption_types.list(): - volume_type_id = d._info['volume_type_id'] - # remove some redundant information - del_key = [ - 'deleted', - 'created_at', - 'updated_at', - 'deleted_at', - 'volume_type_id', - ] - for key in del_key: - d._info.pop(key, None) - # save the encryption information with their volume type ID - encryption[volume_type_id] = d._info + for d in data: + e = volume_client.get_type_encryption(d.id) + if not e.volume_type_id: + continue + + encryption_info = _format_encryption(e) + encryption[e.volume_type_id] = encryption_info + # We need to get volume type ID, then show encryption # information according to the ID, so use "id" to keep # difference to the real "ID" column. @@ -563,7 +629,7 @@ def take_action(self, parsed_args): class SetVolumeType(command.Command): _description = _("Set volume type properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume_type', @@ -616,7 +682,8 @@ def get_parser(self, prog_name): default=False, help=_( "Enabled replication for this volume type " - "(this is an alias for '--property replication_enabled= True') " + "(this is an alias for " + "'--property replication_enabled= True') " "(requires driver support)" ), ) @@ -626,7 +693,8 @@ def get_parser(self, prog_name): dest='availability_zones', help=_( "Set an availability zone for this volume type " - "(this is an alias for '--property RESKEY:availability_zones:') " + "(this is an alias for " + "'--property RESKEY:availability_zones:') " "(repeat option to set multiple availability zones)" ), ) @@ -700,13 +768,16 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - volume_type = utils.find_resource( - volume_client.volume_types, - parsed_args.volume_type, + volume_type = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False ) result = 0 @@ -723,7 +794,7 @@ def take_action(self, parsed_args): if kwargs: try: - volume_client.volume_types.update(volume_type.id, **kwargs) + volume_client.update_type(volume_type.id, **kwargs) except Exception as e: LOG.error( _("Failed to update volume type name or description: %s"), @@ -746,23 +817,21 @@ def take_action(self, parsed_args): ) if properties: try: - volume_type.set_keys(properties) + volume_client.update_type_extra_specs( + volume_type.id, **properties + ) except Exception as e: LOG.error(_("Failed to set volume type properties: %s"), e) result += 1 if parsed_args.project: - project_info = None try: - project_info = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, ) - - volume_client.volume_type_access.add_project_access( - volume_type.id, project_info.id - ) + volume_client.add_type_access(volume_type.id, project_id) except Exception as e: LOG.error( _("Failed to set volume type access to project: %s"), e @@ -796,7 +865,7 @@ def take_action(self, parsed_args): class ShowVolumeType(command.ShowOne): _description = _("Display volume type details") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( "volume_type", @@ -813,19 +882,22 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - volume_type = utils.find_resource( - volume_client.volume_types, parsed_args.volume_type + def take_action( + self, parsed_args: argparse.Namespace + ) -> tuple[Sequence[str], Iterable[Any]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' ) - properties = format_columns.DictColumn( - volume_type._info.pop('extra_specs', {}) + volume_type = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False ) - volume_type._info.update({'properties': properties}) + + info = _format_type(volume_type) + access_project_ids = None if not volume_type.is_public: try: - volume_type_access = volume_client.volume_type_access.list( + volume_type_access = volume_client.get_type_access( volume_type.id ) project_ids = [ @@ -840,18 +912,15 @@ def take_action(self, parsed_args): 'Failed to get access project list for volume type ' '%(type)s: %(e)s' ) - LOG.error(msg % {'type': volume_type.id, 'e': e}) - volume_type._info.update({'access_project_ids': access_project_ids}) + LOG.error(msg, {'type': volume_type.id, 'e': e}) + info['access_project_ids'] = access_project_ids + if parsed_args.encryption_type: # show encryption type information for this volume type try: - encryption = volume_client.volume_encryption_types.get( - volume_type.id - ) - encryption._info.pop("volume_type_id", None) - volume_type._info.update( - {'encryption': format_columns.DictColumn(encryption._info)} - ) + encryption = volume_client.get_type_encryption(volume_type.id) + encryption_info = _format_encryption(encryption) + info['encryption'] = format_columns.DictColumn(encryption_info) except Exception as e: LOG.error( _( @@ -860,14 +929,14 @@ def take_action(self, parsed_args): ), e, ) - volume_type._info.pop("os-volume-type-access:is_public", None) - return zip(*sorted(volume_type._info.items())) + col_headers, col_data = zip(*sorted(info.items())) + return col_headers, col_data class UnsetVolumeType(command.Command): _description = _("Unset volume type properties") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'volume_type', @@ -902,35 +971,36 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - volume_client = self.app.client_manager.volume - identity_client = self.app.client_manager.identity + def take_action(self, parsed_args: argparse.Namespace) -> None: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) - volume_type = utils.find_resource( - volume_client.volume_types, - parsed_args.volume_type, + volume_type = volume_client.find_type( + parsed_args.volume_type, ignore_missing=False ) result = 0 if parsed_args.properties: try: - volume_type.unset_keys(parsed_args.properties) + volume_client.delete_type_extra_specs( + volume_type.id, parsed_args.properties + ) except Exception as e: LOG.error(_("Failed to unset volume type properties: %s"), e) result += 1 if parsed_args.project: - project_info = None try: - project_info = identity_common.find_project( + project_id = identity_common.find_project_id_sdk( identity_client, parsed_args.project, parsed_args.project_domain, ) - - volume_client.volume_type_access.remove_project_access( - volume_type.id, project_info.id - ) + volume_client.remove_type_access(volume_type.id, project_id) except Exception as e: LOG.error( _("Failed to remove volume type access from project: %s"), @@ -939,7 +1009,7 @@ def take_action(self, parsed_args): result += 1 if parsed_args.encryption_type: try: - volume_client.volume_encryption_types.delete(volume_type) + volume_client.delete_type_encryption(None, volume_type.id) except Exception as e: LOG.error( _( diff --git a/pyproject.toml b/pyproject.toml index 50092d406d..9964bbb60c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,35 +1,920 @@ +[build-system] +requires = ["pbr>=6.1.1"] +build-backend = "pbr.build" + +[project] +name = "python-openstackclient" +description = "OpenStack Command-line Client" +authors = [ + {name = "OpenStack", email = "openstack-discuss@lists.openstack.org"}, +] +readme = {file = "README.rst", content-type = "text/x-rst"} +license = "Apache-2.0" +dynamic = ["version", "dependencies"] +requires-python = ">=3.11" +classifiers = [ + "Environment :: OpenStack", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Operating System :: POSIX :: Linux", + "Programming Language :: Python", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Typing :: Typed", +] + +[project.urls] +Homepage = "https://docs.openstack.org/python-openstackclient/" +Repository = "https://opendev.org/openstack/python-openstackclient/" + +[project.scripts] +openstack = "openstackclient.shell:main" + +[project.entry-points."openstack.cli"] +command_list = "openstackclient.common.module:ListCommand" +module_list = "openstackclient.common.module:ListModule" + +[project.entry-points."openstack.cli.base"] +compute = "openstackclient.compute.client" +identity = "openstackclient.identity.client" +image = "openstackclient.image.client" +network = "openstackclient.network.client" +object_store = "openstackclient.object.client" +volume = "openstackclient.volume.client" + +[project.entry-points."openstack.common"] +availability_zone_list = "openstackclient.common.availability_zone:ListAvailabilityZone" +configuration_show = "openstackclient.common.configuration:ShowConfiguration" +extension_list = "openstackclient.common.extension:ListExtension" +extension_show = "openstackclient.common.extension:ShowExtension" +limits_show = "openstackclient.common.limits:ShowLimits" +project_cleanup = "openstackclient.common.project_cleanup:ProjectCleanup" +quota_list = "openstackclient.common.quota:ListQuota" +quota_set = "openstackclient.common.quota:SetQuota" +quota_show = "openstackclient.common.quota:ShowQuota" +quota_delete = "openstackclient.common.quota:DeleteQuota" +versions_show = "openstackclient.common.versions:ShowVersions" + +[project.entry-points."openstack.compute.v2"] +aggregate_add_host = "openstackclient.compute.v2.aggregate:AddAggregateHost" +aggregate_create = "openstackclient.compute.v2.aggregate:CreateAggregate" +aggregate_delete = "openstackclient.compute.v2.aggregate:DeleteAggregate" +aggregate_list = "openstackclient.compute.v2.aggregate:ListAggregate" +aggregate_remove_host = "openstackclient.compute.v2.aggregate:RemoveAggregateHost" +aggregate_set = "openstackclient.compute.v2.aggregate:SetAggregate" +aggregate_show = "openstackclient.compute.v2.aggregate:ShowAggregate" +aggregate_unset = "openstackclient.compute.v2.aggregate:UnsetAggregate" +aggregate_cache_image = "openstackclient.compute.v2.aggregate:CacheImageForAggregate" +compute_agent_create = "openstackclient.compute.v2.agent:CreateAgent" +compute_agent_delete = "openstackclient.compute.v2.agent:DeleteAgent" +compute_agent_list = "openstackclient.compute.v2.agent:ListAgent" +compute_agent_set = "openstackclient.compute.v2.agent:SetAgent" +compute_service_delete = "openstackclient.compute.v2.service:DeleteService" +compute_service_list = "openstackclient.compute.v2.service:ListService" +compute_service_set = "openstackclient.compute.v2.service:SetService" +console_log_show = "openstackclient.compute.v2.console:ShowConsoleLog" +console_url_show = "openstackclient.compute.v2.console:ShowConsoleURL" +console_connection_show = "openstackclient.compute.v2.console_connection:ShowConsoleConnectionInformation" +flavor_create = "openstackclient.compute.v2.flavor:CreateFlavor" +flavor_delete = "openstackclient.compute.v2.flavor:DeleteFlavor" +flavor_list = "openstackclient.compute.v2.flavor:ListFlavor" +flavor_show = "openstackclient.compute.v2.flavor:ShowFlavor" +flavor_set = "openstackclient.compute.v2.flavor:SetFlavor" +flavor_unset = "openstackclient.compute.v2.flavor:UnsetFlavor" +host_list = "openstackclient.compute.v2.host:ListHost" +host_set = "openstackclient.compute.v2.host:SetHost" +host_show = "openstackclient.compute.v2.host:ShowHost" +hypervisor_list = "openstackclient.compute.v2.hypervisor:ListHypervisor" +hypervisor_show = "openstackclient.compute.v2.hypervisor:ShowHypervisor" +hypervisor_stats_show = "openstackclient.compute.v2.hypervisor_stats:ShowHypervisorStats" +keypair_create = "openstackclient.compute.v2.keypair:CreateKeypair" +keypair_delete = "openstackclient.compute.v2.keypair:DeleteKeypair" +keypair_list = "openstackclient.compute.v2.keypair:ListKeypair" +keypair_show = "openstackclient.compute.v2.keypair:ShowKeypair" +server_add_fixed_ip = "openstackclient.compute.v2.server:AddFixedIP" +server_add_floating_ip = "openstackclient.compute.v2.server:AddFloatingIP" +server_add_port = "openstackclient.compute.v2.server:AddPort" +server_add_network = "openstackclient.compute.v2.server:AddNetwork" +server_add_security_group = "openstackclient.compute.v2.server:AddServerSecurityGroup" +server_add_share = "openstackclient.compute.v2.server_share:AddServerShare" +server_add_volume = "openstackclient.compute.v2.server:AddServerVolume" +server_create = "openstackclient.compute.v2.server:CreateServer" +server_delete = "openstackclient.compute.v2.server:DeleteServer" +server_dump_create = "openstackclient.compute.v2.server:CreateServerDump" +server_evacuate = "openstackclient.compute.v2.server:EvacuateServer" +server_list = "openstackclient.compute.v2.server:ListServer" +server_lock = "openstackclient.compute.v2.server:LockServer" +server_migrate = "openstackclient.compute.v2.server:MigrateServer" +server_migrate_confirm = "openstackclient.compute.v2.server:MigrateConfirm" +server_migrate_revert = "openstackclient.compute.v2.server:MigrateRevert" +server_migration_confirm = "openstackclient.compute.v2.server:ConfirmMigration" +server_migration_revert = "openstackclient.compute.v2.server:RevertMigration" +server_pause = "openstackclient.compute.v2.server:PauseServer" +server_reboot = "openstackclient.compute.v2.server:RebootServer" +server_rebuild = "openstackclient.compute.v2.server:RebuildServer" +server_remove_fixed_ip = "openstackclient.compute.v2.server:RemoveFixedIP" +server_remove_floating_ip = "openstackclient.compute.v2.server:RemoveFloatingIP" +server_remove_port = "openstackclient.compute.v2.server:RemovePort" +server_remove_network = "openstackclient.compute.v2.server:RemoveNetwork" +server_remove_security_group = "openstackclient.compute.v2.server:RemoveServerSecurityGroup" +server_remove_share = "openstackclient.compute.v2.server_share:RemoveServerShare" +server_remove_volume = "openstackclient.compute.v2.server:RemoveServerVolume" +server_rescue = "openstackclient.compute.v2.server:RescueServer" +server_resize = "openstackclient.compute.v2.server:ResizeServer" +server_resize_confirm = "openstackclient.compute.v2.server:ResizeConfirm" +server_resize_revert = "openstackclient.compute.v2.server:ResizeRevert" +server_restore = "openstackclient.compute.v2.server:RestoreServer" +server_resume = "openstackclient.compute.v2.server:ResumeServer" +server_set = "openstackclient.compute.v2.server:SetServer" +server_shelve = "openstackclient.compute.v2.server:ShelveServer" +server_show = "openstackclient.compute.v2.server:ShowServer" +server_ssh = "openstackclient.compute.v2.server:SshServer" +server_start = "openstackclient.compute.v2.server:StartServer" +server_stop = "openstackclient.compute.v2.server:StopServer" +server_suspend = "openstackclient.compute.v2.server:SuspendServer" +server_unlock = "openstackclient.compute.v2.server:UnlockServer" +server_unpause = "openstackclient.compute.v2.server:UnpauseServer" +server_unrescue = "openstackclient.compute.v2.server:UnrescueServer" +server_unset = "openstackclient.compute.v2.server:UnsetServer" +server_unshelve = "openstackclient.compute.v2.server:UnshelveServer" +server_backup_create = "openstackclient.compute.v2.server_backup:CreateServerBackup" +server_event_list = "openstackclient.compute.v2.server_event:ListServerEvent" +server_event_show = "openstackclient.compute.v2.server_event:ShowServerEvent" +server_group_create = "openstackclient.compute.v2.server_group:CreateServerGroup" +server_group_delete = "openstackclient.compute.v2.server_group:DeleteServerGroup" +server_group_list = "openstackclient.compute.v2.server_group:ListServerGroup" +server_group_show = "openstackclient.compute.v2.server_group:ShowServerGroup" +server_image_create = "openstackclient.compute.v2.server_image:CreateServerImage" +server_migration_abort = "openstackclient.compute.v2.server_migration:AbortMigration" +server_migration_force_complete = "openstackclient.compute.v2.server_migration:ForceCompleteMigration" +server_migration_list = "openstackclient.compute.v2.server_migration:ListMigration" +server_migration_show = "openstackclient.compute.v2.server_migration:ShowMigration" +server_share_list = "openstackclient.compute.v2.server_share:ListServerShare" +server_share_show = "openstackclient.compute.v2.server_share:ShowServerShare" +server_volume_list = "openstackclient.compute.v2.server_volume:ListServerVolume" +server_volume_set = "openstackclient.compute.v2.server_volume:SetServerVolume" +server_volume_update = "openstackclient.compute.v2.server_volume:UpdateServerVolume" +usage_list = "openstackclient.compute.v2.usage:ListUsage" +usage_show = "openstackclient.compute.v2.usage:ShowUsage" + +[project.entry-points."openstack.identity.v2"] +catalog_list = "openstackclient.identity.v2_0.catalog:ListCatalog" +catalog_show = "openstackclient.identity.v2_0.catalog:ShowCatalog" +ec2_credentials_create = "openstackclient.identity.v2_0.ec2creds:CreateEC2Creds" +ec2_credentials_delete = "openstackclient.identity.v2_0.ec2creds:DeleteEC2Creds" +ec2_credentials_list = "openstackclient.identity.v2_0.ec2creds:ListEC2Creds" +ec2_credentials_show = "openstackclient.identity.v2_0.ec2creds:ShowEC2Creds" +endpoint_create = "openstackclient.identity.v2_0.endpoint:CreateEndpoint" +endpoint_delete = "openstackclient.identity.v2_0.endpoint:DeleteEndpoint" +endpoint_list = "openstackclient.identity.v2_0.endpoint:ListEndpoint" +endpoint_show = "openstackclient.identity.v2_0.endpoint:ShowEndpoint" +project_create = "openstackclient.identity.v2_0.project:CreateProject" +project_delete = "openstackclient.identity.v2_0.project:DeleteProject" +project_list = "openstackclient.identity.v2_0.project:ListProject" +project_set = "openstackclient.identity.v2_0.project:SetProject" +project_show = "openstackclient.identity.v2_0.project:ShowProject" +project_unset = "openstackclient.identity.v2_0.project:UnsetProject" +role_add = "openstackclient.identity.v2_0.role:AddRole" +role_create = "openstackclient.identity.v2_0.role:CreateRole" +role_delete = "openstackclient.identity.v2_0.role:DeleteRole" +role_list = "openstackclient.identity.v2_0.role:ListRole" +role_remove = "openstackclient.identity.v2_0.role:RemoveRole" +role_show = "openstackclient.identity.v2_0.role:ShowRole" +role_assignment_list = "openstackclient.identity.v2_0.role_assignment:ListRoleAssignment" +service_create = "openstackclient.identity.v2_0.service:CreateService" +service_delete = "openstackclient.identity.v2_0.service:DeleteService" +service_list = "openstackclient.identity.v2_0.service:ListService" +service_show = "openstackclient.identity.v2_0.service:ShowService" +token_issue = "openstackclient.identity.v2_0.token:IssueToken" +token_revoke = "openstackclient.identity.v2_0.token:RevokeToken" +user_create = "openstackclient.identity.v2_0.user:CreateUser" +user_delete = "openstackclient.identity.v2_0.user:DeleteUser" +user_list = "openstackclient.identity.v2_0.user:ListUser" +user_set = "openstackclient.identity.v2_0.user:SetUser" +user_show = "openstackclient.identity.v2_0.user:ShowUser" + +[project.entry-points."openstack.identity.v3"] +access_token_create = "openstackclient.identity.v3.token:CreateAccessToken" +access_rule_delete = "openstackclient.identity.v3.access_rule:DeleteAccessRule" +access_rule_list = "openstackclient.identity.v3.access_rule:ListAccessRule" +access_rule_show = "openstackclient.identity.v3.access_rule:ShowAccessRule" +application_credential_create = "openstackclient.identity.v3.application_credential:CreateApplicationCredential" +application_credential_delete = "openstackclient.identity.v3.application_credential:DeleteApplicationCredential" +application_credential_list = "openstackclient.identity.v3.application_credential:ListApplicationCredential" +application_credential_show = "openstackclient.identity.v3.application_credential:ShowApplicationCredential" +catalog_list = "openstackclient.identity.v3.catalog:ListCatalog" +catalog_show = "openstackclient.identity.v3.catalog:ShowCatalog" +consumer_create = "openstackclient.identity.v3.consumer:CreateConsumer" +consumer_delete = "openstackclient.identity.v3.consumer:DeleteConsumer" +consumer_list = "openstackclient.identity.v3.consumer:ListConsumer" +consumer_set = "openstackclient.identity.v3.consumer:SetConsumer" +consumer_show = "openstackclient.identity.v3.consumer:ShowConsumer" +credential_create = "openstackclient.identity.v3.credential:CreateCredential" +credential_delete = "openstackclient.identity.v3.credential:DeleteCredential" +credential_list = "openstackclient.identity.v3.credential:ListCredential" +credential_set = "openstackclient.identity.v3.credential:SetCredential" +credential_show = "openstackclient.identity.v3.credential:ShowCredential" +domain_create = "openstackclient.identity.v3.domain:CreateDomain" +domain_delete = "openstackclient.identity.v3.domain:DeleteDomain" +domain_list = "openstackclient.identity.v3.domain:ListDomain" +domain_set = "openstackclient.identity.v3.domain:SetDomain" +domain_show = "openstackclient.identity.v3.domain:ShowDomain" +ec2_credentials_create = "openstackclient.identity.v3.ec2creds:CreateEC2Creds" +ec2_credentials_delete = "openstackclient.identity.v3.ec2creds:DeleteEC2Creds" +ec2_credentials_list = "openstackclient.identity.v3.ec2creds:ListEC2Creds" +ec2_credentials_show = "openstackclient.identity.v3.ec2creds:ShowEC2Creds" +endpoint_add_project = "openstackclient.identity.v3.endpoint:AddProjectToEndpoint" +endpoint_create = "openstackclient.identity.v3.endpoint:CreateEndpoint" +endpoint_delete = "openstackclient.identity.v3.endpoint:DeleteEndpoint" +endpoint_list = "openstackclient.identity.v3.endpoint:ListEndpoint" +endpoint_remove_project = "openstackclient.identity.v3.endpoint:RemoveProjectFromEndpoint" +endpoint_set = "openstackclient.identity.v3.endpoint:SetEndpoint" +endpoint_show = "openstackclient.identity.v3.endpoint:ShowEndpoint" +endpoint_group_add_project = "openstackclient.identity.v3.endpoint_group:AddProjectToEndpointGroup" +endpoint_group_create = "openstackclient.identity.v3.endpoint_group:CreateEndpointGroup" +endpoint_group_delete = "openstackclient.identity.v3.endpoint_group:DeleteEndpointGroup" +endpoint_group_list = "openstackclient.identity.v3.endpoint_group:ListEndpointGroup" +endpoint_group_remove_project = "openstackclient.identity.v3.endpoint_group:RemoveProjectFromEndpointGroup" +endpoint_group_set = "openstackclient.identity.v3.endpoint_group:SetEndpointGroup" +endpoint_group_show = "openstackclient.identity.v3.endpoint_group:ShowEndpointGroup" +federation_domain_list = "openstackclient.identity.v3.unscoped_saml:ListAccessibleDomains" +federation_project_list = "openstackclient.identity.v3.unscoped_saml:ListAccessibleProjects" +federation_protocol_create = "openstackclient.identity.v3.federation_protocol:CreateProtocol" +federation_protocol_delete = "openstackclient.identity.v3.federation_protocol:DeleteProtocol" +federation_protocol_list = "openstackclient.identity.v3.federation_protocol:ListProtocols" +federation_protocol_set = "openstackclient.identity.v3.federation_protocol:SetProtocol" +federation_protocol_show = "openstackclient.identity.v3.federation_protocol:ShowProtocol" +group_add_user = "openstackclient.identity.v3.group:AddUserToGroup" +group_contains_user = "openstackclient.identity.v3.group:CheckUserInGroup" +group_create = "openstackclient.identity.v3.group:CreateGroup" +group_delete = "openstackclient.identity.v3.group:DeleteGroup" +group_list = "openstackclient.identity.v3.group:ListGroup" +group_remove_user = "openstackclient.identity.v3.group:RemoveUserFromGroup" +group_set = "openstackclient.identity.v3.group:SetGroup" +group_show = "openstackclient.identity.v3.group:ShowGroup" +identity_provider_create = "openstackclient.identity.v3.identity_provider:CreateIdentityProvider" +identity_provider_delete = "openstackclient.identity.v3.identity_provider:DeleteIdentityProvider" +identity_provider_list = "openstackclient.identity.v3.identity_provider:ListIdentityProvider" +identity_provider_set = "openstackclient.identity.v3.identity_provider:SetIdentityProvider" +identity_provider_show = "openstackclient.identity.v3.identity_provider:ShowIdentityProvider" +implied_role_create = "openstackclient.identity.v3.implied_role:CreateImpliedRole" +implied_role_delete = "openstackclient.identity.v3.implied_role:DeleteImpliedRole" +implied_role_list = "openstackclient.identity.v3.implied_role:ListImpliedRole" +limit_create = "openstackclient.identity.v3.limit:CreateLimit" +limit_delete = "openstackclient.identity.v3.limit:DeleteLimit" +limit_list = "openstackclient.identity.v3.limit:ListLimit" +limit_set = "openstackclient.identity.v3.limit:SetLimit" +limit_show = "openstackclient.identity.v3.limit:ShowLimit" +mapping_create = "openstackclient.identity.v3.mapping:CreateMapping" +mapping_delete = "openstackclient.identity.v3.mapping:DeleteMapping" +mapping_list = "openstackclient.identity.v3.mapping:ListMapping" +mapping_set = "openstackclient.identity.v3.mapping:SetMapping" +mapping_show = "openstackclient.identity.v3.mapping:ShowMapping" +policy_create = "openstackclient.identity.v3.policy:CreatePolicy" +policy_delete = "openstackclient.identity.v3.policy:DeletePolicy" +policy_list = "openstackclient.identity.v3.policy:ListPolicy" +policy_set = "openstackclient.identity.v3.policy:SetPolicy" +policy_show = "openstackclient.identity.v3.policy:ShowPolicy" +project_create = "openstackclient.identity.v3.project:CreateProject" +project_delete = "openstackclient.identity.v3.project:DeleteProject" +project_list = "openstackclient.identity.v3.project:ListProject" +project_set = "openstackclient.identity.v3.project:SetProject" +project_show = "openstackclient.identity.v3.project:ShowProject" +region_create = "openstackclient.identity.v3.region:CreateRegion" +region_delete = "openstackclient.identity.v3.region:DeleteRegion" +region_list = "openstackclient.identity.v3.region:ListRegion" +region_set = "openstackclient.identity.v3.region:SetRegion" +region_show = "openstackclient.identity.v3.region:ShowRegion" +registered_limit_create = "openstackclient.identity.v3.registered_limit:CreateRegisteredLimit" +registered_limit_delete = "openstackclient.identity.v3.registered_limit:DeleteRegisteredLimit" +registered_limit_list = "openstackclient.identity.v3.registered_limit:ListRegisteredLimit" +registered_limit_set = "openstackclient.identity.v3.registered_limit:SetRegisteredLimit" +registered_limit_show = "openstackclient.identity.v3.registered_limit:ShowRegisteredLimit" +request_token_authorize = "openstackclient.identity.v3.token:AuthorizeRequestToken" +request_token_create = "openstackclient.identity.v3.token:CreateRequestToken" +role_add = "openstackclient.identity.v3.role:AddRole" +role_create = "openstackclient.identity.v3.role:CreateRole" +role_delete = "openstackclient.identity.v3.role:DeleteRole" +role_list = "openstackclient.identity.v3.role:ListRole" +role_remove = "openstackclient.identity.v3.role:RemoveRole" +role_show = "openstackclient.identity.v3.role:ShowRole" +role_set = "openstackclient.identity.v3.role:SetRole" +role_assignment_list = "openstackclient.identity.v3.role_assignment:ListRoleAssignment" +service_create = "openstackclient.identity.v3.service:CreateService" +service_delete = "openstackclient.identity.v3.service:DeleteService" +service_list = "openstackclient.identity.v3.service:ListService" +service_show = "openstackclient.identity.v3.service:ShowService" +service_set = "openstackclient.identity.v3.service:SetService" +service_provider_create = "openstackclient.identity.v3.service_provider:CreateServiceProvider" +service_provider_delete = "openstackclient.identity.v3.service_provider:DeleteServiceProvider" +service_provider_list = "openstackclient.identity.v3.service_provider:ListServiceProvider" +service_provider_set = "openstackclient.identity.v3.service_provider:SetServiceProvider" +service_provider_show = "openstackclient.identity.v3.service_provider:ShowServiceProvider" +token_issue = "openstackclient.identity.v3.token:IssueToken" +token_revoke = "openstackclient.identity.v3.token:RevokeToken" +trust_create = "openstackclient.identity.v3.trust:CreateTrust" +trust_delete = "openstackclient.identity.v3.trust:DeleteTrust" +trust_list = "openstackclient.identity.v3.trust:ListTrust" +trust_show = "openstackclient.identity.v3.trust:ShowTrust" +user_create = "openstackclient.identity.v3.user:CreateUser" +user_delete = "openstackclient.identity.v3.user:DeleteUser" +user_list = "openstackclient.identity.v3.user:ListUser" +user_set = "openstackclient.identity.v3.user:SetUser" +user_password_set = "openstackclient.identity.v3.user:SetPasswordUser" +user_show = "openstackclient.identity.v3.user:ShowUser" + +[project.entry-points."openstack.image.v1"] +image_create = "openstackclient.image.v1.image:CreateImage" +image_delete = "openstackclient.image.v1.image:DeleteImage" +image_list = "openstackclient.image.v1.image:ListImage" +image_save = "openstackclient.image.v1.image:SaveImage" +image_set = "openstackclient.image.v1.image:SetImage" +image_show = "openstackclient.image.v1.image:ShowImage" + +[project.entry-points."openstack.image.v2"] +image_add_project = "openstackclient.image.v2.image:AddProjectToImage" +image_create = "openstackclient.image.v2.image:CreateImage" +image_delete = "openstackclient.image.v2.image:DeleteImage" +image_list = "openstackclient.image.v2.image:ListImage" +image_member_list = "openstackclient.image.v2.image:ListImageProjects" +image_remove_project = "openstackclient.image.v2.image:RemoveProjectImage" +image_member_get = "openstackclient.image.v2.image:ShowProjectImage" +image_save = "openstackclient.image.v2.image:SaveImage" +image_show = "openstackclient.image.v2.image:ShowImage" +image_set = "openstackclient.image.v2.image:SetImage" +image_unset = "openstackclient.image.v2.image:UnsetImage" +image_stage = "openstackclient.image.v2.image:StageImage" +image_task_show = "openstackclient.image.v2.task:ShowTask" +image_task_list = "openstackclient.image.v2.task:ListTask" +image_import_info = "openstackclient.image.v2.info:ImportInfo" +image_import = "openstackclient.image.v2.image:ImportImage" +image_stores_list = "openstackclient.image.v2.image:StoresInfo" +image_metadef_namespace_create = "openstackclient.image.v2.metadef_namespaces:CreateMetadefNamespace" +image_metadef_namespace_delete = "openstackclient.image.v2.metadef_namespaces:DeleteMetadefNamespace" +image_metadef_namespace_list = "openstackclient.image.v2.metadef_namespaces:ListMetadefNamespace" +image_metadef_namespace_set = "openstackclient.image.v2.metadef_namespaces:SetMetadefNamespace" +image_metadef_namespace_show = "openstackclient.image.v2.metadef_namespaces:ShowMetadefNamespace" +image_metadef_namespace_unset = "openstackclient.image.v2.metadef_namespaces:UnsetMetadefNamespace" +image_metadef_object_create = "openstackclient.image.v2.metadef_objects:CreateMetadefObjects" +image_metadef_object_show = "openstackclient.image.v2.metadef_objects:ShowMetadefObjects" +image_metadef_object_list = "openstackclient.image.v2.metadef_objects:ListMetadefObjects" +image_metadef_object_delete = "openstackclient.image.v2.metadef_objects:DeleteMetadefObject" +image_metadef_object_update = "openstackclient.image.v2.metadef_objects:SetMetadefObject" +image_metadef_object_property_show = "openstackclient.image.v2.metadef_objects:ShowMetadefObjectProperty" +image_metadef_property_create = "openstackclient.image.v2.metadef_properties:CreateMetadefProperty" +image_metadef_property_delete = "openstackclient.image.v2.metadef_properties:DeleteMetadefProperty" +image_metadef_property_list = "openstackclient.image.v2.metadef_properties:ListMetadefProperties" +image_metadef_property_set = "openstackclient.image.v2.metadef_properties:SetMetadefProperty" +image_metadef_property_show = "openstackclient.image.v2.metadef_properties:ShowMetadefProperty" +image_metadef_resource_type_list = "openstackclient.image.v2.metadef_resource_types:ListMetadefResourceTypes" +image_metadef_resource_type_association_create = "openstackclient.image.v2.metadef_resource_type_association:CreateMetadefResourceTypeAssociation" +image_metadef_resource_type_association_delete = "openstackclient.image.v2.metadef_resource_type_association:DeleteMetadefResourceTypeAssociation" +image_metadef_resource_type_association_list = "openstackclient.image.v2.metadef_resource_type_association:ListMetadefResourceTypeAssociations" +cached_image_list = "openstackclient.image.v2.cache:ListCachedImage" +cached_image_queue = "openstackclient.image.v2.cache:QueueCachedImage" +cached_image_delete = "openstackclient.image.v2.cache:DeleteCachedImage" +cached_image_clear = "openstackclient.image.v2.cache:ClearCachedImage" + +[project.entry-points."openstack.network.v2"] +address_group_create = "openstackclient.network.v2.address_group:CreateAddressGroup" +address_group_delete = "openstackclient.network.v2.address_group:DeleteAddressGroup" +address_group_list = "openstackclient.network.v2.address_group:ListAddressGroup" +address_group_set = "openstackclient.network.v2.address_group:SetAddressGroup" +address_group_show = "openstackclient.network.v2.address_group:ShowAddressGroup" +address_group_unset = "openstackclient.network.v2.address_group:UnsetAddressGroup" +address_scope_create = "openstackclient.network.v2.address_scope:CreateAddressScope" +address_scope_delete = "openstackclient.network.v2.address_scope:DeleteAddressScope" +address_scope_list = "openstackclient.network.v2.address_scope:ListAddressScope" +address_scope_set = "openstackclient.network.v2.address_scope:SetAddressScope" +address_scope_show = "openstackclient.network.v2.address_scope:ShowAddressScope" +floating_ip_create = "openstackclient.network.v2.floating_ip:CreateFloatingIP" +floating_ip_delete = "openstackclient.network.v2.floating_ip:DeleteFloatingIP" +floating_ip_list = "openstackclient.network.v2.floating_ip:ListFloatingIP" +floating_ip_set = "openstackclient.network.v2.floating_ip:SetFloatingIP" +floating_ip_show = "openstackclient.network.v2.floating_ip:ShowFloatingIP" +floating_ip_unset = "openstackclient.network.v2.floating_ip:UnsetFloatingIP" +floating_ip_port_forwarding_create = "openstackclient.network.v2.floating_ip_port_forwarding:CreateFloatingIPPortForwarding" +floating_ip_port_forwarding_delete = "openstackclient.network.v2.floating_ip_port_forwarding:DeleteFloatingIPPortForwarding" +floating_ip_port_forwarding_list = "openstackclient.network.v2.floating_ip_port_forwarding:ListFloatingIPPortForwarding" +floating_ip_port_forwarding_set = "openstackclient.network.v2.floating_ip_port_forwarding:SetFloatingIPPortForwarding" +floating_ip_port_forwarding_show = "openstackclient.network.v2.floating_ip_port_forwarding:ShowFloatingIPPortForwarding" +ip_availability_list = "openstackclient.network.v2.ip_availability:ListIPAvailability" +ip_availability_show = "openstackclient.network.v2.ip_availability:ShowIPAvailability" +local_ip_create = "openstackclient.network.v2.local_ip:CreateLocalIP" +local_ip_delete = "openstackclient.network.v2.local_ip:DeleteLocalIP" +local_ip_list = "openstackclient.network.v2.local_ip:ListLocalIP" +local_ip_set = "openstackclient.network.v2.local_ip:SetLocalIP" +local_ip_show = "openstackclient.network.v2.local_ip:ShowLocalIP" +local_ip_association_create = "openstackclient.network.v2.local_ip_association:CreateLocalIPAssociation" +local_ip_association_delete = "openstackclient.network.v2.local_ip_association:DeleteLocalIPAssociation" +local_ip_association_list = "openstackclient.network.v2.local_ip_association:ListLocalIPAssociation" +network_agent_add_network = "openstackclient.network.v2.network_agent:AddNetworkToAgent" +network_agent_add_router = "openstackclient.network.v2.network_agent:AddRouterToAgent" +network_agent_delete = "openstackclient.network.v2.network_agent:DeleteNetworkAgent" +network_agent_list = "openstackclient.network.v2.network_agent:ListNetworkAgent" +network_agent_remove_network = "openstackclient.network.v2.network_agent:RemoveNetworkFromAgent" +network_agent_remove_router = "openstackclient.network.v2.network_agent:RemoveRouterFromAgent" +network_agent_set = "openstackclient.network.v2.network_agent:SetNetworkAgent" +network_agent_router_set = "openstackclient.network.v2.network_agent:SetNetworkAgentRouter" +network_agent_show = "openstackclient.network.v2.network_agent:ShowNetworkAgent" +network_auto_allocated_topology_create = "openstackclient.network.v2.network_auto_allocated_topology:CreateAutoAllocatedTopology" +network_auto_allocated_topology_delete = "openstackclient.network.v2.network_auto_allocated_topology:DeleteAutoAllocatedTopology" +network_flavor_add_profile = "openstackclient.network.v2.network_flavor:AddNetworkFlavorToProfile" +network_flavor_create = "openstackclient.network.v2.network_flavor:CreateNetworkFlavor" +network_flavor_delete = "openstackclient.network.v2.network_flavor:DeleteNetworkFlavor" +network_flavor_list = "openstackclient.network.v2.network_flavor:ListNetworkFlavor" +network_flavor_remove_profile = "openstackclient.network.v2.network_flavor:RemoveNetworkFlavorFromProfile" +network_flavor_set = "openstackclient.network.v2.network_flavor:SetNetworkFlavor" +network_flavor_show = "openstackclient.network.v2.network_flavor:ShowNetworkFlavor" +network_flavor_profile_create = "openstackclient.network.v2.network_flavor_profile:CreateNetworkFlavorProfile" +network_flavor_profile_delete = "openstackclient.network.v2.network_flavor_profile:DeleteNetworkFlavorProfile" +network_flavor_profile_list = "openstackclient.network.v2.network_flavor_profile:ListNetworkFlavorProfile" +network_flavor_profile_set = "openstackclient.network.v2.network_flavor_profile:SetNetworkFlavorProfile" +network_flavor_profile_show = "openstackclient.network.v2.network_flavor_profile:ShowNetworkFlavorProfile" +network_create = "openstackclient.network.v2.network:CreateNetwork" +network_delete = "openstackclient.network.v2.network:DeleteNetwork" +network_list = "openstackclient.network.v2.network:ListNetwork" +network_set = "openstackclient.network.v2.network:SetNetwork" +network_show = "openstackclient.network.v2.network:ShowNetwork" +network_unset = "openstackclient.network.v2.network:UnsetNetwork" +network_l3_conntrack_helper_create = "openstackclient.network.v2.l3_conntrack_helper:CreateConntrackHelper" +network_l3_conntrack_helper_delete = "openstackclient.network.v2.l3_conntrack_helper:DeleteConntrackHelper" +network_l3_conntrack_helper_list = "openstackclient.network.v2.l3_conntrack_helper:ListConntrackHelper" +network_l3_conntrack_helper_set = "openstackclient.network.v2.l3_conntrack_helper:SetConntrackHelper" +network_l3_conntrack_helper_show = "openstackclient.network.v2.l3_conntrack_helper:ShowConntrackHelper" +network_meter_create = "openstackclient.network.v2.network_meter:CreateMeter" +network_meter_delete = "openstackclient.network.v2.network_meter:DeleteMeter" +network_meter_list = "openstackclient.network.v2.network_meter:ListMeter" +network_meter_show = "openstackclient.network.v2.network_meter:ShowMeter" +network_meter_rule_create = "openstackclient.network.v2.network_meter_rule:CreateMeterRule" +network_meter_rule_delete = "openstackclient.network.v2.network_meter_rule:DeleteMeterRule" +network_meter_rule_list = "openstackclient.network.v2.network_meter_rule:ListMeterRule" +network_meter_rule_show = "openstackclient.network.v2.network_meter_rule:ShowMeterRule" +network_qos_policy_create = "openstackclient.network.v2.network_qos_policy:CreateNetworkQosPolicy" +network_qos_policy_delete = "openstackclient.network.v2.network_qos_policy:DeleteNetworkQosPolicy" +network_qos_policy_list = "openstackclient.network.v2.network_qos_policy:ListNetworkQosPolicy" +network_qos_policy_set = "openstackclient.network.v2.network_qos_policy:SetNetworkQosPolicy" +network_qos_policy_show = "openstackclient.network.v2.network_qos_policy:ShowNetworkQosPolicy" +network_qos_rule_create = "openstackclient.network.v2.network_qos_rule:CreateNetworkQosRule" +network_qos_rule_delete = "openstackclient.network.v2.network_qos_rule:DeleteNetworkQosRule" +network_qos_rule_list = "openstackclient.network.v2.network_qos_rule:ListNetworkQosRule" +network_qos_rule_set = "openstackclient.network.v2.network_qos_rule:SetNetworkQosRule" +network_qos_rule_show = "openstackclient.network.v2.network_qos_rule:ShowNetworkQosRule" +network_qos_rule_type_list = "openstackclient.network.v2.network_qos_rule_type:ListNetworkQosRuleType" +network_qos_rule_type_show = "openstackclient.network.v2.network_qos_rule_type:ShowNetworkQosRuleType" +network_rbac_create = "openstackclient.network.v2.network_rbac:CreateNetworkRBAC" +network_rbac_delete = "openstackclient.network.v2.network_rbac:DeleteNetworkRBAC" +network_rbac_list = "openstackclient.network.v2.network_rbac:ListNetworkRBAC" +network_rbac_set = "openstackclient.network.v2.network_rbac:SetNetworkRBAC" +network_rbac_show = "openstackclient.network.v2.network_rbac:ShowNetworkRBAC" +network_segment_create = "openstackclient.network.v2.network_segment:CreateNetworkSegment" +network_segment_delete = "openstackclient.network.v2.network_segment:DeleteNetworkSegment" +network_segment_list = "openstackclient.network.v2.network_segment:ListNetworkSegment" +network_segment_set = "openstackclient.network.v2.network_segment:SetNetworkSegment" +network_segment_show = "openstackclient.network.v2.network_segment:ShowNetworkSegment" +network_segment_range_create = "openstackclient.network.v2.network_segment_range:CreateNetworkSegmentRange" +network_segment_range_delete = "openstackclient.network.v2.network_segment_range:DeleteNetworkSegmentRange" +network_segment_range_list = "openstackclient.network.v2.network_segment_range:ListNetworkSegmentRange" +network_segment_range_set = "openstackclient.network.v2.network_segment_range:SetNetworkSegmentRange" +network_segment_range_show = "openstackclient.network.v2.network_segment_range:ShowNetworkSegmentRange" +network_service_provider_list = "openstackclient.network.v2.network_service_provider:ListNetworkServiceProvider" +network_subport_list = "openstackclient.network.v2.network_trunk:ListNetworkSubport" +network_trunk_create = "openstackclient.network.v2.network_trunk:CreateNetworkTrunk" +network_trunk_delete = "openstackclient.network.v2.network_trunk:DeleteNetworkTrunk" +network_trunk_list = "openstackclient.network.v2.network_trunk:ListNetworkTrunk" +network_trunk_set = "openstackclient.network.v2.network_trunk:SetNetworkTrunk" +network_trunk_show = "openstackclient.network.v2.network_trunk:ShowNetworkTrunk" +network_trunk_unset = "openstackclient.network.v2.network_trunk:UnsetNetworkTrunk" +port_create = "openstackclient.network.v2.port:CreatePort" +port_delete = "openstackclient.network.v2.port:DeletePort" +port_list = "openstackclient.network.v2.port:ListPort" +port_set = "openstackclient.network.v2.port:SetPort" +port_show = "openstackclient.network.v2.port:ShowPort" +port_unset = "openstackclient.network.v2.port:UnsetPort" +router_add_gateway = "openstackclient.network.v2.router:AddGatewayToRouter" +router_add_port = "openstackclient.network.v2.router:AddPortToRouter" +router_add_route = "openstackclient.network.v2.router:AddExtraRoutesToRouter" +router_add_subnet = "openstackclient.network.v2.router:AddSubnetToRouter" +router_create = "openstackclient.network.v2.router:CreateRouter" +router_delete = "openstackclient.network.v2.router:DeleteRouter" +router_list = "openstackclient.network.v2.router:ListRouter" +router_remove_gateway = "openstackclient.network.v2.router:RemoveGatewayFromRouter" +router_remove_port = "openstackclient.network.v2.router:RemovePortFromRouter" +router_remove_route = "openstackclient.network.v2.router:RemoveExtraRoutesFromRouter" +router_remove_subnet = "openstackclient.network.v2.router:RemoveSubnetFromRouter" +router_set = "openstackclient.network.v2.router:SetRouter" +router_show = "openstackclient.network.v2.router:ShowRouter" +router_unset = "openstackclient.network.v2.router:UnsetRouter" +router_ndp_proxy_create = "openstackclient.network.v2.ndp_proxy:CreateNDPProxy" +router_ndp_proxy_delete = "openstackclient.network.v2.ndp_proxy:DeleteNDPProxy" +router_ndp_proxy_list = "openstackclient.network.v2.ndp_proxy:ListNDPProxy" +router_ndp_proxy_set = "openstackclient.network.v2.ndp_proxy:SetNDPProxy" +router_ndp_proxy_show = "openstackclient.network.v2.ndp_proxy:ShowNDPProxy" +security_group_create = "openstackclient.network.v2.security_group:CreateSecurityGroup" +security_group_delete = "openstackclient.network.v2.security_group:DeleteSecurityGroup" +security_group_list = "openstackclient.network.v2.security_group:ListSecurityGroup" +security_group_set = "openstackclient.network.v2.security_group:SetSecurityGroup" +security_group_show = "openstackclient.network.v2.security_group:ShowSecurityGroup" +security_group_unset = "openstackclient.network.v2.security_group:UnsetSecurityGroup" +security_group_rule_create = "openstackclient.network.v2.security_group_rule:CreateSecurityGroupRule" +security_group_rule_delete = "openstackclient.network.v2.security_group_rule:DeleteSecurityGroupRule" +security_group_rule_list = "openstackclient.network.v2.security_group_rule:ListSecurityGroupRule" +security_group_rule_show = "openstackclient.network.v2.security_group_rule:ShowSecurityGroupRule" +default_security_group_rule_create = "openstackclient.network.v2.default_security_group_rule:CreateDefaultSecurityGroupRule" +default_security_group_rule_delete = "openstackclient.network.v2.default_security_group_rule:DeleteDefaultSecurityGroupRule" +default_security_group_rule_list = "openstackclient.network.v2.default_security_group_rule:ListDefaultSecurityGroupRule" +default_security_group_rule_show = "openstackclient.network.v2.default_security_group_rule:ShowDefaultSecurityGroupRule" +security_group_default_statefulness_create = "openstackclient.network.v2.security_groups_default_statefulness:CreateSecurityGroupDefaultStatefulness" +security_group_default_statefulness_delete = "openstackclient.network.v2.security_groups_default_statefulness:DeleteSecurityGroupDefaultStatefulness" +security_group_default_statefulness_list = "openstackclient.network.v2.security_groups_default_statefulness:ListSecurityGroupDefaultStatefulness" +security_group_default_statefulness_set = "openstackclient.network.v2.security_groups_default_statefulness:SetSecurityGroupDefaultStatefulness" +security_group_default_statefulness_show = "openstackclient.network.v2.security_groups_default_statefulness:ShowSecurityGroupDefaultStatefulness" +subnet_create = "openstackclient.network.v2.subnet:CreateSubnet" +subnet_delete = "openstackclient.network.v2.subnet:DeleteSubnet" +subnet_list = "openstackclient.network.v2.subnet:ListSubnet" +subnet_set = "openstackclient.network.v2.subnet:SetSubnet" +subnet_show = "openstackclient.network.v2.subnet:ShowSubnet" +subnet_unset = "openstackclient.network.v2.subnet:UnsetSubnet" +subnet_pool_create = "openstackclient.network.v2.subnet_pool:CreateSubnetPool" +subnet_pool_delete = "openstackclient.network.v2.subnet_pool:DeleteSubnetPool" +subnet_pool_list = "openstackclient.network.v2.subnet_pool:ListSubnetPool" +subnet_pool_set = "openstackclient.network.v2.subnet_pool:SetSubnetPool" +subnet_pool_show = "openstackclient.network.v2.subnet_pool:ShowSubnetPool" +subnet_pool_unset = "openstackclient.network.v2.subnet_pool:UnsetSubnetPool" + +[project.entry-points."openstack.network.v2.bgpvpn"] +bgpvpn_create = "openstackclient.network.v2.bgpvpn.bgpvpn:CreateBgpvpn" +bgpvpn_delete = "openstackclient.network.v2.bgpvpn.bgpvpn:DeleteBgpvpn" +bgpvpn_list = "openstackclient.network.v2.bgpvpn.bgpvpn:ListBgpvpn" +bgpvpn_set = "openstackclient.network.v2.bgpvpn.bgpvpn:SetBgpvpn" +bgpvpn_show = "openstackclient.network.v2.bgpvpn.bgpvpn:ShowBgpvpn" +bgpvpn_unset = "openstackclient.network.v2.bgpvpn.bgpvpn:UnsetBgpvpn" +bgpvpn_network_association_create = "openstackclient.network.v2.bgpvpn.network_association:CreateBgpvpnNetAssoc" +bgpvpn_network_association_delete = "openstackclient.network.v2.bgpvpn.network_association:DeleteBgpvpnNetAssoc" +bgpvpn_network_association_list = "openstackclient.network.v2.bgpvpn.network_association:ListBgpvpnNetAssoc" +bgpvpn_network_association_show = "openstackclient.network.v2.bgpvpn.network_association:ShowBgpvpnNetAssoc" +bgpvpn_router_association_create = "openstackclient.network.v2.bgpvpn.router_association:CreateBgpvpnRouterAssoc" +bgpvpn_router_association_delete = "openstackclient.network.v2.bgpvpn.router_association:DeleteBgpvpnRouterAssoc" +bgpvpn_router_association_list = "openstackclient.network.v2.bgpvpn.router_association:ListBgpvpnRouterAssoc" +bgpvpn_router_association_set = "openstackclient.network.v2.bgpvpn.router_association:SetBgpvpnRouterAssoc" +bgpvpn_router_association_show = "openstackclient.network.v2.bgpvpn.router_association:ShowBgpvpnRouterAssoc" +bgpvpn_router_association_unset = "openstackclient.network.v2.bgpvpn.router_association:UnsetBgpvpnRouterAssoc" +bgpvpn_port_association_create = "openstackclient.network.v2.bgpvpn.port_association:CreateBgpvpnPortAssoc" +bgpvpn_port_association_set = "openstackclient.network.v2.bgpvpn.port_association:SetBgpvpnPortAssoc" +bgpvpn_port_association_unset = "openstackclient.network.v2.bgpvpn.port_association:UnsetBgpvpnPortAssoc" +bgpvpn_port_association_delete = "openstackclient.network.v2.bgpvpn.port_association:DeleteBgpvpnPortAssoc" +bgpvpn_port_association_list = "openstackclient.network.v2.bgpvpn.port_association:ListBgpvpnPortAssoc" +bgpvpn_port_association_show = "openstackclient.network.v2.bgpvpn.port_association:ShowBgpvpnPortAssoc" + +[project.entry-points."openstack.network.v2.dynamic_routing"] +bgp_peer_create = "openstackclient.network.v2.dynamic_routing.bgp_peer:CreateBgpPeer" +bgp_peer_delete = "openstackclient.network.v2.dynamic_routing.bgp_peer:DeleteBgpPeer" +bgp_peer_list = "openstackclient.network.v2.dynamic_routing.bgp_peer:ListBgpPeer" +bgp_peer_set = "openstackclient.network.v2.dynamic_routing.bgp_peer:SetBgpPeer" +bgp_peer_show = "openstackclient.network.v2.dynamic_routing.bgp_peer:ShowBgpPeer" +bgp_speaker_create = "openstackclient.network.v2.dynamic_routing.bgp_speaker:CreateBgpSpeaker" +bgp_speaker_delete = "openstackclient.network.v2.dynamic_routing.bgp_speaker:DeleteBgpSpeaker" +bgp_speaker_list = "openstackclient.network.v2.dynamic_routing.bgp_speaker:ListBgpSpeaker" +bgp_speaker_set = "openstackclient.network.v2.dynamic_routing.bgp_speaker:SetBgpSpeaker" +bgp_speaker_show = "openstackclient.network.v2.dynamic_routing.bgp_speaker:ShowBgpSpeaker" +bgp_speaker_add_network = "openstackclient.network.v2.dynamic_routing.bgp_speaker:AddNetworkToSpeaker" +bgp_speaker_add_peer = "openstackclient.network.v2.dynamic_routing.bgp_speaker:AddPeerToSpeaker" +bgp_speaker_remove_network = "openstackclient.network.v2.dynamic_routing.bgp_speaker:RemoveNetworkFromSpeaker" +bgp_speaker_remove_peer = "openstackclient.network.v2.dynamic_routing.bgp_speaker:RemovePeerFromSpeaker" +bgp_speaker_list_advertised_routes = "openstackclient.network.v2.dynamic_routing.bgp_speaker:ListRoutesAdvertisedBySpeaker" +bgp_dragent_add_speaker = "openstackclient.network.v2.dynamic_routing.bgp_dragent:AddBgpSpeakerToDRAgent" +bgp_dragent_remove_speaker = "openstackclient.network.v2.dynamic_routing.bgp_dragent:RemoveBgpSpeakerFromDRAgent" +bgp_dragent_list = "openstackclient.network.v2.dynamic_routing.bgp_dragent:ListDRAgent" + +[project.entry-points."openstack.network.v2.fwaas"] +firewall_group_create = "openstackclient.network.v2.fwaas.group:CreateFirewallGroup" +firewall_group_delete = "openstackclient.network.v2.fwaas.group:DeleteFirewallGroup" +firewall_group_list = "openstackclient.network.v2.fwaas.group:ListFirewallGroup" +firewall_group_set = "openstackclient.network.v2.fwaas.group:SetFirewallGroup" +firewall_group_show = "openstackclient.network.v2.fwaas.group:ShowFirewallGroup" +firewall_group_unset = "openstackclient.network.v2.fwaas.group:UnsetFirewallGroup" +firewall_group_policy_add_rule = "openstackclient.network.v2.fwaas.policy:FirewallPolicyInsertRule" +firewall_group_policy_create = "openstackclient.network.v2.fwaas.policy:CreateFirewallPolicy" +firewall_group_policy_delete = "openstackclient.network.v2.fwaas.policy:DeleteFirewallPolicy" +firewall_group_policy_list = "openstackclient.network.v2.fwaas.policy:ListFirewallPolicy" +firewall_group_policy_remove_rule = "openstackclient.network.v2.fwaas.policy:FirewallPolicyRemoveRule" +firewall_group_policy_set = "openstackclient.network.v2.fwaas.policy:SetFirewallPolicy" +firewall_group_policy_show = "openstackclient.network.v2.fwaas.policy:ShowFirewallPolicy" +firewall_group_policy_unset = "openstackclient.network.v2.fwaas.policy:UnsetFirewallPolicy" +firewall_group_rule_create = "openstackclient.network.v2.fwaas.rule:CreateFirewallRule" +firewall_group_rule_delete = "openstackclient.network.v2.fwaas.rule:DeleteFirewallRule" +firewall_group_rule_list = "openstackclient.network.v2.fwaas.rule:ListFirewallRule" +firewall_group_rule_set = "openstackclient.network.v2.fwaas.rule:SetFirewallRule" +firewall_group_rule_show = "openstackclient.network.v2.fwaas.rule:ShowFirewallRule" +firewall_group_rule_unset = "openstackclient.network.v2.fwaas.rule:UnsetFirewallRule" + +[project.entry-points."openstack.network.v2.taas"] +tap_flow_create = "openstackclient.network.v2.taas.tap_flow:CreateTapFlow" +tap_flow_delete = "openstackclient.network.v2.taas.tap_flow:DeleteTapFlow" +tap_flow_list = "openstackclient.network.v2.taas.tap_flow:ListTapFlow" +tap_flow_show = "openstackclient.network.v2.taas.tap_flow:ShowTapFlow" +tap_flow_update = "openstackclient.network.v2.taas.tap_flow:UpdateTapFlow" +tap_mirror_create = "openstackclient.network.v2.taas.tap_mirror:CreateTapMirror" +tap_mirror_delete = "openstackclient.network.v2.taas.tap_mirror:DeleteTapMirror" +tap_mirror_list = "openstackclient.network.v2.taas.tap_mirror:ListTapMirror" +tap_mirror_show = "openstackclient.network.v2.taas.tap_mirror:ShowTapMirror" +tap_mirror_update = "openstackclient.network.v2.taas.tap_mirror:UpdateTapMirror" +tap_service_create = "openstackclient.network.v2.taas.tap_service:CreateTapService" +tap_service_delete = "openstackclient.network.v2.taas.tap_service:DeleteTapService" +tap_service_list = "openstackclient.network.v2.taas.tap_service:ListTapService" +tap_service_show = "openstackclient.network.v2.taas.tap_service:ShowTapService" +tap_service_update = "openstackclient.network.v2.taas.tap_service:UpdateTapService" + +[project.entry-points."openstack.network.v2.vpnaas"] +vpn_endpoint_group_create = "openstackclient.network.v2.vpnaas.endpoint_group:CreateEndpointGroup" +vpn_endpoint_group_delete = "openstackclient.network.v2.vpnaas.endpoint_group:DeleteEndpointGroup" +vpn_endpoint_group_list = "openstackclient.network.v2.vpnaas.endpoint_group:ListEndpointGroup" +vpn_endpoint_group_set = "openstackclient.network.v2.vpnaas.endpoint_group:SetEndpointGroup" +vpn_endpoint_group_show = "openstackclient.network.v2.vpnaas.endpoint_group:ShowEndpointGroup" +vpn_ike_policy_create = "openstackclient.network.v2.vpnaas.ikepolicy:CreateIKEPolicy" +vpn_ike_policy_delete = "openstackclient.network.v2.vpnaas.ikepolicy:DeleteIKEPolicy" +vpn_ike_policy_list = "openstackclient.network.v2.vpnaas.ikepolicy:ListIKEPolicy" +vpn_ike_policy_set = "openstackclient.network.v2.vpnaas.ikepolicy:SetIKEPolicy" +vpn_ike_policy_show = "openstackclient.network.v2.vpnaas.ikepolicy:ShowIKEPolicy" +vpn_ipsec_policy_create = "openstackclient.network.v2.vpnaas.ipsecpolicy:CreateIPsecPolicy" +vpn_ipsec_policy_delete = "openstackclient.network.v2.vpnaas.ipsecpolicy:DeleteIPsecPolicy" +vpn_ipsec_policy_list = "openstackclient.network.v2.vpnaas.ipsecpolicy:ListIPsecPolicy" +vpn_ipsec_policy_set = "openstackclient.network.v2.vpnaas.ipsecpolicy:SetIPsecPolicy" +vpn_ipsec_policy_show = "openstackclient.network.v2.vpnaas.ipsecpolicy:ShowIPsecPolicy" +vpn_ipsec_site_connection_create = "openstackclient.network.v2.vpnaas.ipsec_site_connection:CreateIPsecSiteConnection" +vpn_ipsec_site_connection_delete = "openstackclient.network.v2.vpnaas.ipsec_site_connection:DeleteIPsecSiteConnection" +vpn_ipsec_site_connection_list = "openstackclient.network.v2.vpnaas.ipsec_site_connection:ListIPsecSiteConnection" +vpn_ipsec_site_connection_set = "openstackclient.network.v2.vpnaas.ipsec_site_connection:SetIPsecSiteConnection" +vpn_ipsec_site_connection_show = "openstackclient.network.v2.vpnaas.ipsec_site_connection:ShowIPsecSiteConnection" +vpn_service_create = "openstackclient.network.v2.vpnaas.vpnservice:CreateVPNService" +vpn_service_delete = "openstackclient.network.v2.vpnaas.vpnservice:DeleteVPNService" +vpn_service_list = "openstackclient.network.v2.vpnaas.vpnservice:ListVPNService" +vpn_service_set = "openstackclient.network.v2.vpnaas.vpnservice:SetVPNSercice" +vpn_service_show = "openstackclient.network.v2.vpnaas.vpnservice:ShowVPNService" + +[project.entry-points."openstack.object_store.v1"] +object_store_account_set = "openstackclient.object.v1.account:SetAccount" +object_store_account_show = "openstackclient.object.v1.account:ShowAccount" +object_store_account_unset = "openstackclient.object.v1.account:UnsetAccount" +container_create = "openstackclient.object.v1.container:CreateContainer" +container_delete = "openstackclient.object.v1.container:DeleteContainer" +container_list = "openstackclient.object.v1.container:ListContainer" +container_save = "openstackclient.object.v1.container:SaveContainer" +container_set = "openstackclient.object.v1.container:SetContainer" +container_show = "openstackclient.object.v1.container:ShowContainer" +container_unset = "openstackclient.object.v1.container:UnsetContainer" +object_create = "openstackclient.object.v1.object:CreateObject" +object_delete = "openstackclient.object.v1.object:DeleteObject" +object_list = "openstackclient.object.v1.object:ListObject" +object_save = "openstackclient.object.v1.object:SaveObject" +object_set = "openstackclient.object.v1.object:SetObject" +object_show = "openstackclient.object.v1.object:ShowObject" +object_unset = "openstackclient.object.v1.object:UnsetObject" + +[project.entry-points."openstack.share.v2"] +share_group_create = "openstackclient.share.v2.share_groups:CreateShareGroup" +share_group_delete = "openstackclient.share.v2.share_groups:DeleteShareGroup" +share_group_list = "openstackclient.share.v2.share_groups:ListShareGroup" +share_group_show = "openstackclient.share.v2.share_groups:ShowShareGroup" +share_group_set = "openstackclient.share.v2.share_groups:SetShareGroup" +share_group_unset = "openstackclient.share.v2.share_groups:UnsetShareGroup" +share_group_type_create = "openstackclient.share.v2.share_group_types:CreateShareGroupType" +share_group_type_delete = "openstackclient.share.v2.share_group_types:DeleteShareGroupType" +share_group_type_list = "openstackclient.share.v2.share_group_types:ListShareGroupType" +share_group_type_show = "openstackclient.share.v2.share_group_types:ShowShareGroupType" +share_group_type_set = "openstackclient.share.v2.share_group_types:SetShareGroupType" +share_group_type_unset = "openstackclient.share.v2.share_group_types:UnsetShareGroupType" +share_group_type_access_create = "openstackclient.share.v2.share_group_type_access:ShareGroupTypeAccessAllow" +share_group_type_access_list = "openstackclient.share.v2.share_group_type_access:ListShareGroupTypeAccess" +share_group_type_access_delete = "openstackclient.share.v2.share_group_type_access:ShareGroupTypeAccessDeny" +share_group_snapshot_create = "openstackclient.share.v2.share_group_snapshots:CreateShareGroupSnapshot" +share_group_snapshot_delete = "openstackclient.share.v2.share_group_snapshots:DeleteShareGroupSnapshot" +share_group_snapshot_show = "openstackclient.share.v2.share_group_snapshots:ShowShareGroupSnapshot" +share_group_snapshot_list = "openstackclient.share.v2.share_group_snapshots:ListShareGroupSnapshot" +share_group_snapshot_set = "openstackclient.share.v2.share_group_snapshots:SetShareGroupSnapshot" +share_group_snapshot_unset = "openstackclient.share.v2.share_group_snapshots:UnsetShareGroupSnapshot" +share_group_snapshot_members_list = "openstackclient.share.v2.share_group_snapshots:ListShareGroupSnapshotMembers" +share_lock_create = "openstackclient.share.v2.resource_locks:CreateResourceLock" +share_lock_list = "openstackclient.share.v2.resource_locks:ListResourceLock" +share_lock_show = "openstackclient.share.v2.resource_locks:ShowResourceLock" +share_lock_set = "openstackclient.share.v2.resource_locks:SetResourceLock" +share_lock_unset = "openstackclient.share.v2.resource_locks:UnsetResourceLock" +share_lock_delete = "openstackclient.share.v2.resource_locks:DeleteResourceLock" +share_message_delete = "openstackclient.share.v2.messages:DeleteMessage" +share_message_list = "openstackclient.share.v2.messages:ListMessage" +share_message_show = "openstackclient.share.v2.messages:ShowMessage" +share_security_service_create = "openstackclient.share.v2.security_services:CreateShareSecurityService" +share_security_service_delete = "openstackclient.share.v2.security_services:DeleteShareSecurityService" +share_security_service_show = "openstackclient.share.v2.security_services:ShowShareSecurityService" +share_security_service_set = "openstackclient.share.v2.security_services:SetShareSecurityService" +share_security_service_unset = "openstackclient.share.v2.security_services:UnsetShareSecurityService" +share_security_service_list = "openstackclient.share.v2.security_services:ListShareSecurityService" +share_service_set = "openstackclient.share.v2.services:SetShareService" +share_service_list = "openstackclient.share.v2.services:ListShareService" +share_service_ensure_shares = "openstackclient.share.v2.services:EnsureShareService" + +[project.entry-points."openstack.volume.v2"] +consistency_group_add_volume = "openstackclient.volume.v2.consistency_group:AddVolumeToConsistencyGroup" +consistency_group_create = "openstackclient.volume.v2.consistency_group:CreateConsistencyGroup" +consistency_group_delete = "openstackclient.volume.v2.consistency_group:DeleteConsistencyGroup" +consistency_group_list = "openstackclient.volume.v2.consistency_group:ListConsistencyGroup" +consistency_group_remove_volume = "openstackclient.volume.v2.consistency_group:RemoveVolumeFromConsistencyGroup" +consistency_group_set = "openstackclient.volume.v2.consistency_group:SetConsistencyGroup" +consistency_group_show = "openstackclient.volume.v2.consistency_group:ShowConsistencyGroup" +consistency_group_snapshot_create = "openstackclient.volume.v2.consistency_group_snapshot:CreateConsistencyGroupSnapshot" +consistency_group_snapshot_delete = "openstackclient.volume.v2.consistency_group_snapshot:DeleteConsistencyGroupSnapshot" +consistency_group_snapshot_list = "openstackclient.volume.v2.consistency_group_snapshot:ListConsistencyGroupSnapshot" +consistency_group_snapshot_show = "openstackclient.volume.v2.consistency_group_snapshot:ShowConsistencyGroupSnapshot" +volume_create = "openstackclient.volume.v2.volume:CreateVolume" +volume_delete = "openstackclient.volume.v2.volume:DeleteVolume" +volume_list = "openstackclient.volume.v2.volume:ListVolume" +volume_migrate = "openstackclient.volume.v2.volume:MigrateVolume" +volume_set = "openstackclient.volume.v2.volume:SetVolume" +volume_show = "openstackclient.volume.v2.volume:ShowVolume" +volume_unset = "openstackclient.volume.v2.volume:UnsetVolume" +volume_backup_create = "openstackclient.volume.v2.volume_backup:CreateVolumeBackup" +volume_backup_delete = "openstackclient.volume.v2.volume_backup:DeleteVolumeBackup" +volume_backup_list = "openstackclient.volume.v2.volume_backup:ListVolumeBackup" +volume_backup_restore = "openstackclient.volume.v2.volume_backup:RestoreVolumeBackup" +volume_backup_set = "openstackclient.volume.v2.volume_backup:SetVolumeBackup" +volume_backup_show = "openstackclient.volume.v2.volume_backup:ShowVolumeBackup" +volume_backup_record_export = "openstackclient.volume.v2.backup_record:ExportBackupRecord" +volume_backup_record_import = "openstackclient.volume.v2.backup_record:ImportBackupRecord" +volume_backend_capability_show = "openstackclient.volume.v2.volume_backend:ShowCapability" +volume_backend_pool_list = "openstackclient.volume.v2.volume_backend:ListPool" +volume_host_failover = "openstackclient.volume.v2.volume_host:FailoverVolumeHost" +volume_host_set = "openstackclient.volume.v2.volume_host:SetVolumeHost" +volume_snapshot_create = "openstackclient.volume.v2.volume_snapshot:CreateVolumeSnapshot" +volume_snapshot_delete = "openstackclient.volume.v2.volume_snapshot:DeleteVolumeSnapshot" +volume_snapshot_list = "openstackclient.volume.v2.volume_snapshot:ListVolumeSnapshot" +volume_snapshot_set = "openstackclient.volume.v2.volume_snapshot:SetVolumeSnapshot" +volume_snapshot_show = "openstackclient.volume.v2.volume_snapshot:ShowVolumeSnapshot" +volume_snapshot_unset = "openstackclient.volume.v2.volume_snapshot:UnsetVolumeSnapshot" +volume_type_create = "openstackclient.volume.v2.volume_type:CreateVolumeType" +volume_type_delete = "openstackclient.volume.v2.volume_type:DeleteVolumeType" +volume_type_list = "openstackclient.volume.v2.volume_type:ListVolumeType" +volume_type_set = "openstackclient.volume.v2.volume_type:SetVolumeType" +volume_type_show = "openstackclient.volume.v2.volume_type:ShowVolumeType" +volume_type_unset = "openstackclient.volume.v2.volume_type:UnsetVolumeType" +volume_qos_associate = "openstackclient.volume.v2.qos_specs:AssociateQos" +volume_qos_create = "openstackclient.volume.v2.qos_specs:CreateQos" +volume_qos_delete = "openstackclient.volume.v2.qos_specs:DeleteQos" +volume_qos_disassociate = "openstackclient.volume.v2.qos_specs:DisassociateQos" +volume_qos_list = "openstackclient.volume.v2.qos_specs:ListQos" +volume_qos_set = "openstackclient.volume.v2.qos_specs:SetQos" +volume_qos_show = "openstackclient.volume.v2.qos_specs:ShowQos" +volume_qos_unset = "openstackclient.volume.v2.qos_specs:UnsetQos" +volume_service_list = "openstackclient.volume.v2.service:ListService" +volume_service_set = "openstackclient.volume.v2.service:SetService" +volume_transfer_request_accept = "openstackclient.volume.v2.volume_transfer_request:AcceptTransferRequest" +volume_transfer_request_create = "openstackclient.volume.v2.volume_transfer_request:CreateTransferRequest" +volume_transfer_request_delete = "openstackclient.volume.v2.volume_transfer_request:DeleteTransferRequest" +volume_transfer_request_list = "openstackclient.volume.v2.volume_transfer_request:ListTransferRequest" +volume_transfer_request_show = "openstackclient.volume.v2.volume_transfer_request:ShowTransferRequest" + +[project.entry-points."openstack.volume.v3"] +block_storage_log_level_list = "openstackclient.volume.v3.block_storage_log_level:BlockStorageLogLevelList" +block_storage_log_level_set = "openstackclient.volume.v3.block_storage_log_level:BlockStorageLogLevelSet" +block_storage_cleanup = "openstackclient.volume.v3.block_storage_cleanup:BlockStorageCleanup" +block_storage_volume_manageable_list = "openstackclient.volume.v3.block_storage_manage:BlockStorageManageVolumes" +block_storage_snapshot_manageable_list = "openstackclient.volume.v3.block_storage_manage:BlockStorageManageSnapshots" +consistency_group_add_volume = "openstackclient.volume.v3.consistency_group:AddVolumeToConsistencyGroup" +consistency_group_create = "openstackclient.volume.v3.consistency_group:CreateConsistencyGroup" +consistency_group_delete = "openstackclient.volume.v3.consistency_group:DeleteConsistencyGroup" +consistency_group_list = "openstackclient.volume.v3.consistency_group:ListConsistencyGroup" +consistency_group_remove_volume = "openstackclient.volume.v3.consistency_group:RemoveVolumeFromConsistencyGroup" +consistency_group_set = "openstackclient.volume.v3.consistency_group:SetConsistencyGroup" +consistency_group_show = "openstackclient.volume.v3.consistency_group:ShowConsistencyGroup" +consistency_group_snapshot_create = "openstackclient.volume.v3.consistency_group_snapshot:CreateConsistencyGroupSnapshot" +consistency_group_snapshot_delete = "openstackclient.volume.v3.consistency_group_snapshot:DeleteConsistencyGroupSnapshot" +consistency_group_snapshot_list = "openstackclient.volume.v3.consistency_group_snapshot:ListConsistencyGroupSnapshot" +consistency_group_snapshot_show = "openstackclient.volume.v3.consistency_group_snapshot:ShowConsistencyGroupSnapshot" +volume_create = "openstackclient.volume.v3.volume:CreateVolume" +volume_delete = "openstackclient.volume.v3.volume:DeleteVolume" +volume_list = "openstackclient.volume.v3.volume:ListVolume" +volume_migrate = "openstackclient.volume.v3.volume:MigrateVolume" +volume_set = "openstackclient.volume.v3.volume:SetVolume" +volume_show = "openstackclient.volume.v3.volume:ShowVolume" +volume_unset = "openstackclient.volume.v3.volume:UnsetVolume" +volume_attachment_create = "openstackclient.volume.v3.volume_attachment:CreateVolumeAttachment" +volume_attachment_delete = "openstackclient.volume.v3.volume_attachment:DeleteVolumeAttachment" +volume_attachment_list = "openstackclient.volume.v3.volume_attachment:ListVolumeAttachment" +volume_attachment_complete = "openstackclient.volume.v3.volume_attachment:CompleteVolumeAttachment" +volume_attachment_set = "openstackclient.volume.v3.volume_attachment:SetVolumeAttachment" +volume_attachment_show = "openstackclient.volume.v3.volume_attachment:ShowVolumeAttachment" +volume_backup_create = "openstackclient.volume.v3.volume_backup:CreateVolumeBackup" +volume_backup_delete = "openstackclient.volume.v3.volume_backup:DeleteVolumeBackup" +volume_backup_list = "openstackclient.volume.v3.volume_backup:ListVolumeBackup" +volume_backup_restore = "openstackclient.volume.v3.volume_backup:RestoreVolumeBackup" +volume_backup_set = "openstackclient.volume.v3.volume_backup:SetVolumeBackup" +volume_backup_unset = "openstackclient.volume.v3.volume_backup:UnsetVolumeBackup" +volume_backup_show = "openstackclient.volume.v3.volume_backup:ShowVolumeBackup" +volume_backend_capability_show = "openstackclient.volume.v3.volume_backend:ShowCapability" +volume_backend_pool_list = "openstackclient.volume.v3.volume_backend:ListPool" +volume_backup_record_export = "openstackclient.volume.v3.backup_record:ExportBackupRecord" +volume_backup_record_import = "openstackclient.volume.v3.backup_record:ImportBackupRecord" +volume_group_create = "openstackclient.volume.v3.volume_group:CreateVolumeGroup" +volume_group_delete = "openstackclient.volume.v3.volume_group:DeleteVolumeGroup" +volume_group_list = "openstackclient.volume.v3.volume_group:ListVolumeGroup" +volume_group_failover = "openstackclient.volume.v3.volume_group:FailoverVolumeGroup" +volume_group_set = "openstackclient.volume.v3.volume_group:SetVolumeGroup" +volume_group_show = "openstackclient.volume.v3.volume_group:ShowVolumeGroup" +volume_group_snapshot_create = "openstackclient.volume.v3.volume_group_snapshot:CreateVolumeGroupSnapshot" +volume_group_snapshot_delete = "openstackclient.volume.v3.volume_group_snapshot:DeleteVolumeGroupSnapshot" +volume_group_snapshot_list = "openstackclient.volume.v3.volume_group_snapshot:ListVolumeGroupSnapshot" +volume_group_snapshot_show = "openstackclient.volume.v3.volume_group_snapshot:ShowVolumeGroupSnapshot" +volume_group_type_create = "openstackclient.volume.v3.volume_group_type:CreateVolumeGroupType" +volume_group_type_delete = "openstackclient.volume.v3.volume_group_type:DeleteVolumeGroupType" +volume_group_type_list = "openstackclient.volume.v3.volume_group_type:ListVolumeGroupType" +volume_group_type_set = "openstackclient.volume.v3.volume_group_type:SetVolumeGroupType" +volume_group_type_show = "openstackclient.volume.v3.volume_group_type:ShowVolumeGroupType" +volume_host_failover = "openstackclient.volume.v3.volume_host:FailoverVolumeHost" +volume_host_set = "openstackclient.volume.v3.volume_host:SetVolumeHost" +volume_message_delete = "openstackclient.volume.v3.volume_message:DeleteMessage" +volume_message_list = "openstackclient.volume.v3.volume_message:ListMessages" +volume_message_show = "openstackclient.volume.v3.volume_message:ShowMessage" +block_storage_cluster_list = "openstackclient.volume.v3.block_storage_cluster:ListBlockStorageCluster" +block_storage_cluster_set = "openstackclient.volume.v3.block_storage_cluster:SetBlockStorageCluster" +block_storage_cluster_show = "openstackclient.volume.v3.block_storage_cluster:ShowBlockStorageCluster" +block_storage_resource_filter_list = "openstackclient.volume.v3.block_storage_resource_filter:ListBlockStorageResourceFilter" +block_storage_resource_filter_show = "openstackclient.volume.v3.block_storage_resource_filter:ShowBlockStorageResourceFilter" +volume_snapshot_create = "openstackclient.volume.v3.volume_snapshot:CreateVolumeSnapshot" +volume_snapshot_delete = "openstackclient.volume.v3.volume_snapshot:DeleteVolumeSnapshot" +volume_snapshot_list = "openstackclient.volume.v3.volume_snapshot:ListVolumeSnapshot" +volume_snapshot_set = "openstackclient.volume.v3.volume_snapshot:SetVolumeSnapshot" +volume_snapshot_show = "openstackclient.volume.v3.volume_snapshot:ShowVolumeSnapshot" +volume_snapshot_unset = "openstackclient.volume.v3.volume_snapshot:UnsetVolumeSnapshot" +volume_type_create = "openstackclient.volume.v3.volume_type:CreateVolumeType" +volume_type_delete = "openstackclient.volume.v3.volume_type:DeleteVolumeType" +volume_type_list = "openstackclient.volume.v3.volume_type:ListVolumeType" +volume_type_set = "openstackclient.volume.v3.volume_type:SetVolumeType" +volume_type_show = "openstackclient.volume.v3.volume_type:ShowVolumeType" +volume_type_unset = "openstackclient.volume.v3.volume_type:UnsetVolumeType" +volume_qos_associate = "openstackclient.volume.v3.qos_specs:AssociateQos" +volume_qos_create = "openstackclient.volume.v3.qos_specs:CreateQos" +volume_qos_delete = "openstackclient.volume.v3.qos_specs:DeleteQos" +volume_qos_disassociate = "openstackclient.volume.v3.qos_specs:DisassociateQos" +volume_qos_list = "openstackclient.volume.v3.qos_specs:ListQos" +volume_qos_set = "openstackclient.volume.v3.qos_specs:SetQos" +volume_qos_show = "openstackclient.volume.v3.qos_specs:ShowQos" +volume_qos_unset = "openstackclient.volume.v3.qos_specs:UnsetQos" +volume_service_list = "openstackclient.volume.v3.service:ListService" +volume_service_set = "openstackclient.volume.v3.service:SetService" +volume_transfer_request_accept = "openstackclient.volume.v3.volume_transfer_request:AcceptTransferRequest" +volume_transfer_request_create = "openstackclient.volume.v3.volume_transfer_request:CreateTransferRequest" +volume_transfer_request_delete = "openstackclient.volume.v3.volume_transfer_request:DeleteTransferRequest" +volume_transfer_request_list = "openstackclient.volume.v3.volume_transfer_request:ListTransferRequest" +volume_transfer_request_show = "openstackclient.volume.v3.volume_transfer_request:ShowTransferRequest" +volume_summary = "openstackclient.volume.v3.volume:VolumeSummary" +volume_revert = "openstackclient.volume.v3.volume:VolumeRevertToSnapshot" + +[tool.setuptools.packages.find] +include = ["openstackclient*"] + [tool.mypy] -python_version = "3.10" +python_version = "3.11" show_column_numbers = true show_error_context = true ignore_missing_imports = true follow_imports = "normal" incremental = true check_untyped_defs = true +disallow_untyped_defs = true warn_unused_ignores = true -# keep this in-sync with 'mypy.exclude' in '.pre-commit-config.yaml' -exclude = ''' -(?x)( - doc - | examples - | releasenotes - ) -''' +exclude = "(?x)(doc | examples | hacking | releasenotes)" + +[[tool.mypy.overrides]] +module = [ + "openstackclient.tests.functional.*", +] +disallow_untyped_defs = false [[tool.mypy.overrides]] -module = ["openstackclient.tests.unit.*"] +module = [ + "openstackclient.tests.unit.*", +] ignore_errors = true [tool.ruff] line-length = 79 -target-version = "py310" [tool.ruff.format] quote-style = "preserve" docstring-code-format = true [tool.ruff.lint] -select = ["E4", "E7", "E9", "F", "S", "U"] +select = ["E4", "E5", "E7", "E9", "F", "G", "RUF", "S", "UP"] +ignore = [ + "S101", # Asserts are used for type narrowing + # the following are ignored because they don't provide enough value for the + # changes required + "RUF012", # Mutable default value for class attribute +] +# don't remove hacking (H) or openstackclient (O) checks +external = ["H", "O"] [tool.ruff.lint.per-file-ignores] -"openstackclient/tests/*" = ["S"] +"openstackclient/tests/*" = ["E501", "S"] diff --git a/releasenotes/notes/Move-Dynamic-Routing-CLI-to-OSC-98b3dbd6285810b6.yaml b/releasenotes/notes/Move-Dynamic-Routing-CLI-to-OSC-98b3dbd6285810b6.yaml new file mode 100644 index 0000000000..2a1184b046 --- /dev/null +++ b/releasenotes/notes/Move-Dynamic-Routing-CLI-to-OSC-98b3dbd6285810b6.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + ``Neutron Dynamic routing`` OSC commands were moved from + ``python-neutronclient``. The following commands are included: + + - ``bgp peer create|delete|list|show|set`` + - ``bgp speaker create|delete|list|show|set`` + - ``bgp speaker add network|add peer|remove network|remove peer`` + - ``bgp speaker list advertised routes`` + - ``bgp dragent add speaker|remove speaker|list`` diff --git a/releasenotes/notes/Move-bgpvpn-CLI-to-OSC-4df0140af2b40577.yaml b/releasenotes/notes/Move-bgpvpn-CLI-to-OSC-4df0140af2b40577.yaml new file mode 100644 index 0000000000..f23ba14b02 --- /dev/null +++ b/releasenotes/notes/Move-bgpvpn-CLI-to-OSC-4df0140af2b40577.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + ``bgpvpn`` OSC commands were moved from ``python-neutronclient``. The + following commands are included: + + - ``bgpvpn create|delete|list|show|set|unset`` + - ``bgpvpn network association create|delete|list}show`` + - ``bgpvpn router association create|delete|list|show|set|unset`` + - ``bgpvpn port association create|delete|list|show|set|unset`` diff --git a/releasenotes/notes/Move-vpnaas-CLI-to-OSC-901b711d619e813e.yaml b/releasenotes/notes/Move-vpnaas-CLI-to-OSC-901b711d619e813e.yaml new file mode 100644 index 0000000000..211d6996af --- /dev/null +++ b/releasenotes/notes/Move-vpnaas-CLI-to-OSC-901b711d619e813e.yaml @@ -0,0 +1,11 @@ +--- +features: + - | + ``neutron-vpnaas`` OSC commands were moved from + ``python-neutronclient``. The following commands are included: + + * ``vpn endpoint group create|delete|list|set|show`` + * ``vpn ike policy create|delete|list|set|show`` + * ``vpn ipsec policy create|delete|list|set|show`` + * ``vpn ipsec site connection create|delete|list|set|show`` + * ``vpn service create|delete|list|set|show`` diff --git a/releasenotes/notes/add-chunk-size-to-image-save-37871f9e62693264.yaml b/releasenotes/notes/add-chunk-size-to-image-save-37871f9e62693264.yaml new file mode 100644 index 0000000000..433dc87733 --- /dev/null +++ b/releasenotes/notes/add-chunk-size-to-image-save-37871f9e62693264.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--chunk-size`` option to ``image save`` command to control the size + of bytes to read at one time. diff --git a/releasenotes/notes/add-cluster-option-to-volume-migration-9fe0cc84e9c80a4c.yaml b/releasenotes/notes/add-cluster-option-to-volume-migration-9fe0cc84e9c80a4c.yaml new file mode 100644 index 0000000000..5b9e878056 --- /dev/null +++ b/releasenotes/notes/add-cluster-option-to-volume-migration-9fe0cc84e9c80a4c.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + The ``volume migration`` command now supports the ``--cluster`` + optional argument, allowing volumes to be migrated to a destination + cluster. This feature requires Cinder API microversion 3.16 or + higher and is mutually exclusive with the ``--host`` option. \ No newline at end of file diff --git a/releasenotes/notes/add-ha-chassis-priority-to-add-router-to-agent-27c0a79d899a425c.yaml b/releasenotes/notes/add-ha-chassis-priority-to-add-router-to-agent-27c0a79d899a425c.yaml new file mode 100644 index 0000000000..0b9ca21413 --- /dev/null +++ b/releasenotes/notes/add-ha-chassis-priority-to-add-router-to-agent-27c0a79d899a425c.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add a new option ``--ha-chassis-priority`` to the + ``network agent add router`` command. This allows setting + the HA Chassis priority when adding a router to an L3 agent. diff --git a/releasenotes/notes/add-image-metadef-namespace-object-delete-b6b2de24fc66e602.yaml b/releasenotes/notes/add-image-metadef-namespace-object-delete-b6b2de24fc66e602.yaml new file mode 100644 index 0000000000..7861a0c7a0 --- /dev/null +++ b/releasenotes/notes/add-image-metadef-namespace-object-delete-b6b2de24fc66e602.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Adds operation which deletes all metadef object inside a namespace. diff --git a/releasenotes/notes/add-image-metadef-property-delete-1e1bb8410130d901.yaml b/releasenotes/notes/add-image-metadef-property-delete-1e1bb8410130d901.yaml new file mode 100644 index 0000000000..09aff5905c --- /dev/null +++ b/releasenotes/notes/add-image-metadef-property-delete-1e1bb8410130d901.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The ``image property delete`` command will now delete all properties in + the provided namespace if no property is provided. diff --git a/releasenotes/notes/add-image-metadef-tag-list-64cb60f80d4331e6.yaml b/releasenotes/notes/add-image-metadef-tag-list-64cb60f80d4331e6.yaml new file mode 100644 index 0000000000..655ed64c2a --- /dev/null +++ b/releasenotes/notes/add-image-metadef-tag-list-64cb60f80d4331e6.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add support for metadef-tags associated to namespace for adding, updating, + removing a tag. diff --git a/releasenotes/notes/add-image-options-dcbc4ead7822c495.yaml b/releasenotes/notes/add-image-options-dcbc4ead7822c495.yaml new file mode 100644 index 0000000000..7a528d7c9d --- /dev/null +++ b/releasenotes/notes/add-image-options-dcbc4ead7822c495.yaml @@ -0,0 +1,4 @@ +--- +features: + - The ``os_hash_algo`` and ``os_hash_value image`` attributes are now shown + in the ``image list --long`` output. diff --git a/releasenotes/notes/add-missing-network-pagination-opts-6782692ae98fe9e8.yaml b/releasenotes/notes/add-missing-network-pagination-opts-6782692ae98fe9e8.yaml new file mode 100644 index 0000000000..f1a432eab4 --- /dev/null +++ b/releasenotes/notes/add-missing-network-pagination-opts-6782692ae98fe9e8.yaml @@ -0,0 +1,35 @@ +--- +features: + - | + A number of networking-related *list* commands now support the ``--marker`` + and ``--limit`` options for marker-based pagination: + + - ``address group list`` + - ``address scope list`` + - ``floating ip list`` + - ``floating ip port forwarding list`` + - ``ip availability list`` + - ``network agent list`` + - ``network flavor list`` + - ``network flavor profile list`` + - ``network list`` + - ``network l3 conntrack helper list`` + - ``network meter list`` + - ``network meter rule list`` + - ``network qos policy list`` + - ``network qos rule type list`` + - ``network rbac list`` + - ``network segment list`` + - ``network segment range list`` + - ``network service provider list`` + - ``port list`` + - ``router list`` + - ``security group list`` + - ``security group rule list`` + - ``subnet list`` + - ``subnet pool list`` + + For more information on this feature, refer to the API documentation for + the `Networking service`__. + + .. __: https://docs.openstack.org/api-ref/network/v2/#pagination diff --git a/releasenotes/notes/add-pagination-to-user-and-project-list-7d41e0dc417f85d4.yaml b/releasenotes/notes/add-pagination-to-user-and-project-list-7d41e0dc417f85d4.yaml new file mode 100644 index 0000000000..d7560797ce --- /dev/null +++ b/releasenotes/notes/add-pagination-to-user-and-project-list-7d41e0dc417f85d4.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Added ``--limit`` and ``--marker`` options to the + ``openstack user list`` and ``openstack project list`` commands to enable + marker-based pagination through large result sets. diff --git a/releasenotes/notes/add-pvlan-support-062a4500420862c2.yaml b/releasenotes/notes/add-pvlan-support-062a4500420862c2.yaml new file mode 100644 index 0000000000..e868ea4320 --- /dev/null +++ b/releasenotes/notes/add-pvlan-support-062a4500420862c2.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Adds attributes of Private VLAN (PVLAN) parameters to the networking + service. Users can now create and set ``--pvlan`` in a network, and the + same with ``--pvlan-type`` and ``--pvlan-community`` properties when + managing Neutron ports. Networks cannot have pvlan enabled if port security + is disabled. Ports cannot have pvlan properties if port security is + disabled. pvlan-community can only be set if pvlan-type is community. + Enabling PVLAN service plugin is required for using these parameters. diff --git a/releasenotes/notes/add-router-routes-quota-option-c4d5e6f789012345.yaml b/releasenotes/notes/add-router-routes-quota-option-c4d5e6f789012345.yaml new file mode 100644 index 0000000000..c634f21c67 --- /dev/null +++ b/releasenotes/notes/add-router-routes-quota-option-c4d5e6f789012345.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Added ``--router-routes`` option to the ``quota set`` command and + ``router_routes`` to the ``quota show`` and ``quota list --network`` + output. This exposes the new Neutron ``router_route`` per-project + quota that controls the total number of extra routes across all + routers in a project. diff --git a/releasenotes/notes/add-set-router-to-agent-d4e343cd5bf34573.yaml b/releasenotes/notes/add-set-router-to-agent-d4e343cd5bf34573.yaml new file mode 100644 index 0000000000..efb60f9050 --- /dev/null +++ b/releasenotes/notes/add-set-router-to-agent-d4e343cd5bf34573.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + Add a new ``network agent router set`` command that allows updating + properties of a router associated to an ML2/OVN L3 agent. Currently + supports setting ``--ha-chassis-priority``. diff --git a/releasenotes/notes/add-user-project-enabled-filters-9f2090cdcc97b667.yaml b/releasenotes/notes/add-user-project-enabled-filters-9f2090cdcc97b667.yaml new file mode 100644 index 0000000000..a812ca6b73 --- /dev/null +++ b/releasenotes/notes/add-user-project-enabled-filters-9f2090cdcc97b667.yaml @@ -0,0 +1,4 @@ +--- +features: + - | + Add filters to search for enabled and disabled users and projects. diff --git a/releasenotes/notes/add-volume-backup-project-filter-6c09b2c8aba83341.yaml b/releasenotes/notes/add-volume-backup-project-filter-6c09b2c8aba83341.yaml new file mode 100644 index 0000000000..06bc7a4f02 --- /dev/null +++ b/releasenotes/notes/add-volume-backup-project-filter-6c09b2c8aba83341.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--project`` option to ``volume backup list`` command, + to allow filtering for projects when listing volume backups. diff --git a/releasenotes/notes/add-volume-list-property-option-62008dc24762663b.yaml b/releasenotes/notes/add-volume-list-property-option-62008dc24762663b.yaml new file mode 100644 index 0000000000..ab4b60025a --- /dev/null +++ b/releasenotes/notes/add-volume-list-property-option-62008dc24762663b.yaml @@ -0,0 +1,3 @@ +--- +features: + - Add ``--property`` option to ``volume list`` command to filter volumes. diff --git a/releasenotes/notes/aggregate-list-filters-7b9f3a1c8e2d4f60.yaml b/releasenotes/notes/aggregate-list-filters-7b9f3a1c8e2d4f60.yaml new file mode 100644 index 0000000000..befb755853 --- /dev/null +++ b/releasenotes/notes/aggregate-list-filters-7b9f3a1c8e2d4f60.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Added ``--availability-zone`` and ``--host`` options to the + ``aggregate list`` command. These allow the results to be filtered by + availability zone name and by aggregates containing a given host. The + compute API does not support server-side filtering of aggregates, so the + filtering is performed client-side. When both options are supplied they + are combined with AND, returning only aggregates that match every filter. diff --git a/releasenotes/notes/bug-1648317-2d12dabc357c4d52.yaml b/releasenotes/notes/bug-1648317-2d12dabc357c4d52.yaml new file mode 100644 index 0000000000..ffd1ff90b6 --- /dev/null +++ b/releasenotes/notes/bug-1648317-2d12dabc357c4d52.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Add ``--project`` and --project-domain`` options to the following network + commands: + + - ``openstack security group rule list`` + + [Bug `1648317 `_] + diff --git a/releasenotes/notes/bug-2089188-snapshot-mv-366-force.yaml b/releasenotes/notes/bug-2089188-snapshot-mv-366-force.yaml new file mode 100644 index 0000000000..f0f1a94874 --- /dev/null +++ b/releasenotes/notes/bug-2089188-snapshot-mv-366-force.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + Fix ``volume snapshot create`` to work with Cinder API microversion 3.66 + and later. Since version 3.66, the 'force' parameter is no longer + needed for snapshot creation as in-use volumes can be snapshotted by + default. The command now only passes the 'force' parameter for version + prior to 3.66. + [Bug `2089188 `_] diff --git a/releasenotes/notes/bug-2126565-a119ac242d9ac795.yaml b/releasenotes/notes/bug-2126565-a119ac242d9ac795.yaml new file mode 100644 index 0000000000..87fe689b18 --- /dev/null +++ b/releasenotes/notes/bug-2126565-a119ac242d9ac795.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + Running ``openstack application credential show`` on + a non-existent application credential does not + raise an exception. + + [Bug `2126565 `_] diff --git a/releasenotes/notes/bug-2137636-fix-quota-usage-display-2d8f07dccc21f79c.yaml b/releasenotes/notes/bug-2137636-fix-quota-usage-display-2d8f07dccc21f79c.yaml new file mode 100644 index 0000000000..818e29ae82 --- /dev/null +++ b/releasenotes/notes/bug-2137636-fix-quota-usage-display-2d8f07dccc21f79c.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + Fix ``openstack quota show --usage`` to correctly display resource usage + and reservation. diff --git a/releasenotes/notes/bug-2138903-f75c7348f22db195.yaml b/releasenotes/notes/bug-2138903-f75c7348f22db195.yaml new file mode 100644 index 0000000000..249242146d --- /dev/null +++ b/releasenotes/notes/bug-2138903-f75c7348f22db195.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + The ``--all-stores`` of the ``image import`` command is now correctly + treated as a boolean flag. diff --git a/releasenotes/notes/drop-flavor-rxtx_factor-9062e1857de2353a.yaml b/releasenotes/notes/drop-flavor-rxtx_factor-9062e1857de2353a.yaml new file mode 100644 index 0000000000..9d3f8870e8 --- /dev/null +++ b/releasenotes/notes/drop-flavor-rxtx_factor-9062e1857de2353a.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The ``flavor create`` command will no longer provide a default value for + ``rxtx_factor``, following the removal of this field in Nova API + microversion v2.102. This field was a no-op for all supported virt + drivers so this should not affect any users. diff --git a/releasenotes/notes/drop-python-310-6771fd5b530ceaba.yaml b/releasenotes/notes/drop-python-310-6771fd5b530ceaba.yaml new file mode 100644 index 0000000000..c76dfd61ba --- /dev/null +++ b/releasenotes/notes/drop-python-310-6771fd5b530ceaba.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + Support for Python 3.10 has been dropped. diff --git a/releasenotes/notes/fip-filter-opts-a847f8743fef467f.yaml b/releasenotes/notes/fip-filter-opts-a847f8743fef467f.yaml new file mode 100644 index 0000000000..997075c272 --- /dev/null +++ b/releasenotes/notes/fip-filter-opts-a847f8743fef467f.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + The ``--network``, ``--port``, and ``--router`` options of the ``floating + ip list`` command can now be specified multiple times. diff --git a/releasenotes/notes/fix-image-set-project-accept-owner-bug-2136795.yaml b/releasenotes/notes/fix-image-set-project-accept-owner-bug-2136795.yaml new file mode 100644 index 0000000000..4cd755b3fe --- /dev/null +++ b/releasenotes/notes/fix-image-set-project-accept-owner-bug-2136795.yaml @@ -0,0 +1,10 @@ +--- +fixes: + - | + Fix a bug where using ``openstack image set --project + --accept `` incorrectly changed the image owner to the specified + project instead of only updating the member status. The ``--project`` + parameter when used with ``--accept``, ``--reject``, or ``--pending`` + should only identify which member's status to update, not change the + image ownership. + [Bug `2136795 `_] diff --git a/releasenotes/notes/fix-resize-server-args-required-2e9013bcbf207f6a.yaml b/releasenotes/notes/fix-resize-server-args-required-2e9013bcbf207f6a.yaml new file mode 100644 index 0000000000..e1faf7e601 --- /dev/null +++ b/releasenotes/notes/fix-resize-server-args-required-2e9013bcbf207f6a.yaml @@ -0,0 +1,8 @@ +--- +fixes: + - | + The ``openstack server resize`` command now requires the ``--flavor`` + option or one of the deprecated ``--confirm`` or ``--revert`` options + to be provided. + Previously, the command would silently exit successfully without + performing any action if no option was provided. diff --git a/releasenotes/notes/fix-user-password-set-34175b7577b121e9.yaml b/releasenotes/notes/fix-user-password-set-34175b7577b121e9.yaml new file mode 100644 index 0000000000..3208b47106 --- /dev/null +++ b/releasenotes/notes/fix-user-password-set-34175b7577b121e9.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fix ``openstack user password set`` command to support user + self-service password reset. + [Bug `2144315 `_] + diff --git a/releasenotes/notes/fix-volume-group-type-show-b1c3a2f9e8d047ab.yaml b/releasenotes/notes/fix-volume-group-type-show-b1c3a2f9e8d047ab.yaml new file mode 100644 index 0000000000..014005b776 --- /dev/null +++ b/releasenotes/notes/fix-volume-group-type-show-b1c3a2f9e8d047ab.yaml @@ -0,0 +1,7 @@ +--- +fixes: + - | + Fixed ``volume group type show`` command which was failing with + ``AttributeError: 'Namespace' object has no attribute 'group'`` due to + a typo in the ``take_action`` method. + [Bug `2150856 `_] diff --git a/releasenotes/notes/flavor-id-auto-e21157f97dc1d7f2.yaml b/releasenotes/notes/flavor-id-auto-e21157f97dc1d7f2.yaml new file mode 100644 index 0000000000..96b56b8df6 --- /dev/null +++ b/releasenotes/notes/flavor-id-auto-e21157f97dc1d7f2.yaml @@ -0,0 +1,6 @@ +--- +deprecations: + - | + The ``--id auto`` alias for the ``flavor create`` command is deprecated + for removal. Omit the option entirely to ensure the server creates the + ID for you. diff --git a/releasenotes/notes/keystone-create-user-no-password-619bcddcd046dda8.yaml b/releasenotes/notes/keystone-create-user-no-password-619bcddcd046dda8.yaml new file mode 100644 index 0000000000..7cd6acba0e --- /dev/null +++ b/releasenotes/notes/keystone-create-user-no-password-619bcddcd046dda8.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + [Bug `2136148 `_] Keystone allows + users to be created with no password but no value should be submitted for + the password instead of a ``null`` value. diff --git a/releasenotes/notes/limits-project-domain-option-84bfbb0e30e21b73.yaml b/releasenotes/notes/limits-project-domain-option-84bfbb0e30e21b73.yaml new file mode 100644 index 0000000000..ab9468022c --- /dev/null +++ b/releasenotes/notes/limits-project-domain-option-84bfbb0e30e21b73.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Add ``--project-domain`` option for the ``limit list`` and ``limit create`` + commands. diff --git a/releasenotes/notes/manila-shares-attachments-09ca572e615bbe1f.yaml b/releasenotes/notes/manila-shares-attachments-09ca572e615bbe1f.yaml new file mode 100644 index 0000000000..7df454982a --- /dev/null +++ b/releasenotes/notes/manila-shares-attachments-09ca572e615bbe1f.yaml @@ -0,0 +1,8 @@ +--- +features: + - | + Manila is a service in OpenStack that enables shared filesystems. + Nova can now link the shared resources provided by Manila to instances + using virtiofs using the following + commands: ``server share list``, ``server share show``, + ``server add share``, ``server remove share``. diff --git a/releasenotes/notes/max-items-parameter-fd3e9ad456143842.yaml b/releasenotes/notes/max-items-parameter-fd3e9ad456143842.yaml new file mode 100644 index 0000000000..3ee7184403 --- /dev/null +++ b/releasenotes/notes/max-items-parameter-fd3e9ad456143842.yaml @@ -0,0 +1,6 @@ +--- +features: + - | + A number of list commands now accept a ``--max-items`` parameter. When + specified, this configures the amount of resources to retrieve from the + server. diff --git a/releasenotes/notes/migrate-domain-to-sdk-da6ec38221e79a37.yaml b/releasenotes/notes/migrate-domain-to-sdk-da6ec38221e79a37.yaml new file mode 100644 index 0000000000..ce26909b4a --- /dev/null +++ b/releasenotes/notes/migrate-domain-to-sdk-da6ec38221e79a37.yaml @@ -0,0 +1,10 @@ +--- +upgrade: + - | + The following commands have been migrated to SDK: + + - ``domain create`` + - ``domain delete`` + - ``domain list`` + - ``domain set`` + - ``domain show`` diff --git a/releasenotes/notes/migrate-endpoint-project-association-to-sdk-6528e0c722dce87a.yaml b/releasenotes/notes/migrate-endpoint-project-association-to-sdk-6528e0c722dce87a.yaml new file mode 100644 index 0000000000..883cf0db0a --- /dev/null +++ b/releasenotes/notes/migrate-endpoint-project-association-to-sdk-6528e0c722dce87a.yaml @@ -0,0 +1,7 @@ +--- +upgrade: + - | + The following commands have been migrated to SDK: + + - ``endpoint add project`` + - ``endpoint remove project`` diff --git a/releasenotes/notes/migrate-federation-protocol-to-sdk-43dc2b50fb277da6.yaml b/releasenotes/notes/migrate-federation-protocol-to-sdk-43dc2b50fb277da6.yaml new file mode 100644 index 0000000000..ea3808a7b0 --- /dev/null +++ b/releasenotes/notes/migrate-federation-protocol-to-sdk-43dc2b50fb277da6.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Migrate ``federation protocol`` commands from keystoneclient to SDK. +upgrade: + - | + Filtering in ``federation protocol`` commands is now case sensitive. diff --git a/releasenotes/notes/migrate-group-to-sdk-59beef31a7c40bbb.yaml b/releasenotes/notes/migrate-group-to-sdk-59beef31a7c40bbb.yaml new file mode 100644 index 0000000000..3cbb9b3dd1 --- /dev/null +++ b/releasenotes/notes/migrate-group-to-sdk-59beef31a7c40bbb.yaml @@ -0,0 +1,4 @@ +--- +upgrade: + - | + Migrate ``group`` commands from keystoneclient to SDK. diff --git a/releasenotes/notes/migrate-identity-provider-to-sdk-e881c7c17c55d62f.yaml b/releasenotes/notes/migrate-identity-provider-to-sdk-e881c7c17c55d62f.yaml new file mode 100644 index 0000000000..792bef4a52 --- /dev/null +++ b/releasenotes/notes/migrate-identity-provider-to-sdk-e881c7c17c55d62f.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Migrate ``identity provider`` commands from keystoneclient to SDK. +upgrade: + - | + Filtering in ``identity provider`` commands is now case sensitive. diff --git a/releasenotes/notes/migrate-limit-to-sdk-378037ec2b79e302.yaml b/releasenotes/notes/migrate-limit-to-sdk-378037ec2b79e302.yaml new file mode 100644 index 0000000000..d4a57329ef --- /dev/null +++ b/releasenotes/notes/migrate-limit-to-sdk-378037ec2b79e302.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + Migrate ``limit`` commands from keystoneclient to SDK. +upgrade: + - | + Filtering in ``limit`` commands is now case sensitive. + - | + Specifying ``--region None`` is no longer supported for ``limit`` commands. diff --git a/releasenotes/notes/migrate-mapping-to-sdk-fb495993225fbdfc.yaml b/releasenotes/notes/migrate-mapping-to-sdk-fb495993225fbdfc.yaml new file mode 100644 index 0000000000..2972f87789 --- /dev/null +++ b/releasenotes/notes/migrate-mapping-to-sdk-fb495993225fbdfc.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Migrate ``mapping`` commands from keystoneclient to SDK. +upgrade: + - | + Filtering in ``mapping`` commands is now case sensitive. diff --git a/releasenotes/notes/migrate-policy-to-sdk-f794272fd21e1de4.yaml b/releasenotes/notes/migrate-policy-to-sdk-f794272fd21e1de4.yaml new file mode 100644 index 0000000000..dc3c98cc0f --- /dev/null +++ b/releasenotes/notes/migrate-policy-to-sdk-f794272fd21e1de4.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Migrate ``policy`` commands from keystoneclient to SDK. +upgrade: + - | + Filtering in ``policy`` commands is now case sensitive. diff --git a/releasenotes/notes/migrate-project-to-sdk-9201efd2804371de.yaml b/releasenotes/notes/migrate-project-to-sdk-9201efd2804371de.yaml new file mode 100644 index 0000000000..90c6031742 --- /dev/null +++ b/releasenotes/notes/migrate-project-to-sdk-9201efd2804371de.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + Migrate ``project`` commands from keystoneclient to SDK. +upgrade: + - | + Filtering in ``project`` commands is now case sensitive. diff --git a/releasenotes/notes/migrate-registered-limit-to-sdk-36b6451e3a799a43.yaml b/releasenotes/notes/migrate-registered-limit-to-sdk-36b6451e3a799a43.yaml new file mode 100644 index 0000000000..77f404a9ff --- /dev/null +++ b/releasenotes/notes/migrate-registered-limit-to-sdk-36b6451e3a799a43.yaml @@ -0,0 +1,10 @@ +--- +features: + - | + Migrate ``registered limit`` commands from keystoneclient to SDK. +upgrade: + - | + Filtering in ``registered limit`` commands is now case sensitive. + - | + Specifying ``--region None`` is no longer supported for ``registered limit`` + commands. diff --git a/releasenotes/notes/move-fwaas-from-neutronclient-a1b2c3d4e5f6g7h8.yaml b/releasenotes/notes/move-fwaas-from-neutronclient-a1b2c3d4e5f6g7h8.yaml new file mode 100644 index 0000000000..93da234d0f --- /dev/null +++ b/releasenotes/notes/move-fwaas-from-neutronclient-a1b2c3d4e5f6g7h8.yaml @@ -0,0 +1,27 @@ +--- +features: + - | + Firewall as a Service (FWaaS) v2 commands have been migrated from + ``python-neutronclient`` to ``python-openstackclient``. The following + commands are now available natively in OSC: + + * ``firewall group create`` + * ``firewall group delete`` + * ``firewall group list`` + * ``firewall group set`` + * ``firewall group show`` + * ``firewall group unset`` + * ``firewall group policy create`` + * ``firewall group policy delete`` + * ``firewall group policy add rule`` + * ``firewall group policy remove rule`` + * ``firewall group policy list`` + * ``firewall group policy set`` + * ``firewall group policy show`` + * ``firewall group policy unset`` + * ``firewall group rule create`` + * ``firewall group rule delete`` + * ``firewall group rule list`` + * ``firewall group rule set`` + * ``firewall group rule show`` + * ``firewall group rule unset`` diff --git a/releasenotes/notes/port-unset-device-id-and-owner-9fce242155c82992.yaml b/releasenotes/notes/port-unset-device-id-and-owner-9fce242155c82992.yaml new file mode 100644 index 0000000000..e14baacdd3 --- /dev/null +++ b/releasenotes/notes/port-unset-device-id-and-owner-9fce242155c82992.yaml @@ -0,0 +1,5 @@ +--- +features: + - | + Added ``--device`` and ``--device-owner`` parameter to the + ``port unset`` command. diff --git a/releasenotes/notes/quota-set-not-fail-on-missing-endpoint-7ae3bafeba5d663c.yaml b/releasenotes/notes/quota-set-not-fail-on-missing-endpoint-7ae3bafeba5d663c.yaml new file mode 100644 index 0000000000..8039b1e664 --- /dev/null +++ b/releasenotes/notes/quota-set-not-fail-on-missing-endpoint-7ae3bafeba5d663c.yaml @@ -0,0 +1,9 @@ +--- +fixes: + - | + When attempting to set or delete a quota for nova the command can fail if + the endpoint for the service does not exist but the service does. An + endpoint is a service at a specific region for a specific interface. The + other quota commands gracefully handle this leading it not being possible + to set or delete a quota in this case but to be able to see it. + [`Bug 2150251 `_] diff --git a/releasenotes/notes/remove-nova-network-ab5749694b3ba9e7.yaml b/releasenotes/notes/remove-nova-network-ab5749694b3ba9e7.yaml new file mode 100644 index 0000000000..b2607fb931 --- /dev/null +++ b/releasenotes/notes/remove-nova-network-ab5749694b3ba9e7.yaml @@ -0,0 +1,8 @@ +--- +upgrade: + - | + Nova network support has been removed from the networking commands. + As Neutron has been the preferred networking solution in + Openstack for close to ten years, the old Nova network + code was finally removed. All networking API calls are still + supported, but they will only use the Neutron API endpoint. diff --git a/releasenotes/notes/use-project-domain-for-parent-cb29ee3f5adeb647.yaml b/releasenotes/notes/use-project-domain-for-parent-cb29ee3f5adeb647.yaml new file mode 100644 index 0000000000..acbf2ba6cf --- /dev/null +++ b/releasenotes/notes/use-project-domain-for-parent-cb29ee3f5adeb647.yaml @@ -0,0 +1,5 @@ +--- +upgrade: + - | + When creating or listing projects, parent project will now be searched from + the same domain as the child project. diff --git a/releasenotes/notes/volume-delete-cascade-384003efc8896096.yaml b/releasenotes/notes/volume-delete-cascade-384003efc8896096.yaml new file mode 100644 index 0000000000..679964f1bc --- /dev/null +++ b/releasenotes/notes/volume-delete-cascade-384003efc8896096.yaml @@ -0,0 +1,6 @@ +--- +upgrade: + - | + The ``--purge`` argument to the ``volume delete`` command has been renamed + to ``--cascade`` to better match the Cinder API and the meaning of what + this argument does. An alias is provided for backwards compatibility. diff --git a/releasenotes/notes/volume-service-set-fix-345a8bc84267f743.yaml b/releasenotes/notes/volume-service-set-fix-345a8bc84267f743.yaml new file mode 100644 index 0000000000..e21e12dbbc --- /dev/null +++ b/releasenotes/notes/volume-service-set-fix-345a8bc84267f743.yaml @@ -0,0 +1,5 @@ +--- +fixes: + - | + The 'volume service set' command could not work due to a bad API call. + [Bug `2116969 `_] diff --git a/releasenotes/source/2024.1.rst b/releasenotes/source/2024.1.rst index 4977a4f1a0..6896656be6 100644 --- a/releasenotes/source/2024.1.rst +++ b/releasenotes/source/2024.1.rst @@ -3,4 +3,4 @@ =========================== .. release-notes:: - :branch: stable/2024.1 + :branch: unmaintained/2024.1 diff --git a/releasenotes/source/2025.2.rst b/releasenotes/source/2025.2.rst new file mode 100644 index 0000000000..4dae18d869 --- /dev/null +++ b/releasenotes/source/2025.2.rst @@ -0,0 +1,6 @@ +=========================== +2025.2 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2025.2 diff --git a/releasenotes/source/2026.1.rst b/releasenotes/source/2026.1.rst new file mode 100644 index 0000000000..3d28615808 --- /dev/null +++ b/releasenotes/source/2026.1.rst @@ -0,0 +1,6 @@ +=========================== +2026.1 Series Release Notes +=========================== + +.. release-notes:: + :branch: stable/2026.1 diff --git a/releasenotes/source/index.rst b/releasenotes/source/index.rst index a9c057978d..2b9b35f7bc 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,8 @@ OpenStackClient Release Notes :maxdepth: 1 unreleased + 2026.1 + 2025.2 2025.1 2024.2 2024.1 diff --git a/requirements.txt b/requirements.txt index 5e9b33d275..3a5bbccfc1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,15 +2,13 @@ # date but we do not test them so no guarantee of having them all correct. If # you find any incorrect lower bounds, let us know or propose a fix. -pbr!=2.1.0,>=2.0.0 # Apache-2.0 - cryptography>=2.7 # BSD/Apache-2.0 -cliff>=3.5.0 # Apache-2.0 +cliff>=4.13.0 # Apache-2.0 iso8601>=0.1.11 # MIT -openstacksdk>=4.5.0 # Apache-2.0 -osc-lib>=2.3.0 # Apache-2.0 +openstacksdk>=4.16.0 # Apache-2.0 +osc-lib>=4.6.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 python-keystoneclient>=3.22.0 # Apache-2.0 -python-cinderclient>=3.3.0 # Apache-2.0 +python-manilaclient>=6.0.0 # Apache-2.0 requests>=2.27.0 # Apache-2.0 stevedore>=2.0.1 # Apache-2.0 diff --git a/setup.cfg b/setup.cfg index cd3a107e48..28a5b7809f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,821 +1,2 @@ [metadata] name = python-openstackclient -summary = OpenStack Command-line Client -description_file = - README.rst -description_content_type = text/x-rst -author = OpenStack -author_email = openstack-discuss@lists.openstack.org -home_page = https://docs.openstack.org/python-openstackclient/latest/ -python_requires = >=3.10 -classifier = - Environment :: OpenStack - Intended Audience :: Information Technology - Intended Audience :: System Administrators - License :: OSI Approved :: Apache Software License - Operating System :: POSIX :: Linux - Programming Language :: Python - Programming Language :: Python :: 3 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - Programming Language :: Python :: 3.12 - Programming Language :: Python :: 3.13 - -[files] -packages = - openstackclient - -[entry_points] -console_scripts = - openstack = openstackclient.shell:main - -openstack.cli = - command_list = openstackclient.common.module:ListCommand - module_list = openstackclient.common.module:ListModule - -openstack.cli.base = - compute = openstackclient.compute.client - identity = openstackclient.identity.client - image = openstackclient.image.client - network = openstackclient.network.client - object_store = openstackclient.object.client - volume = openstackclient.volume.client - -openstack.common = - availability_zone_list = openstackclient.common.availability_zone:ListAvailabilityZone - configuration_show = openstackclient.common.configuration:ShowConfiguration - extension_list = openstackclient.common.extension:ListExtension - extension_show = openstackclient.common.extension:ShowExtension - limits_show = openstackclient.common.limits:ShowLimits - project_cleanup = openstackclient.common.project_cleanup:ProjectCleanup - quota_list = openstackclient.common.quota:ListQuota - quota_set = openstackclient.common.quota:SetQuota - quota_show = openstackclient.common.quota:ShowQuota - quota_delete = openstackclient.common.quota:DeleteQuota - versions_show = openstackclient.common.versions:ShowVersions - -openstack.compute.v2 = - compute_agent_create = openstackclient.compute.v2.agent:CreateAgent - compute_agent_delete = openstackclient.compute.v2.agent:DeleteAgent - compute_agent_list = openstackclient.compute.v2.agent:ListAgent - compute_agent_set = openstackclient.compute.v2.agent:SetAgent - - aggregate_add_host = openstackclient.compute.v2.aggregate:AddAggregateHost - aggregate_create = openstackclient.compute.v2.aggregate:CreateAggregate - aggregate_delete = openstackclient.compute.v2.aggregate:DeleteAggregate - aggregate_list = openstackclient.compute.v2.aggregate:ListAggregate - aggregate_remove_host = openstackclient.compute.v2.aggregate:RemoveAggregateHost - aggregate_set = openstackclient.compute.v2.aggregate:SetAggregate - aggregate_show = openstackclient.compute.v2.aggregate:ShowAggregate - aggregate_unset = openstackclient.compute.v2.aggregate:UnsetAggregate - aggregate_cache_image = openstackclient.compute.v2.aggregate:CacheImageForAggregate - - compute_service_delete = openstackclient.compute.v2.service:DeleteService - compute_service_list = openstackclient.compute.v2.service:ListService - compute_service_set = openstackclient.compute.v2.service:SetService - - console_log_show = openstackclient.compute.v2.console:ShowConsoleLog - console_url_show = openstackclient.compute.v2.console:ShowConsoleURL - - console_connection_show = openstackclient.compute.v2.console_connection:ShowConsoleConnectionInformation - - flavor_create = openstackclient.compute.v2.flavor:CreateFlavor - flavor_delete = openstackclient.compute.v2.flavor:DeleteFlavor - flavor_list = openstackclient.compute.v2.flavor:ListFlavor - flavor_show = openstackclient.compute.v2.flavor:ShowFlavor - flavor_set = openstackclient.compute.v2.flavor:SetFlavor - flavor_unset = openstackclient.compute.v2.flavor:UnsetFlavor - - host_list = openstackclient.compute.v2.host:ListHost - host_set = openstackclient.compute.v2.host:SetHost - host_show = openstackclient.compute.v2.host:ShowHost - - hypervisor_list = openstackclient.compute.v2.hypervisor:ListHypervisor - hypervisor_show = openstackclient.compute.v2.hypervisor:ShowHypervisor - - hypervisor_stats_show = openstackclient.compute.v2.hypervisor_stats:ShowHypervisorStats - - keypair_create = openstackclient.compute.v2.keypair:CreateKeypair - keypair_delete = openstackclient.compute.v2.keypair:DeleteKeypair - keypair_list = openstackclient.compute.v2.keypair:ListKeypair - keypair_show = openstackclient.compute.v2.keypair:ShowKeypair - - server_add_fixed_ip = openstackclient.compute.v2.server:AddFixedIP - server_add_floating_ip = openstackclient.compute.v2.server:AddFloatingIP - server_add_port = openstackclient.compute.v2.server:AddPort - server_add_network = openstackclient.compute.v2.server:AddNetwork - server_add_security_group = openstackclient.compute.v2.server:AddServerSecurityGroup - server_add_volume = openstackclient.compute.v2.server:AddServerVolume - server_create = openstackclient.compute.v2.server:CreateServer - server_delete = openstackclient.compute.v2.server:DeleteServer - server_dump_create = openstackclient.compute.v2.server:CreateServerDump - server_evacuate = openstackclient.compute.v2.server:EvacuateServer - server_list = openstackclient.compute.v2.server:ListServer - server_lock = openstackclient.compute.v2.server:LockServer - server_migrate = openstackclient.compute.v2.server:MigrateServer - server_migrate_confirm = openstackclient.compute.v2.server:MigrateConfirm - server_migrate_revert = openstackclient.compute.v2.server:MigrateRevert - server_migration_confirm = openstackclient.compute.v2.server:ConfirmMigration - server_migration_revert = openstackclient.compute.v2.server:RevertMigration - server_pause = openstackclient.compute.v2.server:PauseServer - server_reboot = openstackclient.compute.v2.server:RebootServer - server_rebuild = openstackclient.compute.v2.server:RebuildServer - server_remove_fixed_ip = openstackclient.compute.v2.server:RemoveFixedIP - server_remove_floating_ip = openstackclient.compute.v2.server:RemoveFloatingIP - server_remove_port = openstackclient.compute.v2.server:RemovePort - server_remove_network = openstackclient.compute.v2.server:RemoveNetwork - server_remove_security_group = openstackclient.compute.v2.server:RemoveServerSecurityGroup - server_remove_volume = openstackclient.compute.v2.server:RemoveServerVolume - server_rescue = openstackclient.compute.v2.server:RescueServer - server_resize = openstackclient.compute.v2.server:ResizeServer - server_resize_confirm = openstackclient.compute.v2.server:ResizeConfirm - server_resize_revert = openstackclient.compute.v2.server:ResizeRevert - server_restore = openstackclient.compute.v2.server:RestoreServer - server_resume = openstackclient.compute.v2.server:ResumeServer - server_set = openstackclient.compute.v2.server:SetServer - server_shelve = openstackclient.compute.v2.server:ShelveServer - server_show = openstackclient.compute.v2.server:ShowServer - server_ssh = openstackclient.compute.v2.server:SshServer - server_start = openstackclient.compute.v2.server:StartServer - server_stop = openstackclient.compute.v2.server:StopServer - server_suspend = openstackclient.compute.v2.server:SuspendServer - server_unlock = openstackclient.compute.v2.server:UnlockServer - server_unpause = openstackclient.compute.v2.server:UnpauseServer - server_unrescue = openstackclient.compute.v2.server:UnrescueServer - server_unset = openstackclient.compute.v2.server:UnsetServer - server_unshelve = openstackclient.compute.v2.server:UnshelveServer - - server_backup_create = openstackclient.compute.v2.server_backup:CreateServerBackup - - server_event_list = openstackclient.compute.v2.server_event:ListServerEvent - server_event_show = openstackclient.compute.v2.server_event:ShowServerEvent - - server_group_create = openstackclient.compute.v2.server_group:CreateServerGroup - server_group_delete = openstackclient.compute.v2.server_group:DeleteServerGroup - server_group_list = openstackclient.compute.v2.server_group:ListServerGroup - server_group_show = openstackclient.compute.v2.server_group:ShowServerGroup - - server_image_create = openstackclient.compute.v2.server_image:CreateServerImage - - server_migration_abort = openstackclient.compute.v2.server_migration:AbortMigration - server_migration_force_complete = openstackclient.compute.v2.server_migration:ForceCompleteMigration - server_migration_list = openstackclient.compute.v2.server_migration:ListMigration - server_migration_show = openstackclient.compute.v2.server_migration:ShowMigration - - server_volume_list = openstackclient.compute.v2.server_volume:ListServerVolume - server_volume_set = openstackclient.compute.v2.server_volume:SetServerVolume - server_volume_update = openstackclient.compute.v2.server_volume:UpdateServerVolume - - usage_list = openstackclient.compute.v2.usage:ListUsage - usage_show = openstackclient.compute.v2.usage:ShowUsage - -openstack.identity.v2 = - catalog_list = openstackclient.identity.v2_0.catalog:ListCatalog - catalog_show = openstackclient.identity.v2_0.catalog:ShowCatalog - - ec2_credentials_create = openstackclient.identity.v2_0.ec2creds:CreateEC2Creds - ec2_credentials_delete = openstackclient.identity.v2_0.ec2creds:DeleteEC2Creds - ec2_credentials_list = openstackclient.identity.v2_0.ec2creds:ListEC2Creds - ec2_credentials_show = openstackclient.identity.v2_0.ec2creds:ShowEC2Creds - - endpoint_create = openstackclient.identity.v2_0.endpoint:CreateEndpoint - endpoint_delete = openstackclient.identity.v2_0.endpoint:DeleteEndpoint - endpoint_list = openstackclient.identity.v2_0.endpoint:ListEndpoint - endpoint_show = openstackclient.identity.v2_0.endpoint:ShowEndpoint - - project_create = openstackclient.identity.v2_0.project:CreateProject - project_delete = openstackclient.identity.v2_0.project:DeleteProject - project_list = openstackclient.identity.v2_0.project:ListProject - project_set = openstackclient.identity.v2_0.project:SetProject - project_show = openstackclient.identity.v2_0.project:ShowProject - project_unset = openstackclient.identity.v2_0.project:UnsetProject - - role_add = openstackclient.identity.v2_0.role:AddRole - role_create = openstackclient.identity.v2_0.role:CreateRole - role_delete = openstackclient.identity.v2_0.role:DeleteRole - role_list = openstackclient.identity.v2_0.role:ListRole - role_remove = openstackclient.identity.v2_0.role:RemoveRole - role_show = openstackclient.identity.v2_0.role:ShowRole - role_assignment_list = openstackclient.identity.v2_0.role_assignment:ListRoleAssignment - - service_create = openstackclient.identity.v2_0.service:CreateService - service_delete = openstackclient.identity.v2_0.service:DeleteService - service_list = openstackclient.identity.v2_0.service:ListService - service_show = openstackclient.identity.v2_0.service:ShowService - - token_issue = openstackclient.identity.v2_0.token:IssueToken - token_revoke = openstackclient.identity.v2_0.token:RevokeToken - - user_create = openstackclient.identity.v2_0.user:CreateUser - user_delete = openstackclient.identity.v2_0.user:DeleteUser - user_list = openstackclient.identity.v2_0.user:ListUser - user_set = openstackclient.identity.v2_0.user:SetUser - user_show = openstackclient.identity.v2_0.user:ShowUser - -openstack.identity.v3 = - access_token_create = openstackclient.identity.v3.token:CreateAccessToken - - access_rule_delete = openstackclient.identity.v3.access_rule:DeleteAccessRule - access_rule_list = openstackclient.identity.v3.access_rule:ListAccessRule - access_rule_show = openstackclient.identity.v3.access_rule:ShowAccessRule - - application_credential_create = openstackclient.identity.v3.application_credential:CreateApplicationCredential - application_credential_delete = openstackclient.identity.v3.application_credential:DeleteApplicationCredential - application_credential_list = openstackclient.identity.v3.application_credential:ListApplicationCredential - application_credential_show = openstackclient.identity.v3.application_credential:ShowApplicationCredential - - catalog_list = openstackclient.identity.v3.catalog:ListCatalog - catalog_show = openstackclient.identity.v3.catalog:ShowCatalog - - consumer_create = openstackclient.identity.v3.consumer:CreateConsumer - consumer_delete = openstackclient.identity.v3.consumer:DeleteConsumer - consumer_list = openstackclient.identity.v3.consumer:ListConsumer - consumer_set = openstackclient.identity.v3.consumer:SetConsumer - consumer_show = openstackclient.identity.v3.consumer:ShowConsumer - - credential_create = openstackclient.identity.v3.credential:CreateCredential - credential_delete = openstackclient.identity.v3.credential:DeleteCredential - credential_list = openstackclient.identity.v3.credential:ListCredential - credential_set = openstackclient.identity.v3.credential:SetCredential - credential_show = openstackclient.identity.v3.credential:ShowCredential - - domain_create = openstackclient.identity.v3.domain:CreateDomain - domain_delete = openstackclient.identity.v3.domain:DeleteDomain - domain_list = openstackclient.identity.v3.domain:ListDomain - domain_set = openstackclient.identity.v3.domain:SetDomain - domain_show = openstackclient.identity.v3.domain:ShowDomain - - ec2_credentials_create = openstackclient.identity.v3.ec2creds:CreateEC2Creds - ec2_credentials_delete = openstackclient.identity.v3.ec2creds:DeleteEC2Creds - ec2_credentials_list = openstackclient.identity.v3.ec2creds:ListEC2Creds - ec2_credentials_show = openstackclient.identity.v3.ec2creds:ShowEC2Creds - - endpoint_add_project = openstackclient.identity.v3.endpoint:AddProjectToEndpoint - endpoint_create = openstackclient.identity.v3.endpoint:CreateEndpoint - endpoint_delete = openstackclient.identity.v3.endpoint:DeleteEndpoint - endpoint_list = openstackclient.identity.v3.endpoint:ListEndpoint - endpoint_remove_project = openstackclient.identity.v3.endpoint:RemoveProjectFromEndpoint - endpoint_set = openstackclient.identity.v3.endpoint:SetEndpoint - endpoint_show = openstackclient.identity.v3.endpoint:ShowEndpoint - - endpoint_group_add_project = openstackclient.identity.v3.endpoint_group:AddProjectToEndpointGroup - endpoint_group_create = openstackclient.identity.v3.endpoint_group:CreateEndpointGroup - endpoint_group_delete = openstackclient.identity.v3.endpoint_group:DeleteEndpointGroup - endpoint_group_list = openstackclient.identity.v3.endpoint_group:ListEndpointGroup - endpoint_group_remove_project = openstackclient.identity.v3.endpoint_group:RemoveProjectFromEndpointGroup - endpoint_group_set = openstackclient.identity.v3.endpoint_group:SetEndpointGroup - endpoint_group_show = openstackclient.identity.v3.endpoint_group:ShowEndpointGroup - - federation_domain_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleDomains - federation_project_list = openstackclient.identity.v3.unscoped_saml:ListAccessibleProjects - - federation_protocol_create = openstackclient.identity.v3.federation_protocol:CreateProtocol - federation_protocol_delete = openstackclient.identity.v3.federation_protocol:DeleteProtocol - federation_protocol_list = openstackclient.identity.v3.federation_protocol:ListProtocols - federation_protocol_set = openstackclient.identity.v3.federation_protocol:SetProtocol - federation_protocol_show = openstackclient.identity.v3.federation_protocol:ShowProtocol - - group_add_user = openstackclient.identity.v3.group:AddUserToGroup - group_contains_user = openstackclient.identity.v3.group:CheckUserInGroup - group_create = openstackclient.identity.v3.group:CreateGroup - group_delete = openstackclient.identity.v3.group:DeleteGroup - group_list = openstackclient.identity.v3.group:ListGroup - group_remove_user = openstackclient.identity.v3.group:RemoveUserFromGroup - group_set = openstackclient.identity.v3.group:SetGroup - group_show = openstackclient.identity.v3.group:ShowGroup - - identity_provider_create = openstackclient.identity.v3.identity_provider:CreateIdentityProvider - identity_provider_delete = openstackclient.identity.v3.identity_provider:DeleteIdentityProvider - identity_provider_list = openstackclient.identity.v3.identity_provider:ListIdentityProvider - identity_provider_set = openstackclient.identity.v3.identity_provider:SetIdentityProvider - identity_provider_show = openstackclient.identity.v3.identity_provider:ShowIdentityProvider - - implied_role_create = openstackclient.identity.v3.implied_role:CreateImpliedRole - implied_role_delete = openstackclient.identity.v3.implied_role:DeleteImpliedRole - implied_role_list = openstackclient.identity.v3.implied_role:ListImpliedRole - - limit_create = openstackclient.identity.v3.limit:CreateLimit - limit_delete = openstackclient.identity.v3.limit:DeleteLimit - limit_list = openstackclient.identity.v3.limit:ListLimit - limit_set = openstackclient.identity.v3.limit:SetLimit - limit_show = openstackclient.identity.v3.limit:ShowLimit - - mapping_create = openstackclient.identity.v3.mapping:CreateMapping - mapping_delete = openstackclient.identity.v3.mapping:DeleteMapping - mapping_list = openstackclient.identity.v3.mapping:ListMapping - mapping_set = openstackclient.identity.v3.mapping:SetMapping - mapping_show = openstackclient.identity.v3.mapping:ShowMapping - - policy_create = openstackclient.identity.v3.policy:CreatePolicy - policy_delete = openstackclient.identity.v3.policy:DeletePolicy - policy_list = openstackclient.identity.v3.policy:ListPolicy - policy_set = openstackclient.identity.v3.policy:SetPolicy - policy_show = openstackclient.identity.v3.policy:ShowPolicy - - project_create = openstackclient.identity.v3.project:CreateProject - project_delete = openstackclient.identity.v3.project:DeleteProject - project_list = openstackclient.identity.v3.project:ListProject - project_set = openstackclient.identity.v3.project:SetProject - project_show = openstackclient.identity.v3.project:ShowProject - - region_create = openstackclient.identity.v3.region:CreateRegion - region_delete = openstackclient.identity.v3.region:DeleteRegion - region_list = openstackclient.identity.v3.region:ListRegion - region_set = openstackclient.identity.v3.region:SetRegion - region_show = openstackclient.identity.v3.region:ShowRegion - - registered_limit_create = openstackclient.identity.v3.registered_limit:CreateRegisteredLimit - registered_limit_delete = openstackclient.identity.v3.registered_limit:DeleteRegisteredLimit - registered_limit_list = openstackclient.identity.v3.registered_limit:ListRegisteredLimit - registered_limit_set = openstackclient.identity.v3.registered_limit:SetRegisteredLimit - registered_limit_show = openstackclient.identity.v3.registered_limit:ShowRegisteredLimit - - request_token_authorize = openstackclient.identity.v3.token:AuthorizeRequestToken - request_token_create = openstackclient.identity.v3.token:CreateRequestToken - - role_add = openstackclient.identity.v3.role:AddRole - role_create = openstackclient.identity.v3.role:CreateRole - role_delete = openstackclient.identity.v3.role:DeleteRole - role_list = openstackclient.identity.v3.role:ListRole - role_remove = openstackclient.identity.v3.role:RemoveRole - role_show = openstackclient.identity.v3.role:ShowRole - role_set = openstackclient.identity.v3.role:SetRole - role_assignment_list = openstackclient.identity.v3.role_assignment:ListRoleAssignment - - service_create = openstackclient.identity.v3.service:CreateService - service_delete = openstackclient.identity.v3.service:DeleteService - service_list = openstackclient.identity.v3.service:ListService - service_show = openstackclient.identity.v3.service:ShowService - service_set = openstackclient.identity.v3.service:SetService - - service_provider_create = openstackclient.identity.v3.service_provider:CreateServiceProvider - service_provider_delete = openstackclient.identity.v3.service_provider:DeleteServiceProvider - service_provider_list = openstackclient.identity.v3.service_provider:ListServiceProvider - service_provider_set = openstackclient.identity.v3.service_provider:SetServiceProvider - service_provider_show = openstackclient.identity.v3.service_provider:ShowServiceProvider - - token_issue = openstackclient.identity.v3.token:IssueToken - token_revoke = openstackclient.identity.v3.token:RevokeToken - - trust_create = openstackclient.identity.v3.trust:CreateTrust - trust_delete = openstackclient.identity.v3.trust:DeleteTrust - trust_list = openstackclient.identity.v3.trust:ListTrust - trust_show = openstackclient.identity.v3.trust:ShowTrust - - user_create = openstackclient.identity.v3.user:CreateUser - user_delete = openstackclient.identity.v3.user:DeleteUser - user_list = openstackclient.identity.v3.user:ListUser - user_set = openstackclient.identity.v3.user:SetUser - user_password_set = openstackclient.identity.v3.user:SetPasswordUser - user_show = openstackclient.identity.v3.user:ShowUser - -openstack.image.v1 = - image_create = openstackclient.image.v1.image:CreateImage - image_delete = openstackclient.image.v1.image:DeleteImage - image_list = openstackclient.image.v1.image:ListImage - image_save = openstackclient.image.v1.image:SaveImage - image_set = openstackclient.image.v1.image:SetImage - image_show = openstackclient.image.v1.image:ShowImage - -openstack.image.v2 = - image_add_project = openstackclient.image.v2.image:AddProjectToImage - image_create = openstackclient.image.v2.image:CreateImage - image_delete = openstackclient.image.v2.image:DeleteImage - image_list = openstackclient.image.v2.image:ListImage - image_member_list = openstackclient.image.v2.image:ListImageProjects - image_remove_project = openstackclient.image.v2.image:RemoveProjectImage - image_member_get = openstackclient.image.v2.image:ShowProjectImage - image_save = openstackclient.image.v2.image:SaveImage - image_show = openstackclient.image.v2.image:ShowImage - image_set = openstackclient.image.v2.image:SetImage - image_unset = openstackclient.image.v2.image:UnsetImage - image_stage = openstackclient.image.v2.image:StageImage - image_task_show = openstackclient.image.v2.task:ShowTask - image_task_list = openstackclient.image.v2.task:ListTask - image_import_info = openstackclient.image.v2.info:ImportInfo - image_import = openstackclient.image.v2.image:ImportImage - image_stores_list = openstackclient.image.v2.image:StoresInfo - - image_metadef_namespace_create = openstackclient.image.v2.metadef_namespaces:CreateMetadefNamespace - image_metadef_namespace_delete = openstackclient.image.v2.metadef_namespaces:DeleteMetadefNamespace - image_metadef_namespace_list = openstackclient.image.v2.metadef_namespaces:ListMetadefNamespace - image_metadef_namespace_set = openstackclient.image.v2.metadef_namespaces:SetMetadefNamespace - image_metadef_namespace_show = openstackclient.image.v2.metadef_namespaces:ShowMetadefNamespace - - image_metadef_object_create = openstackclient.image.v2.metadef_objects:CreateMetadefObjects - image_metadef_object_show = openstackclient.image.v2.metadef_objects:ShowMetadefObjects - image_metadef_object_list = openstackclient.image.v2.metadef_objects:ListMetadefObjects - image_metadef_object_delete = openstackclient.image.v2.metadef_objects:DeleteMetadefObject - image_metadef_object_update = openstackclient.image.v2.metadef_objects:SetMetadefObject - image_metadef_object_property_show = openstackclient.image.v2.metadef_objects:ShowMetadefObjectProperty - - image_metadef_property_create = openstackclient.image.v2.metadef_properties:CreateMetadefProperty - image_metadef_property_delete = openstackclient.image.v2.metadef_properties:DeleteMetadefProperty - image_metadef_property_list = openstackclient.image.v2.metadef_properties:ListMetadefProperties - image_metadef_property_set = openstackclient.image.v2.metadef_properties:SetMetadefProperty - image_metadef_property_show = openstackclient.image.v2.metadef_properties:ShowMetadefProperty - - image_metadef_resource_type_list = openstackclient.image.v2.metadef_resource_types:ListMetadefResourceTypes - image_metadef_resource_type_association_create = openstackclient.image.v2.metadef_resource_type_association:CreateMetadefResourceTypeAssociation - image_metadef_resource_type_association_delete = openstackclient.image.v2.metadef_resource_type_association:DeleteMetadefResourceTypeAssociation - image_metadef_resource_type_association_list = openstackclient.image.v2.metadef_resource_type_association:ListMetadefResourceTypeAssociations - - cached_image_list = openstackclient.image.v2.cache:ListCachedImage - cached_image_queue = openstackclient.image.v2.cache:QueueCachedImage - cached_image_delete = openstackclient.image.v2.cache:DeleteCachedImage - cached_image_clear = openstackclient.image.v2.cache:ClearCachedImage - -openstack.network.v2 = - address_group_create = openstackclient.network.v2.address_group:CreateAddressGroup - address_group_delete = openstackclient.network.v2.address_group:DeleteAddressGroup - address_group_list = openstackclient.network.v2.address_group:ListAddressGroup - address_group_set = openstackclient.network.v2.address_group:SetAddressGroup - address_group_show = openstackclient.network.v2.address_group:ShowAddressGroup - address_group_unset = openstackclient.network.v2.address_group:UnsetAddressGroup - - address_scope_create = openstackclient.network.v2.address_scope:CreateAddressScope - address_scope_delete = openstackclient.network.v2.address_scope:DeleteAddressScope - address_scope_list = openstackclient.network.v2.address_scope:ListAddressScope - address_scope_set = openstackclient.network.v2.address_scope:SetAddressScope - address_scope_show = openstackclient.network.v2.address_scope:ShowAddressScope - - floating_ip_create = openstackclient.network.v2.floating_ip:CreateFloatingIP - floating_ip_delete = openstackclient.network.v2.floating_ip:DeleteFloatingIP - floating_ip_list = openstackclient.network.v2.floating_ip:ListFloatingIP - floating_ip_set = openstackclient.network.v2.floating_ip:SetFloatingIP - floating_ip_show = openstackclient.network.v2.floating_ip:ShowFloatingIP - floating_ip_unset = openstackclient.network.v2.floating_ip:UnsetFloatingIP - - floating_ip_pool_list = openstackclient.network.v2.floating_ip_pool:ListFloatingIPPool - - floating_ip_port_forwarding_create = openstackclient.network.v2.floating_ip_port_forwarding:CreateFloatingIPPortForwarding - floating_ip_port_forwarding_delete = openstackclient.network.v2.floating_ip_port_forwarding:DeleteFloatingIPPortForwarding - floating_ip_port_forwarding_list = openstackclient.network.v2.floating_ip_port_forwarding:ListFloatingIPPortForwarding - floating_ip_port_forwarding_set = openstackclient.network.v2.floating_ip_port_forwarding:SetFloatingIPPortForwarding - floating_ip_port_forwarding_show = openstackclient.network.v2.floating_ip_port_forwarding:ShowFloatingIPPortForwarding - - ip_availability_list = openstackclient.network.v2.ip_availability:ListIPAvailability - ip_availability_show = openstackclient.network.v2.ip_availability:ShowIPAvailability - - local_ip_create = openstackclient.network.v2.local_ip:CreateLocalIP - local_ip_delete = openstackclient.network.v2.local_ip:DeleteLocalIP - local_ip_list = openstackclient.network.v2.local_ip:ListLocalIP - local_ip_set = openstackclient.network.v2.local_ip:SetLocalIP - local_ip_show = openstackclient.network.v2.local_ip:ShowLocalIP - - local_ip_association_create = openstackclient.network.v2.local_ip_association:CreateLocalIPAssociation - local_ip_association_delete = openstackclient.network.v2.local_ip_association:DeleteLocalIPAssociation - local_ip_association_list = openstackclient.network.v2.local_ip_association:ListLocalIPAssociation - - network_agent_add_network = openstackclient.network.v2.network_agent:AddNetworkToAgent - network_agent_add_router = openstackclient.network.v2.network_agent:AddRouterToAgent - network_agent_delete = openstackclient.network.v2.network_agent:DeleteNetworkAgent - network_agent_list = openstackclient.network.v2.network_agent:ListNetworkAgent - network_agent_remove_network = openstackclient.network.v2.network_agent:RemoveNetworkFromAgent - network_agent_remove_router = openstackclient.network.v2.network_agent:RemoveRouterFromAgent - network_agent_set = openstackclient.network.v2.network_agent:SetNetworkAgent - network_agent_show = openstackclient.network.v2.network_agent:ShowNetworkAgent - - network_auto_allocated_topology_create = openstackclient.network.v2.network_auto_allocated_topology:CreateAutoAllocatedTopology - network_auto_allocated_topology_delete = openstackclient.network.v2.network_auto_allocated_topology:DeleteAutoAllocatedTopology - - network_flavor_add_profile = openstackclient.network.v2.network_flavor:AddNetworkFlavorToProfile - network_flavor_create = openstackclient.network.v2.network_flavor:CreateNetworkFlavor - network_flavor_delete = openstackclient.network.v2.network_flavor:DeleteNetworkFlavor - network_flavor_list = openstackclient.network.v2.network_flavor:ListNetworkFlavor - network_flavor_remove_profile = openstackclient.network.v2.network_flavor:RemoveNetworkFlavorFromProfile - network_flavor_set = openstackclient.network.v2.network_flavor:SetNetworkFlavor - network_flavor_show = openstackclient.network.v2.network_flavor:ShowNetworkFlavor - - network_flavor_profile_create = openstackclient.network.v2.network_flavor_profile:CreateNetworkFlavorProfile - network_flavor_profile_delete = openstackclient.network.v2.network_flavor_profile:DeleteNetworkFlavorProfile - network_flavor_profile_list = openstackclient.network.v2.network_flavor_profile:ListNetworkFlavorProfile - network_flavor_profile_set = openstackclient.network.v2.network_flavor_profile:SetNetworkFlavorProfile - network_flavor_profile_show = openstackclient.network.v2.network_flavor_profile:ShowNetworkFlavorProfile - - network_create = openstackclient.network.v2.network:CreateNetwork - network_delete = openstackclient.network.v2.network:DeleteNetwork - network_list = openstackclient.network.v2.network:ListNetwork - network_set = openstackclient.network.v2.network:SetNetwork - network_show = openstackclient.network.v2.network:ShowNetwork - network_unset = openstackclient.network.v2.network:UnsetNetwork - - network_l3_conntrack_helper_create = openstackclient.network.v2.l3_conntrack_helper:CreateConntrackHelper - network_l3_conntrack_helper_delete = openstackclient.network.v2.l3_conntrack_helper:DeleteConntrackHelper - network_l3_conntrack_helper_list = openstackclient.network.v2.l3_conntrack_helper:ListConntrackHelper - network_l3_conntrack_helper_set = openstackclient.network.v2.l3_conntrack_helper:SetConntrackHelper - network_l3_conntrack_helper_show = openstackclient.network.v2.l3_conntrack_helper:ShowConntrackHelper - - network_meter_create = openstackclient.network.v2.network_meter:CreateMeter - network_meter_delete = openstackclient.network.v2.network_meter:DeleteMeter - network_meter_list = openstackclient.network.v2.network_meter:ListMeter - network_meter_show = openstackclient.network.v2.network_meter:ShowMeter - - network_meter_rule_create = openstackclient.network.v2.network_meter_rule:CreateMeterRule - network_meter_rule_delete = openstackclient.network.v2.network_meter_rule:DeleteMeterRule - network_meter_rule_list = openstackclient.network.v2.network_meter_rule:ListMeterRule - network_meter_rule_show = openstackclient.network.v2.network_meter_rule:ShowMeterRule - - network_qos_policy_create = openstackclient.network.v2.network_qos_policy:CreateNetworkQosPolicy - network_qos_policy_delete = openstackclient.network.v2.network_qos_policy:DeleteNetworkQosPolicy - network_qos_policy_list = openstackclient.network.v2.network_qos_policy:ListNetworkQosPolicy - network_qos_policy_set = openstackclient.network.v2.network_qos_policy:SetNetworkQosPolicy - network_qos_policy_show = openstackclient.network.v2.network_qos_policy:ShowNetworkQosPolicy - - network_qos_rule_create = openstackclient.network.v2.network_qos_rule:CreateNetworkQosRule - network_qos_rule_delete = openstackclient.network.v2.network_qos_rule:DeleteNetworkQosRule - network_qos_rule_list = openstackclient.network.v2.network_qos_rule:ListNetworkQosRule - network_qos_rule_set = openstackclient.network.v2.network_qos_rule:SetNetworkQosRule - network_qos_rule_show = openstackclient.network.v2.network_qos_rule:ShowNetworkQosRule - - network_qos_rule_type_list = openstackclient.network.v2.network_qos_rule_type:ListNetworkQosRuleType - network_qos_rule_type_show = openstackclient.network.v2.network_qos_rule_type:ShowNetworkQosRuleType - - network_rbac_create = openstackclient.network.v2.network_rbac:CreateNetworkRBAC - network_rbac_delete = openstackclient.network.v2.network_rbac:DeleteNetworkRBAC - network_rbac_list = openstackclient.network.v2.network_rbac:ListNetworkRBAC - network_rbac_set = openstackclient.network.v2.network_rbac:SetNetworkRBAC - network_rbac_show = openstackclient.network.v2.network_rbac:ShowNetworkRBAC - - network_segment_create = openstackclient.network.v2.network_segment:CreateNetworkSegment - network_segment_delete = openstackclient.network.v2.network_segment:DeleteNetworkSegment - network_segment_list = openstackclient.network.v2.network_segment:ListNetworkSegment - network_segment_set = openstackclient.network.v2.network_segment:SetNetworkSegment - network_segment_show = openstackclient.network.v2.network_segment:ShowNetworkSegment - - network_segment_range_create = openstackclient.network.v2.network_segment_range:CreateNetworkSegmentRange - network_segment_range_delete = openstackclient.network.v2.network_segment_range:DeleteNetworkSegmentRange - network_segment_range_list = openstackclient.network.v2.network_segment_range:ListNetworkSegmentRange - network_segment_range_set = openstackclient.network.v2.network_segment_range:SetNetworkSegmentRange - network_segment_range_show = openstackclient.network.v2.network_segment_range:ShowNetworkSegmentRange - - network_service_provider_list = openstackclient.network.v2.network_service_provider:ListNetworkServiceProvider - - network_subport_list = openstackclient.network.v2.network_trunk:ListNetworkSubport - network_trunk_create = openstackclient.network.v2.network_trunk:CreateNetworkTrunk - network_trunk_delete = openstackclient.network.v2.network_trunk:DeleteNetworkTrunk - network_trunk_list = openstackclient.network.v2.network_trunk:ListNetworkTrunk - network_trunk_set = openstackclient.network.v2.network_trunk:SetNetworkTrunk - network_trunk_show = openstackclient.network.v2.network_trunk:ShowNetworkTrunk - network_trunk_unset = openstackclient.network.v2.network_trunk:UnsetNetworkTrunk - - port_create = openstackclient.network.v2.port:CreatePort - port_delete = openstackclient.network.v2.port:DeletePort - port_list = openstackclient.network.v2.port:ListPort - port_set = openstackclient.network.v2.port:SetPort - port_show = openstackclient.network.v2.port:ShowPort - port_unset = openstackclient.network.v2.port:UnsetPort - - router_add_gateway = openstackclient.network.v2.router:AddGatewayToRouter - router_add_port = openstackclient.network.v2.router:AddPortToRouter - router_add_route = openstackclient.network.v2.router:AddExtraRoutesToRouter - router_add_subnet = openstackclient.network.v2.router:AddSubnetToRouter - router_create = openstackclient.network.v2.router:CreateRouter - router_delete = openstackclient.network.v2.router:DeleteRouter - router_list = openstackclient.network.v2.router:ListRouter - router_remove_gateway = openstackclient.network.v2.router:RemoveGatewayFromRouter - router_remove_port = openstackclient.network.v2.router:RemovePortFromRouter - router_remove_route = openstackclient.network.v2.router:RemoveExtraRoutesFromRouter - router_remove_subnet = openstackclient.network.v2.router:RemoveSubnetFromRouter - router_set = openstackclient.network.v2.router:SetRouter - router_show = openstackclient.network.v2.router:ShowRouter - router_unset = openstackclient.network.v2.router:UnsetRouter - - router_ndp_proxy_create = openstackclient.network.v2.ndp_proxy:CreateNDPProxy - router_ndp_proxy_delete = openstackclient.network.v2.ndp_proxy:DeleteNDPProxy - router_ndp_proxy_list = openstackclient.network.v2.ndp_proxy:ListNDPProxy - router_ndp_proxy_set = openstackclient.network.v2.ndp_proxy:SetNDPProxy - router_ndp_proxy_show = openstackclient.network.v2.ndp_proxy:ShowNDPProxy - - security_group_create = openstackclient.network.v2.security_group:CreateSecurityGroup - security_group_delete = openstackclient.network.v2.security_group:DeleteSecurityGroup - security_group_list = openstackclient.network.v2.security_group:ListSecurityGroup - security_group_set = openstackclient.network.v2.security_group:SetSecurityGroup - security_group_show = openstackclient.network.v2.security_group:ShowSecurityGroup - security_group_unset = openstackclient.network.v2.security_group:UnsetSecurityGroup - - security_group_rule_create = openstackclient.network.v2.security_group_rule:CreateSecurityGroupRule - security_group_rule_delete = openstackclient.network.v2.security_group_rule:DeleteSecurityGroupRule - security_group_rule_list = openstackclient.network.v2.security_group_rule:ListSecurityGroupRule - security_group_rule_show = openstackclient.network.v2.security_group_rule:ShowSecurityGroupRule - - default_security_group_rule_create = openstackclient.network.v2.default_security_group_rule:CreateDefaultSecurityGroupRule - default_security_group_rule_delete = openstackclient.network.v2.default_security_group_rule:DeleteDefaultSecurityGroupRule - default_security_group_rule_list = openstackclient.network.v2.default_security_group_rule:ListDefaultSecurityGroupRule - default_security_group_rule_show = openstackclient.network.v2.default_security_group_rule:ShowDefaultSecurityGroupRule - - subnet_create = openstackclient.network.v2.subnet:CreateSubnet - subnet_delete = openstackclient.network.v2.subnet:DeleteSubnet - subnet_list = openstackclient.network.v2.subnet:ListSubnet - subnet_set = openstackclient.network.v2.subnet:SetSubnet - subnet_show = openstackclient.network.v2.subnet:ShowSubnet - subnet_unset = openstackclient.network.v2.subnet:UnsetSubnet - - subnet_pool_create = openstackclient.network.v2.subnet_pool:CreateSubnetPool - subnet_pool_delete = openstackclient.network.v2.subnet_pool:DeleteSubnetPool - subnet_pool_list = openstackclient.network.v2.subnet_pool:ListSubnetPool - subnet_pool_set = openstackclient.network.v2.subnet_pool:SetSubnetPool - subnet_pool_show = openstackclient.network.v2.subnet_pool:ShowSubnetPool - subnet_pool_unset = openstackclient.network.v2.subnet_pool:UnsetSubnetPool - -openstack.object_store.v1 = - object_store_account_set = openstackclient.object.v1.account:SetAccount - object_store_account_show = openstackclient.object.v1.account:ShowAccount - object_store_account_unset = openstackclient.object.v1.account:UnsetAccount - container_create = openstackclient.object.v1.container:CreateContainer - container_delete = openstackclient.object.v1.container:DeleteContainer - container_list = openstackclient.object.v1.container:ListContainer - container_save = openstackclient.object.v1.container:SaveContainer - container_set = openstackclient.object.v1.container:SetContainer - container_show = openstackclient.object.v1.container:ShowContainer - container_unset = openstackclient.object.v1.container:UnsetContainer - object_create = openstackclient.object.v1.object:CreateObject - object_delete = openstackclient.object.v1.object:DeleteObject - object_list = openstackclient.object.v1.object:ListObject - object_save = openstackclient.object.v1.object:SaveObject - object_set = openstackclient.object.v1.object:SetObject - object_show = openstackclient.object.v1.object:ShowObject - object_unset = openstackclient.object.v1.object:UnsetObject - -openstack.volume.v2 = - consistency_group_add_volume = openstackclient.volume.v2.consistency_group:AddVolumeToConsistencyGroup - consistency_group_create = openstackclient.volume.v2.consistency_group:CreateConsistencyGroup - consistency_group_delete = openstackclient.volume.v2.consistency_group:DeleteConsistencyGroup - consistency_group_list = openstackclient.volume.v2.consistency_group:ListConsistencyGroup - consistency_group_remove_volume = openstackclient.volume.v2.consistency_group:RemoveVolumeFromConsistencyGroup - consistency_group_set = openstackclient.volume.v2.consistency_group:SetConsistencyGroup - consistency_group_show = openstackclient.volume.v2.consistency_group:ShowConsistencyGroup - - consistency_group_snapshot_create = openstackclient.volume.v2.consistency_group_snapshot:CreateConsistencyGroupSnapshot - consistency_group_snapshot_delete = openstackclient.volume.v2.consistency_group_snapshot:DeleteConsistencyGroupSnapshot - consistency_group_snapshot_list = openstackclient.volume.v2.consistency_group_snapshot:ListConsistencyGroupSnapshot - consistency_group_snapshot_show = openstackclient.volume.v2.consistency_group_snapshot:ShowConsistencyGroupSnapshot - - volume_create = openstackclient.volume.v2.volume:CreateVolume - volume_delete = openstackclient.volume.v2.volume:DeleteVolume - volume_list = openstackclient.volume.v2.volume:ListVolume - volume_migrate = openstackclient.volume.v2.volume:MigrateVolume - volume_set = openstackclient.volume.v2.volume:SetVolume - volume_show = openstackclient.volume.v2.volume:ShowVolume - volume_unset = openstackclient.volume.v2.volume:UnsetVolume - - volume_backup_create = openstackclient.volume.v2.volume_backup:CreateVolumeBackup - volume_backup_delete = openstackclient.volume.v2.volume_backup:DeleteVolumeBackup - volume_backup_list = openstackclient.volume.v2.volume_backup:ListVolumeBackup - volume_backup_restore = openstackclient.volume.v2.volume_backup:RestoreVolumeBackup - volume_backup_set = openstackclient.volume.v2.volume_backup:SetVolumeBackup - volume_backup_show = openstackclient.volume.v2.volume_backup:ShowVolumeBackup - - volume_backup_record_export = openstackclient.volume.v2.backup_record:ExportBackupRecord - volume_backup_record_import = openstackclient.volume.v2.backup_record:ImportBackupRecord - - volume_backend_capability_show = openstackclient.volume.v2.volume_backend:ShowCapability - volume_backend_pool_list = openstackclient.volume.v2.volume_backend:ListPool - - volume_host_failover = openstackclient.volume.v2.volume_host:FailoverVolumeHost - volume_host_set = openstackclient.volume.v2.volume_host:SetVolumeHost - - volume_snapshot_create = openstackclient.volume.v2.volume_snapshot:CreateVolumeSnapshot - volume_snapshot_delete = openstackclient.volume.v2.volume_snapshot:DeleteVolumeSnapshot - volume_snapshot_list = openstackclient.volume.v2.volume_snapshot:ListVolumeSnapshot - volume_snapshot_set = openstackclient.volume.v2.volume_snapshot:SetVolumeSnapshot - volume_snapshot_show = openstackclient.volume.v2.volume_snapshot:ShowVolumeSnapshot - volume_snapshot_unset = openstackclient.volume.v2.volume_snapshot:UnsetVolumeSnapshot - - volume_type_create = openstackclient.volume.v2.volume_type:CreateVolumeType - volume_type_delete = openstackclient.volume.v2.volume_type:DeleteVolumeType - volume_type_list = openstackclient.volume.v2.volume_type:ListVolumeType - volume_type_set = openstackclient.volume.v2.volume_type:SetVolumeType - volume_type_show = openstackclient.volume.v2.volume_type:ShowVolumeType - volume_type_unset = openstackclient.volume.v2.volume_type:UnsetVolumeType - - volume_qos_associate = openstackclient.volume.v2.qos_specs:AssociateQos - volume_qos_create = openstackclient.volume.v2.qos_specs:CreateQos - volume_qos_delete = openstackclient.volume.v2.qos_specs:DeleteQos - volume_qos_disassociate = openstackclient.volume.v2.qos_specs:DisassociateQos - volume_qos_list = openstackclient.volume.v2.qos_specs:ListQos - volume_qos_set = openstackclient.volume.v2.qos_specs:SetQos - volume_qos_show = openstackclient.volume.v2.qos_specs:ShowQos - volume_qos_unset = openstackclient.volume.v2.qos_specs:UnsetQos - - volume_service_list = openstackclient.volume.v2.service:ListService - volume_service_set = openstackclient.volume.v2.service:SetService - - volume_transfer_request_accept = openstackclient.volume.v2.volume_transfer_request:AcceptTransferRequest - volume_transfer_request_create = openstackclient.volume.v2.volume_transfer_request:CreateTransferRequest - volume_transfer_request_delete = openstackclient.volume.v2.volume_transfer_request:DeleteTransferRequest - volume_transfer_request_list = openstackclient.volume.v2.volume_transfer_request:ListTransferRequest - volume_transfer_request_show = openstackclient.volume.v2.volume_transfer_request:ShowTransferRequest - -openstack.volume.v3 = - consistency_group_add_volume = openstackclient.volume.v2.consistency_group:AddVolumeToConsistencyGroup - consistency_group_create = openstackclient.volume.v2.consistency_group:CreateConsistencyGroup - consistency_group_delete = openstackclient.volume.v2.consistency_group:DeleteConsistencyGroup - consistency_group_list = openstackclient.volume.v2.consistency_group:ListConsistencyGroup - consistency_group_remove_volume = openstackclient.volume.v2.consistency_group:RemoveVolumeFromConsistencyGroup - consistency_group_set = openstackclient.volume.v2.consistency_group:SetConsistencyGroup - consistency_group_show = openstackclient.volume.v2.consistency_group:ShowConsistencyGroup - - consistency_group_snapshot_create = openstackclient.volume.v2.consistency_group_snapshot:CreateConsistencyGroupSnapshot - consistency_group_snapshot_delete = openstackclient.volume.v2.consistency_group_snapshot:DeleteConsistencyGroupSnapshot - consistency_group_snapshot_list = openstackclient.volume.v2.consistency_group_snapshot:ListConsistencyGroupSnapshot - consistency_group_snapshot_show = openstackclient.volume.v2.consistency_group_snapshot:ShowConsistencyGroupSnapshot - - volume_create = openstackclient.volume.v3.volume:CreateVolume - volume_delete = openstackclient.volume.v3.volume:DeleteVolume - volume_list = openstackclient.volume.v3.volume:ListVolume - volume_migrate = openstackclient.volume.v3.volume:MigrateVolume - volume_set = openstackclient.volume.v3.volume:SetVolume - volume_show = openstackclient.volume.v3.volume:ShowVolume - volume_unset = openstackclient.volume.v3.volume:UnsetVolume - - volume_attachment_create = openstackclient.volume.v3.volume_attachment:CreateVolumeAttachment - volume_attachment_delete = openstackclient.volume.v3.volume_attachment:DeleteVolumeAttachment - volume_attachment_list = openstackclient.volume.v3.volume_attachment:ListVolumeAttachment - volume_attachment_complete = openstackclient.volume.v3.volume_attachment:CompleteVolumeAttachment - volume_attachment_set = openstackclient.volume.v3.volume_attachment:SetVolumeAttachment - volume_attachment_show = openstackclient.volume.v3.volume_attachment:ShowVolumeAttachment - - volume_backup_create = openstackclient.volume.v3.volume_backup:CreateVolumeBackup - volume_backup_delete = openstackclient.volume.v3.volume_backup:DeleteVolumeBackup - volume_backup_list = openstackclient.volume.v3.volume_backup:ListVolumeBackup - volume_backup_restore = openstackclient.volume.v3.volume_backup:RestoreVolumeBackup - volume_backup_set = openstackclient.volume.v3.volume_backup:SetVolumeBackup - volume_backup_unset = openstackclient.volume.v3.volume_backup:UnsetVolumeBackup - volume_backup_show = openstackclient.volume.v3.volume_backup:ShowVolumeBackup - - volume_backend_capability_show = openstackclient.volume.v2.volume_backend:ShowCapability - volume_backend_pool_list = openstackclient.volume.v2.volume_backend:ListPool - - volume_backup_record_export = openstackclient.volume.v2.backup_record:ExportBackupRecord - volume_backup_record_import = openstackclient.volume.v2.backup_record:ImportBackupRecord - - volume_group_create = openstackclient.volume.v3.volume_group:CreateVolumeGroup - volume_group_delete = openstackclient.volume.v3.volume_group:DeleteVolumeGroup - volume_group_list = openstackclient.volume.v3.volume_group:ListVolumeGroup - volume_group_failover = openstackclient.volume.v3.volume_group:FailoverVolumeGroup - volume_group_set = openstackclient.volume.v3.volume_group:SetVolumeGroup - volume_group_show = openstackclient.volume.v3.volume_group:ShowVolumeGroup - - volume_group_snapshot_create = openstackclient.volume.v3.volume_group_snapshot:CreateVolumeGroupSnapshot - volume_group_snapshot_delete = openstackclient.volume.v3.volume_group_snapshot:DeleteVolumeGroupSnapshot - volume_group_snapshot_list = openstackclient.volume.v3.volume_group_snapshot:ListVolumeGroupSnapshot - volume_group_snapshot_show = openstackclient.volume.v3.volume_group_snapshot:ShowVolumeGroupSnapshot - - volume_group_type_create = openstackclient.volume.v3.volume_group_type:CreateVolumeGroupType - volume_group_type_delete = openstackclient.volume.v3.volume_group_type:DeleteVolumeGroupType - volume_group_type_list = openstackclient.volume.v3.volume_group_type:ListVolumeGroupType - volume_group_type_set = openstackclient.volume.v3.volume_group_type:SetVolumeGroupType - volume_group_type_show = openstackclient.volume.v3.volume_group_type:ShowVolumeGroupType - - volume_host_set = openstackclient.volume.v2.volume_host:SetVolumeHost - - volume_message_delete = openstackclient.volume.v3.volume_message:DeleteMessage - volume_message_list = openstackclient.volume.v3.volume_message:ListMessages - volume_message_show = openstackclient.volume.v3.volume_message:ShowMessage - - block_storage_cluster_list = openstackclient.volume.v3.block_storage_cluster:ListBlockStorageCluster - block_storage_cluster_set = openstackclient.volume.v3.block_storage_cluster:SetBlockStorageCluster - block_storage_cluster_show = openstackclient.volume.v3.block_storage_cluster:ShowBlockStorageCluster - block_storage_resource_filter_list = openstackclient.volume.v3.block_storage_resource_filter:ListBlockStorageResourceFilter - block_storage_resource_filter_show = openstackclient.volume.v3.block_storage_resource_filter:ShowBlockStorageResourceFilter - - volume_snapshot_create = openstackclient.volume.v2.volume_snapshot:CreateVolumeSnapshot - volume_snapshot_delete = openstackclient.volume.v3.volume_snapshot:DeleteVolumeSnapshot - volume_snapshot_list = openstackclient.volume.v2.volume_snapshot:ListVolumeSnapshot - volume_snapshot_set = openstackclient.volume.v2.volume_snapshot:SetVolumeSnapshot - volume_snapshot_show = openstackclient.volume.v2.volume_snapshot:ShowVolumeSnapshot - volume_snapshot_unset = openstackclient.volume.v2.volume_snapshot:UnsetVolumeSnapshot - - volume_type_create = openstackclient.volume.v3.volume_type:CreateVolumeType - volume_type_delete = openstackclient.volume.v3.volume_type:DeleteVolumeType - volume_type_list = openstackclient.volume.v3.volume_type:ListVolumeType - volume_type_set = openstackclient.volume.v3.volume_type:SetVolumeType - volume_type_show = openstackclient.volume.v3.volume_type:ShowVolumeType - volume_type_unset = openstackclient.volume.v3.volume_type:UnsetVolumeType - - volume_qos_associate = openstackclient.volume.v2.qos_specs:AssociateQos - volume_qos_create = openstackclient.volume.v2.qos_specs:CreateQos - volume_qos_delete = openstackclient.volume.v2.qos_specs:DeleteQos - volume_qos_disassociate = openstackclient.volume.v2.qos_specs:DisassociateQos - volume_qos_list = openstackclient.volume.v2.qos_specs:ListQos - volume_qos_set = openstackclient.volume.v2.qos_specs:SetQos - volume_qos_show = openstackclient.volume.v2.qos_specs:ShowQos - volume_qos_unset = openstackclient.volume.v2.qos_specs:UnsetQos - - volume_service_list = openstackclient.volume.v3.service:ListService - volume_service_set = openstackclient.volume.v2.service:SetService - - volume_transfer_request_accept = openstackclient.volume.v3.volume_transfer_request:AcceptTransferRequest - volume_transfer_request_create = openstackclient.volume.v3.volume_transfer_request:CreateTransferRequest - volume_transfer_request_delete = openstackclient.volume.v3.volume_transfer_request:DeleteTransferRequest - volume_transfer_request_list = openstackclient.volume.v3.volume_transfer_request:ListTransferRequest - volume_transfer_request_show = openstackclient.volume.v3.volume_transfer_request:ShowTransferRequest - - volume_summary = openstackclient.volume.v3.volume:VolumeSummary - volume_revert = openstackclient.volume.v3.volume:VolumeRevertToSnapshot - block_storage_log_level_list = openstackclient.volume.v3.block_storage_log_level:BlockStorageLogLevelList - block_storage_log_level_set = openstackclient.volume.v3.block_storage_log_level:BlockStorageLogLevelSet - block_storage_cleanup = openstackclient.volume.v3.block_storage_cleanup:BlockStorageCleanup - block_storage_volume_manageable_list = openstackclient.volume.v3.block_storage_manage:BlockStorageManageVolumes - block_storage_snapshot_manageable_list = openstackclient.volume.v3.block_storage_manage:BlockStorageManageSnapshots diff --git a/test-requirements.txt b/test-requirements.txt index c9c1b28ccd..d6a0db5b41 100644 --- a/test-requirements.txt +++ b/test-requirements.txt @@ -1,5 +1,6 @@ coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD +hacking>=8.0.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 stestr>=1.0.0 # Apache-2.0 testtools>=2.2.0 # MIT diff --git a/tox.ini b/tox.ini index 7b3d951d20..5c3f49b626 100644 --- a/tox.ini +++ b/tox.ini @@ -4,118 +4,135 @@ envlist = py3,pep8 [testenv] description = - Run unit tests. + Run unit tests. usedevelop = true setenv = - OS_STDOUT_CAPTURE=1 - OS_STDERR_CAPTURE=1 - OS_TEST_TIMEOUT=60 + OS_STDOUT_CAPTURE=1 + OS_STDERR_CAPTURE=1 + OS_TEST_TIMEOUT=60 deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/test-requirements.txt - -r{toxinidir}/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/test-requirements.txt + -r{toxinidir}/requirements.txt commands = - stestr run {posargs} + stestr run {posargs} [testenv:pep8] description = - Run style checks. -skip_install = true + Run style checks. deps = - pre-commit + pre-commit + {[testenv:mypy]deps} commands = - pre-commit run --all-files --show-diff-on-failure + pre-commit run --all-files --show-diff-on-failure + {[testenv:mypy]commands} -[testenv:bandit] +[testenv:mypy] description = - Run bandit security checks. -skip_install = true + Run type checks. deps = - pre-commit + {[testenv]deps} + mypy + types-requests commands = - pre-commit run --all-files --show-diff-on-failure bandit + mypy --cache-dir="{envdir}/mypy_cache" {posargs:openstackclient} [testenv:unit-tips] commands = - python -m pip install -q -U -e {toxinidir}/../cliff#egg=cliff - python -m pip install -q -U -e {toxinidir}/../keystoneauth#egg=keystoneauth - python -m pip install -q -U -e {toxinidir}/../osc-lib#egg=osc_lib - python -m pip install -q -U -e {toxinidir}/../openstacksdk#egg=openstacksdk - python -m pip freeze - stestr run {posargs} + python -m pip install -q -U -e {toxinidir}/../cliff#egg=cliff + python -m pip install -q -U -e {toxinidir}/../keystoneauth#egg=keystoneauth + python -m pip install -q -U -e {toxinidir}/../osc-lib#egg=osc_lib + python -m pip install -q -U -e {toxinidir}/../openstacksdk#egg=openstacksdk + python -m pip freeze + stestr run {posargs} -[testenv:functional{,-tips,-py38,-py39,-py310,-py311,-py312}] +[testenv:functional{,-tips,-py310,-py311,-py312,-py313,-py314}] description = - Run functional tests. + Run functional tests. setenv = - OS_TEST_PATH=./openstackclient/tests/functional + OS_TEST_PATH=./openstackclient/tests/functional passenv = - OS_* + OS_* commands = - tips: python -m pip install -q -U -e {toxinidir}/../cliff#egg=cliff - tips: python -m pip install -q -U -e {toxinidir}/../keystoneauth#egg=keystoneauth1 - tips: python -m pip install -q -U -e {toxinidir}/../osc-lib#egg=osc_lib - tips: python -m pip install -q -U -e {toxinidir}/../openstacksdk#egg=openstacksdk - tips: python -m pip freeze - {[testenv]commands} + tips: python -m pip install -q -U -e {toxinidir}/../cliff#egg=cliff + tips: python -m pip install -q -U -e {toxinidir}/../keystoneauth#egg=keystoneauth1 + tips: python -m pip install -q -U -e {toxinidir}/../osc-lib#egg=osc_lib + tips: python -m pip install -q -U -e {toxinidir}/../openstacksdk#egg=openstacksdk + tips: python -m pip freeze + {[testenv]commands} [testenv:venv] description = - Run specified command in a virtual environment with all dependencies installed. + Run specified command in a virtual environment with all dependencies installed. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/requirements.txt - -r{toxinidir}/doc/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/requirements.txt + -r{toxinidir}/doc/requirements.txt commands = - {posargs} + {posargs} [testenv:cover] description = - Run unit tests and generate coverage report. + Run unit tests and generate coverage report. setenv = - {[testenv]setenv} - PYTHON=coverage run --source openstackclient --parallel-mode + {[testenv]setenv} + PYTHON=coverage run --source openstackclient --parallel-mode commands = - stestr run {posargs} - coverage combine - coverage html -d cover - coverage xml -o cover/coverage.xml + stestr run {posargs} + coverage combine + coverage html -d cover + coverage xml -o cover/coverage.xml [testenv:debug] description = - Run specified tests through oslo_debug_helper, which allows use of pdb. + Run specified tests through oslo_debug_helper, which allows use of pdb. passenv = - OS_* + OS_* commands = - oslo_debug_helper -t openstackclient/tests {posargs} + oslo_debug_helper -t openstackclient/tests {posargs} [testenv:docs] description = - Build documentation in HTML format. + Build documentation in HTML format. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/doc/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt commands = - sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html - sphinx-build -a -E -W -d doc/build/doctrees -b man doc/source doc/build/man - # Validate redirects (must be done after the docs build - whereto doc/build/html/.htaccess doc/test/redirect-tests.txt + sphinx-build -a -E -W -d doc/build/doctrees -b html doc/source doc/build/html + sphinx-build -a -E -W -d doc/build/doctrees -b man doc/source doc/build/man + # Validate redirects (must be done after the docs build + whereto doc/build/html/.htaccess doc/test/redirect-tests.txt [testenv:releasenotes] description = - Build release note documentation in HTML format. + Build release note documentation in HTML format. deps = - -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} - -r{toxinidir}/doc/requirements.txt + -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} + -r{toxinidir}/doc/requirements.txt commands = - sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html + sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [flake8] show-source = true exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build,tools,releasenotes -# We only enable the hacking (H) checks -select = H +# We only enable the hacking (H) and openstackclient (O) checks +select = H,O # H301 Black will put commas after imports that can't fit on one line ignore = H301 import-order-style = pep8 application_import_names = openstackclient + +[flake8:local-plugins] +extension = + O400 = checks:assert_no_oslo + O401 = checks:assert_no_duplicated_setup + O402 = checks:assert_use_of_client_aliases + O403 = checks:assert_find_ignore_missing_kwargs + O404 = checks:assert_use_of_osc_command +paths = ./hacking + +[hacking] +import_exceptions = + collections.abc + openstackclient.i18n + typing