diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b277bb059b..c76b800593 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,28 +15,14 @@ repos: files: .*\.(yaml|yml)$ args: ['--unsafe'] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.14.0 + rev: v0.15.18 hooks: - 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.18.2 - hooks: - - id: mypy - additional_dependencies: - - types-requests - # keep this in-sync with '[tool.mypy] exclude' in 'pyproject.toml' - exclude: | - (?x)( - doc/.* - | examples/.* - | hacking/.* - | releasenotes/.* - ) diff --git a/.zuul.yaml b/.zuul.yaml index d63ee7c5f4..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,6 +169,7 @@ - osc-tox-unit-tips - openstack-cover-jobs - openstack-python3-jobs + - openstack-python3-next-jobs - publish-openstack-docs-pti - check-requirements - release-notes-jobs-python3 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/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 8c09fc3648..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 ---------------- 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 f978962274..b380886d6f 100644 --- a/doc/source/cli/data/glance.csv +++ b/doc/source/cli/data/glance.csv @@ -30,7 +30,7 @@ md-namespace-objects-delete,image metadef object delete,Delete all metadata defi 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 index 0eb485e7e2..80807df87b 100644 --- a/hacking/checks.py +++ b/hacking/checks.py @@ -30,17 +30,28 @@ def assert_no_oslo(logical_line): """Check for use of oslo libraries. - O400 + Okay: import os + Okay: from os import path + O400: import oslo_messaging + O400: from oslo_log import log """ - if re.match(r'(from|import) oslo_.*', logical_line): - yield (0, "0400: oslo libraries should not be used in SDK projects") + 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. - O401 + 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 @@ -53,7 +64,7 @@ def assert_no_duplicated_setup(logical_line, filename): if os.path.basename(filename) != 'fakes.py': if re.match( - r'self.[a-z_]+_client = self.app.client_manager.*', logical_line + r'self.[a-zA-Z0-9_]+ = self.app.client_manager.*', logical_line ): yield ( 0, @@ -75,10 +86,17 @@ def assert_no_duplicated_setup(logical_line, filename): @core.flake8ext -def assert_use_of_client_aliases(logical_line): +def assert_use_of_client_aliases(logical_line, filename): """Ensure we use $service_client instead of $sdk_connection.service. - O402 + 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( @@ -86,7 +104,22 @@ def assert_use_of_client_aliases(logical_line): logical_line, ): service = match.group(1) - yield (0, f"0402: prefer {service}_client to sdk_connection.{service}") + 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 @@ -137,6 +170,11 @@ def visit_Call(self, node): 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) @@ -177,3 +215,22 @@ def assert_find_ignore_missing_kwargs(logical_line, filename): '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 41e1b685b0..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,7 +70,7 @@ def list_security_groups(compute_client, all_projects=None): return response.json()['security_groups'] -def find_security_group(compute_client, 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,7 +258,7 @@ def list_networks(compute_client): return response.json()['networks'] -def find_network(compute_client, 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 933b01b836..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,33 +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)}/" - f"{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: @@ -298,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: @@ -374,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: @@ -394,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: @@ -403,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: @@ -434,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: @@ -456,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: @@ -502,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: @@ -519,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: @@ -541,8 +515,8 @@ def account_show(self): def account_unset( self, - properties, - ): + properties: dict[str, str], + ) -> None: """Unset account properties :param dict properties: @@ -555,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 @@ -567,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 @@ -581,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) @@ -591,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 index 9575379c69..ef2191b179 100644 --- a/openstackclient/api/volume_v2.py +++ b/openstackclient/api/volume_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,7 @@ # consistency groups -def find_consistency_group(compute_client, name_or_id): +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 diff --git a/openstackclient/api/volume_v3.py b/openstackclient/api/volume_v3.py index 1a3f25fa01..d86537c3be 100644 --- a/openstackclient/api/volume_v3.py +++ b/openstackclient/api/volume_v3.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,7 @@ # consistency groups -def find_consistency_group(compute_client, name_or_id): +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 diff --git a/openstackclient/common/availability_zone.py b/openstackclient/common/availability_zone.py index 6f5e4fd455..cc8d82d580 100644 --- a/openstackclient/common/availability_zone.py +++ b/openstackclient/common/availability_zone.py @@ -13,10 +13,14 @@ """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 openstack import utils as sdk_utils from osc_lib import utils from openstackclient import command @@ -26,8 +30,10 @@ 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 51911aaf99..ca081664e0 100644 --- a/openstackclient/common/clientmanager.py +++ b/openstackclient/common/clientmanager.py @@ -20,25 +20,23 @@ 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 -if ty.TYPE_CHECKING: - from keystoneauth1 import access as ksa_access - from openstack.compute.v2 import _proxy as compute_proxy - from openstack.image.v2 import _proxy as image_proxy - from openstack.network.v2 import _proxy as network_proxy - - from openstackclient.api import object_store_v1 +from openstackclient.api import object_store_v1 LOG = logging.getLogger(__name__) - -PLUGIN_MODULES: list[ty.Any] = [] - +PLUGIN_MODULES: list[Any] = [] USER_AGENT = 'python-openstackclient' @@ -50,30 +48,27 @@ class ClientManager(clientmanager.ClientManager): in osc-lib so we need to maintain a transition period. """ - if ty.TYPE_CHECKING: - # 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 and volume once we've - # migrated everything to SDK. Hopefully by then we'll have figured out - # how to statically distinguish between the v2 and v3 versions of both - # services... - # TODO(stephenfin): We also need to migrate object storage... - compute: compute_proxy.Proxy - identity: ty.Any - image: image_proxy.Proxy - network: network_proxy.Proxy - object_store: object_store_v1.APIv1 - volume: ty.Any + # 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, @@ -81,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 @@ -89,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: @@ -125,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. @@ -150,52 +145,77 @@ 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 = ty.TypeVar('ArgumentParserT', bound=argparse.ArgumentParser) +ArgumentParserT = TypeVar('ArgumentParserT', bound=argparse.ArgumentParser) -@ty.runtime_checkable # Optional: allows usage with isinstance() -class PluginModule(ty.Protocol): +@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[..., ty.Any] + 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: BaseException, ) -> None: @@ -204,7 +224,7 @@ def _on_load_failure_callback( ) -def get_plugin_modules(group): +def get_plugin_modules(group: str) -> list[Any]: """Find plugin entry points""" mod_list = [] mgr: stevedore.ExtensionManager[PluginModule] @@ -238,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 4637ad22b8..e73d42373b 100644 --- a/openstackclient/common/configuration.py +++ b/openstackclient/common/configuration.py @@ -13,6 +13,10 @@ """Configuration action implementations""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from keystoneauth1.loading import base from openstackclient import command @@ -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 3f9b257bf3..106f65a5c6 100644 --- a/openstackclient/common/extension.py +++ b/openstackclient/common/extension.py @@ -15,8 +15,12 @@ """Extension action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any +from openstack.identity import v2 as identity_v2 from osc_lib import utils from openstackclient import command @@ -25,7 +29,9 @@ 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 6512e0fcd1..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 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 6ca5dc2315..ca0ab779b8 100644 --- a/openstackclient/common/module.py +++ b/openstackclient/common/module.py @@ -15,7 +15,11 @@ """Module action implementation""" +import argparse +import importlib.metadata import sys +from collections.abc import Iterable, Sequence +from typing import Any from osc_lib import utils @@ -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,7 +45,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, ...]]]: cm = self.app.command_manager groups = cm.get_command_groups() groups = sorted(groups) @@ -61,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)) @@ -73,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', @@ -83,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 444f23ec2c..6721028bce 100644 --- a/openstackclient/common/project_cleanup.py +++ b/openstackclient/common/project_cleanup.py @@ -13,13 +13,15 @@ # 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 openstack import utils as sdk_utils from openstackclient import command from openstackclient.i18n import _ @@ -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,7 +91,7 @@ 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: connection = self.app.client_manager.sdk_connection if parsed_args.auth_project: @@ -97,13 +99,16 @@ def take_action(self, parsed_args): # as-is pass elif parsed_args.project: - project = connection.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 ) connection = connection.connect_as_project(project) if connection: - status_queue: queue.Queue[ty.Any] = queue.Queue() + status_queue: queue.Queue[Any] = queue.Queue() parsed_args.max_width = int( os.environ.get('CLIFF_MAX_TERM_WIDTH', 0) ) diff --git a/openstackclient/common/quota.py b/openstackclient/common/quota.py index 6d0025a754..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 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'] @@ -793,11 +804,15 @@ 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) - def _normalize_names(section: dict) -> None: + 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 @@ -805,7 +820,6 @@ def _normalize_names(section: dict) -> None: # 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(), ): @@ -818,13 +832,11 @@ def _normalize_names(section: dict) -> None: _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()] @@ -870,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', @@ -915,8 +927,11 @@ 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 ) @@ -934,7 +949,7 @@ def take_action(self, parsed_args): parsed_args.service == 'all' and 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_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 dfd84e059d..5fb39e1168 100644 --- a/openstackclient/common/versions.py +++ b/openstackclient/common/versions.py @@ -14,6 +14,10 @@ """Versions Action Implementation""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from openstackclient import command from openstackclient.i18n import _ @@ -21,7 +25,7 @@ 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( @@ -67,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 71b68d4c61..3582a59b0f 100644 --- a/openstackclient/compute/v2/agent.py +++ b/openstackclient/compute/v2/agent.py @@ -11,11 +11,13 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# """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 import exceptions @@ -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 f8c3d9677e..71aff69746 100644 --- a/openstackclient/compute/v2/aggregate.py +++ b/openstackclient/compute/v2/aggregate.py @@ -16,8 +16,10 @@ """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 @@ -33,7 +35,7 @@ LOG = logging.getLogger(__name__) -_aggregate_formatters: dict[str, type[columns.FormattableColumn[ty.Any]]] = { +_aggregate_formatters: dict[str, type[columns.FormattableColumn[Any]]] = { 'Hosts': format_columns.ListColumn, 'Metadata': format_columns.DictColumn, 'hosts': format_columns.ListColumn, @@ -41,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 = { @@ -56,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', @@ -68,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( @@ -89,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") @@ -111,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} @@ -137,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', @@ -147,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: @@ -180,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', @@ -188,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") @@ -240,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', @@ -252,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( @@ -273,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', @@ -309,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 @@ -323,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. @@ -345,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', @@ -354,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 @@ -374,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", @@ -394,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 @@ -411,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', @@ -429,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'): diff --git a/openstackclient/compute/v2/console.py b/openstackclient/compute/v2/console.py index cbcce4b09f..4fb09a5600 100644 --- a/openstackclient/compute/v2/console.py +++ b/openstackclient/compute/v2/console.py @@ -15,6 +15,10 @@ """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 import utils @@ -22,7 +26,7 @@ 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( @@ -78,7 +82,7 @@ def take_action(self, parsed_args): 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', @@ -138,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 97eb1a80e1..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.""" +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 de3a710298..645536b52c 100644 --- a/openstackclient/compute/v2/flavor.py +++ b/openstackclient/compute/v2/flavor.py @@ -15,7 +15,10 @@ """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 @@ -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,9 +150,13 @@ 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") @@ -177,10 +183,19 @@ def take_action(self, parsed_args): '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 = _( @@ -195,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( @@ -225,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", @@ -235,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: @@ -264,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( @@ -308,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 @@ -323,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 @@ -335,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: @@ -386,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", @@ -430,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( @@ -478,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 @@ -500,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", @@ -509,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 @@ -535,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", @@ -577,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( @@ -605,11 +629,12 @@ 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 + validate_actor_existence=False, + ) 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 58023676d4..98bf75efed 100644 --- a/openstackclient/compute/v2/host.py +++ b/openstackclient/compute/v2/host.py @@ -15,6 +15,10 @@ """Host action implementations""" +import argparse +from collections.abc import Iterable +from typing import Any + from openstack import exceptions as sdk_exceptions from osc_lib import utils @@ -25,7 +29,7 @@ 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 9e1b265b19..9f12df8a07 100644 --- a/openstackclient/compute/v2/hypervisor.py +++ b/openstackclient/compute/v2/hypervisor.py @@ -15,8 +15,11 @@ """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 @@ -29,7 +32,9 @@ 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 d151b41614..6c0213502a 100644 --- a/openstackclient/compute/v2/hypervisor_stats.py +++ b/openstackclient/compute/v2/hypervisor_stats.py @@ -13,13 +13,19 @@ """Hypervisor Stats action implementations""" +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 b7744698b4..5bb51b365c 100644 --- a/openstackclient/compute/v2/keypair.py +++ b/openstackclient/compute/v2/keypair.py @@ -11,16 +11,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. -# """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 import exceptions from osc_lib import utils @@ -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 1eee828d9b..66cb3415d2 100644 --- a/openstackclient/compute/v2/server.py +++ b/openstackclient/compute/v2/server.py @@ -17,11 +17,12 @@ import argparse import base64 +from collections.abc import Iterable, Sequence import getpass import json import logging import os -import typing as ty +from typing import Any from cliff import columns as cliff_columns import iso8601 @@ -38,7 +39,6 @@ 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__) @@ -59,45 +59,63 @@ class PowerStateColumn(cliff_columns.FormattableColumn[int]): '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[ty.Any]): +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 []) } + def __lt__(self, other: Any) -> bool: + # cliff only ever compares two values from the same column, + # so `other` is expected to also be an AddressesColumn + return self.human_readable() < other.human_readable() + + def __eq__(self, other: object) -> bool: + if not isinstance(other, AddressesColumn): + return NotImplemented + return self.human_readable() == other.human_readable() + + def __hash__(self) -> int: + return hash(self.human_readable()) + 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]: @@ -130,7 +148,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 @@ -352,7 +376,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", @@ -381,7 +405,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 @@ -403,7 +429,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} @@ -447,10 +473,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="", @@ -475,7 +502,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 = {} @@ -534,19 +562,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", @@ -568,7 +588,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( @@ -600,7 +620,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", @@ -622,7 +642,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( @@ -655,7 +675,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', @@ -673,7 +693,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( @@ -696,9 +716,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 @@ -729,7 +750,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', @@ -775,9 +796,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, @@ -849,7 +872,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, @@ -859,7 +887,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, []) @@ -868,7 +902,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, @@ -878,7 +917,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, []) @@ -889,12 +934,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, @@ -909,7 +954,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, []) @@ -940,9 +991,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," @@ -970,12 +1021,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 = _( @@ -1014,7 +1071,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', @@ -1039,7 +1098,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. @@ -1075,7 +1134,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, []) @@ -1091,10 +1156,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', @@ -1167,6 +1252,15 @@ def get_parser(self, prog_name): 'options.' ), ) + parser.add_argument( + '--delete-on-termination', + action='store_true', + default=False, + help=_( + 'Delete the boot volume automatically when the server is ' + 'deleted (only valid with --boot-from-volume).' + ), + ) # TODO(stephenfin): Remove this in the v7.0 parser.add_argument( '--block-device-mapping', @@ -1535,16 +1629,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: @@ -1554,7 +1653,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: @@ -1615,11 +1714,22 @@ 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 + if ( + parsed_args.delete_on_termination + and not parsed_args.boot_from_volume + ): + msg = _( + "--delete-on-termination can only be used with " + "--boot-from-volume" + ) + raise exceptions.CommandError(msg) + snapshot = None if parsed_args.snapshot: # --snapshot and --boot-from-volume are mutually exclusive. @@ -1627,9 +1737,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( @@ -1704,6 +1815,7 @@ def _match_image(image_api, wanted_properties): 'source_type': 'image', 'destination_type': 'volume', 'volume_size': parsed_args.boot_from_volume, + 'delete_on_termination': parsed_args.delete_on_termination, } ] # If booting from volume we do not pass an image to compute. @@ -1769,15 +1881,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': @@ -1909,6 +2023,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( @@ -1969,7 +2084,8 @@ def _match_image(image_api, wanted_properties): 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' @@ -2153,7 +2269,8 @@ def _match_image(image_api, wanted_properties): 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): @@ -2168,7 +2285,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', @@ -2178,7 +2295,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 name_or_id in parsed_args.server: server = compute_client.find_server( @@ -2190,7 +2307,7 @@ def take_action(self, parsed_args): 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', @@ -2219,8 +2336,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() @@ -2273,17 +2390,17 @@ def _show_progress(progress): 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 ' @@ -2307,7 +2424,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") @@ -2317,7 +2440,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', @@ -2657,27 +2780,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. @@ -2790,6 +2917,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']: @@ -3038,8 +3168,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 @@ -3060,30 +3190,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 @@ -3134,7 +3267,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', @@ -3153,7 +3286,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 = {} @@ -3189,16 +3322,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', @@ -3272,8 +3414,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() @@ -3370,7 +3512,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', @@ -3380,7 +3522,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( @@ -3393,7 +3535,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', @@ -3424,8 +3566,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() @@ -3453,7 +3595,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', @@ -3629,8 +3771,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() @@ -3822,7 +3966,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): @@ -3843,7 +3988,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', @@ -3890,8 +4035,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() @@ -3946,13 +4093,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", @@ -3966,7 +4114,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( @@ -3977,10 +4125,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="", @@ -3995,7 +4144,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, @@ -4003,15 +4153,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", @@ -4025,7 +4171,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( @@ -4050,7 +4196,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", @@ -4064,7 +4210,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( @@ -4090,7 +4236,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', @@ -4108,7 +4254,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( @@ -4131,9 +4277,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 @@ -4164,7 +4311,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', @@ -4178,9 +4325,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, @@ -4192,8 +4341,8 @@ def take_action(self, parsed_args): ) compute_client.delete_volume_attachment( - volume, server, + volume, ignore_missing=False, ) @@ -4206,7 +4355,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', @@ -4231,13 +4380,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 @@ -4245,7 +4394,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 ) @@ -4260,14 +4409,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='', @@ -4298,8 +4447,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() @@ -4356,7 +4505,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', @@ -4365,7 +4514,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 @@ -4377,7 +4526,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." @@ -4404,7 +4553,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', @@ -4413,7 +4562,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 @@ -4425,7 +4574,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." @@ -4447,7 +4596,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', @@ -4457,7 +4606,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( @@ -4470,7 +4619,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', @@ -4480,7 +4629,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( @@ -4493,7 +4642,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', @@ -4590,10 +4739,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 @@ -4606,7 +4765,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 @@ -4636,6 +4795,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: @@ -4647,6 +4814,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) @@ -4704,7 +4876,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', @@ -4730,8 +4902,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() @@ -4804,7 +4976,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', @@ -4830,7 +5002,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 @@ -4841,8 +5015,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: @@ -4860,13 +5035,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', @@ -4960,7 +5136,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( @@ -5017,8 +5193,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 @@ -5027,7 +5203,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', @@ -5046,7 +5222,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 +5238,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', @@ -5081,7 +5257,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( @@ -5096,7 +5272,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', @@ -5106,7 +5282,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( @@ -5119,7 +5295,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', @@ -5129,7 +5305,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( @@ -5142,7 +5318,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', @@ -5152,7 +5328,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( @@ -5165,7 +5341,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', @@ -5174,7 +5350,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 @@ -5185,7 +5361,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', @@ -5239,9 +5415,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( @@ -5263,6 +5448,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 = _( @@ -5281,7 +5476,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', @@ -5327,8 +5522,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 bb06f761c9..d7985baf70 100644 --- a/openstackclient/compute/v2/server_backup.py +++ b/openstackclient/compute/v2/server_backup.py @@ -15,7 +15,10 @@ """Compute v2 Server action implementations""" +import argparse +from collections.abc import Iterable, Sequence import importlib +from typing import Any from osc_lib import exceptions from osc_lib import utils @@ -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 0e1e2b4632..40e8962ea6 100644 --- a/openstackclient/compute/v2/server_event.py +++ b/openstackclient/compute/v2/server_event.py @@ -15,11 +15,15 @@ """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 import exceptions @@ -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 b74c25626b..09c8ab048d 100644 --- a/openstackclient/compute/v2/server_group.py +++ b/openstackclient/compute/v2/server_group.py @@ -15,8 +15,10 @@ """Compute v2 Server Group 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 @@ -32,14 +34,16 @@ LOG = logging.getLogger(__name__) -_formatters: dict[str, type[columns.FormattableColumn[ty.Any]]] = { +_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'] @@ -57,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', @@ -94,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'): @@ -144,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', @@ -154,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: @@ -179,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', @@ -198,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 = {} @@ -211,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) @@ -256,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', @@ -265,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 26dbb4ccc3..5b70ecb0f3 100644 --- a/openstackclient/compute/v2/server_image.py +++ b/openstackclient/compute/v2/server_image.py @@ -15,8 +15,11 @@ """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 import exceptions @@ -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 f2ea68343f..aa6b75379c 100644 --- a/openstackclient/compute/v2/server_migration.py +++ b/openstackclient/compute/v2/server_migration.py @@ -12,6 +12,9 @@ """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 @@ -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 d92d137b73..f5ae05b437 100644 --- a/openstackclient/compute/v2/server_volume.py +++ b/openstackclient/compute/v2/server_volume.py @@ -14,6 +14,10 @@ """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 import exceptions from osc_lib import utils @@ -25,7 +29,7 @@ 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 b911835f80..6e50342c8a 100644 --- a/openstackclient/compute/v2/service.py +++ b/openstackclient/compute/v2/service.py @@ -15,8 +15,12 @@ """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 import exceptions from osc_lib import utils @@ -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 3015a9e709..06ac6560e0 100644 --- a/openstackclient/compute/v2/usage.py +++ b/openstackclient/compute/v2/usage.py @@ -15,16 +15,19 @@ """Usage action implementations""" -from collections.abc import Collection +import argparse +from collections.abc import Collection, Iterable, Sequence import datetime import functools -import typing as ty +from typing import Any from cliff import columns as cliff_columns +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 @@ -40,32 +43,34 @@ class ProjectColumn(cliff_columns.FormattableColumn[str]): 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[Collection[ty.Any]]): - 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[float]): - def human_readable(self): - return float(f"{self._value:.2f}") + 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 @@ -77,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 @@ -99,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) @@ -110,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", @@ -128,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", @@ -154,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( @@ -179,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 @@ -210,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", @@ -218,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="", @@ -234,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( @@ -253,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 0684764706..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: @@ -107,7 +106,7 @@ def find_service_sdk(identity_client, name_type_or_id): 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 @@ -126,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 @@ -138,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 @@ -174,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, @@ -200,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 @@ -216,12 +227,12 @@ def find_group(identity_client, name_or_id, domain_name_or_id=None): def find_group_id_sdk( - identity_client, - name_or_id, - domain_name_or_id=None, + identity_client: 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_group, @@ -242,7 +253,11 @@ def find_group_id_sdk( ) -def find_project(identity_client, name_or_id, domain_name_or_id=None): +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 @@ -256,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 @@ -268,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, @@ -294,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 @@ -334,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 @@ -346,10 +415,10 @@ def _find_sdk_id( if not validate_actor_existence: return name_or_id raise exceptions.CommandError from exc - return resource.id + 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='', @@ -361,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='', @@ -373,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='', @@ -399,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', @@ -410,7 +479,7 @@ def add_inherited_option_to_parser(parser): ) -def add_resource_option_to_parser(parser): +def add_resource_option_to_parser(parser: argparse.ArgumentParser) -> None: immutable_group = parser.add_mutually_exclusive_group() immutable_group.add_argument( '--immutable', diff --git a/openstackclient/identity/v2_0/catalog.py b/openstackclient/identity/v2_0/catalog.py index 437cad2fc2..9f52a57371 100644 --- a/openstackclient/identity/v2_0/catalog.py +++ b/openstackclient/identity/v2_0/catalog.py @@ -13,8 +13,10 @@ """Identity v2 Service Catalog 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 osc_lib import exceptions @@ -27,8 +29,8 @@ LOG = logging.getLogger(__name__) -class EndpointsColumn(cliff_columns.FormattableColumn[ty.Any]): - def human_readable(self): +class EndpointsColumn(cliff_columns.FormattableColumn[Any]): + def human_readable(self) -> str: if not self._value: return "" ret = '' @@ -47,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: @@ -75,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', @@ -84,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: @@ -108,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 360a090246..33b16563ba 100644 --- a/openstackclient/identity/v2_0/ec2creds.py +++ b/openstackclient/identity/v2_0/ec2creds.py @@ -16,7 +16,10 @@ """Identity v2 EC2 Credentials action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from osc_lib import exceptions from osc_lib import utils @@ -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 38f7f4e56c..5ea22b52e5 100644 --- a/openstackclient/identity/v2_0/endpoint.py +++ b/openstackclient/identity/v2_0/endpoint.py @@ -15,7 +15,10 @@ """Endpoint action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from osc_lib import exceptions from osc_lib import utils @@ -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 bf19d7d09c..61ead6c0f8 100644 --- a/openstackclient/identity/v2_0/project.py +++ b/openstackclient/identity/v2_0/project.py @@ -15,7 +15,10 @@ """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 @@ -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', @@ -73,7 +76,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 enabled = True @@ -102,7 +107,8 @@ 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): @@ -115,7 +121,7 @@ class DeleteProject(command.Command): "regardless." ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'projects', @@ -125,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 @@ -158,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', @@ -177,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') @@ -200,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', @@ -240,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( @@ -272,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', @@ -281,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 = {} @@ -324,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', @@ -350,7 +361,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( identity_client.tenants, diff --git a/openstackclient/identity/v2_0/role.py b/openstackclient/identity/v2_0/role.py index e54c07af08..b2169fd943 100644 --- a/openstackclient/identity/v2_0/role.py +++ b/openstackclient/identity/v2_0/role.py @@ -15,7 +15,10 @@ """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 import exceptions @@ -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 0aa800ef84..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""" +import argparse +from collections.abc import Iterable +from typing import Any + from osc_lib import exceptions from osc_lib import utils from openstackclient import command -from openstackclient.i18n import _ # noqa +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 diff --git a/openstackclient/identity/v2_0/service.py b/openstackclient/identity/v2_0/service.py index 5e8dca7354..417bd48dd8 100644 --- a/openstackclient/identity/v2_0/service.py +++ b/openstackclient/identity/v2_0/service.py @@ -15,7 +15,10 @@ """Service action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from osc_lib import exceptions from osc_lib import utils @@ -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 ebb2269a92..e3e91c9f97 100644 --- a/openstackclient/identity/v2_0/token.py +++ b/openstackclient/identity/v2_0/token.py @@ -15,6 +15,10 @@ """Identity v2 Token action implementations""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib import exceptions from openstackclient import command @@ -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 244b9e9da9..d76ba8d095 100644 --- a/openstackclient/identity/v2_0/user.py +++ b/openstackclient/identity/v2_0/user.py @@ -15,8 +15,11 @@ """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 @@ -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 1859ef6fa5..bee319379e 100644 --- a/openstackclient/identity/v3/access_rule.py +++ b/openstackclient/identity/v3/access_rule.py @@ -15,8 +15,12 @@ """Identity v3 Access Rule action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils @@ -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,14 +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 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.identity) + 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: @@ -73,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', @@ -83,8 +92,12 @@ 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 @@ -95,7 +108,7 @@ def take_action(self, parsed_args): if auth is None: # this will never happen raise exceptions.CommandError('invalid authentication info') - user_id = auth.get_user_id(conn.identity) + user_id = auth.get_user_id(conn.session) columns = ('ID', 'Service', 'Method', 'Path') data = identity_client.access_rules(user=user_id) @@ -115,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', @@ -124,14 +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 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.identity) + 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 3b38f17b5a..3b7630186a 100644 --- a/openstackclient/identity/v3/application_credential.py +++ b/openstackclient/identity/v3/application_credential.py @@ -15,13 +15,19 @@ """Identity v3 Application Credential action implementations""" +import argparse +from collections.abc import Iterable, Sequence import datetime import json import logging -import typing as ty +from typing import Any import uuid 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 @@ -32,16 +38,18 @@ LOG = logging.getLogger(__name__) -class RolesColumn(cliff_columns.FormattableColumn[ty.Any]): +class RolesColumn(cliff_columns.FormattableColumn[Any]): """Generate a formatted string of role names.""" - def human_readable(self): - return utils.format_list(list(r['name'] for r in self._value)) + def human_readable(self) -> str: + return utils.format_list(list(r['name'] for r in self._value)) or "" def _format_application_credential( - application_credential, *, include_secret=False -): + application_credential: _application_credential.ApplicationCredential, + *, + include_secret: bool = False, +) -> tuple[tuple[str, ...], Iterable[Any]]: column_headers: tuple[str, ...] = ( 'ID', 'Name', @@ -74,7 +82,11 @@ def _format_application_credential( ) -def _format_application_credentials(application_credentials): +def _format_application_credentials( + application_credentials: Iterable[ + _application_credential.ApplicationCredential + ], +) -> tuple[tuple[str, ...], Any]: column_headers = ( 'ID', 'Name', @@ -108,7 +120,7 @@ def _format_application_credentials(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 @@ -131,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', @@ -203,15 +215,22 @@ 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 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.identity) + 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.roles: @@ -266,7 +285,7 @@ def take_action(self, parsed_args): 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', @@ -276,15 +295,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) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) + 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.identity) + 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: @@ -319,7 +344,7 @@ def take_action(self, parsed_args): 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', @@ -329,8 +354,12 @@ 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 @@ -341,7 +370,11 @@ def take_action(self, parsed_args): if auth is None: # this will never happen raise exceptions.CommandError('invalid authentication info') - user_id = auth.get_user_id(conn.identity) + _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 @@ -353,7 +386,7 @@ def take_action(self, parsed_args): 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', @@ -362,14 +395,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[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 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.identity) + + 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) application_credential = identity_client.find_application_credential( user_id, parsed_args.application_credential, ignore_missing=False diff --git a/openstackclient/identity/v3/catalog.py b/openstackclient/identity/v3/catalog.py index 7d37e6cbd2..7c85dd1447 100644 --- a/openstackclient/identity/v3/catalog.py +++ b/openstackclient/identity/v3/catalog.py @@ -12,8 +12,10 @@ """Identity v3 Service Catalog 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 osc_lib import exceptions @@ -26,8 +28,8 @@ LOG = logging.getLogger(__name__) -class EndpointsColumn(cliff_columns.FormattableColumn[ty.Any]): - 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 c58441ca83..874ed3ff2c 100644 --- a/openstackclient/identity/v3/consumer.py +++ b/openstackclient/identity/v3/consumer.py @@ -15,7 +15,10 @@ """Identity v3 Consumer action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from osc_lib import exceptions from osc_lib import utils @@ -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 02eef649c7..9b1078a649 100644 --- a/openstackclient/identity/v3/credential.py +++ b/openstackclient/identity/v3/credential.py @@ -15,8 +15,13 @@ """Identity v3 Credential action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +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 @@ -24,11 +29,12 @@ 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 28481c00d6..91f9c49e8c 100644 --- a/openstackclient/identity/v3/domain.py +++ b/openstackclient/identity/v3/domain.py @@ -15,9 +15,14 @@ """Identity v3 Domain 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.identity.v3 import domain as _domain +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils @@ -25,11 +30,12 @@ from openstackclient.i18n import _ from openstackclient.identity import common - LOG = logging.getLogger(__name__) -def _format_domain(domain): +def _format_domain( + domain: _domain.Domain, +) -> tuple[tuple[str, ...], tuple[Any, ...]]: columns = ( 'id', 'name', @@ -57,7 +63,7 @@ def _format_domain(domain): 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', @@ -91,8 +97,12 @@ 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' + ) options = {} if parsed_args.immutable is not None: @@ -120,7 +130,7 @@ def take_action(self, parsed_args): 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', @@ -130,8 +140,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.domain: try: @@ -159,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', @@ -174,7 +186,12 @@ 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 = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) kwargs = {} if parsed_args.name: kwargs['name'] = parsed_args.name @@ -183,9 +200,7 @@ def take_action(self, parsed_args): columns = ('id', 'name', 'is_enabled', 'description') column_headers = ('ID', 'Name', 'Enabled', 'Description') - data = self.app.client_manager.sdk_connection.identity.domains( - **kwargs - ) + data = identity_client.domains(**kwargs) return ( column_headers, @@ -203,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', @@ -238,8 +253,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' + ) domain = identity_client.find_domain( parsed_args.domain, ignore_missing=False ) @@ -259,7 +276,7 @@ def take_action(self, parsed_args): 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', @@ -268,8 +285,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' + ) domain = identity_client.find_domain( parsed_args.domain, ignore_missing=False ) diff --git a/openstackclient/identity/v3/ec2creds.py b/openstackclient/identity/v3/ec2creds.py index dbbf7a2474..d090fb646c 100644 --- a/openstackclient/identity/v3/ec2creds.py +++ b/openstackclient/identity/v3/ec2creds.py @@ -12,7 +12,10 @@ """Identity v3 EC2 Credentials action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any, cast from osc_lib import exceptions from osc_lib import utils @@ -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 9083fdc708..93d236efbb 100644 --- a/openstackclient/identity/v3/endpoint.py +++ b/openstackclient/identity/v3/endpoint.py @@ -15,8 +15,14 @@ """Identity v3 Endpoint action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +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 @@ -24,11 +30,12 @@ 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,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' + ) result = 0 for i in parsed_args.endpoint: try: @@ -195,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', @@ -227,8 +244,12 @@ 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: @@ -253,54 +274,65 @@ def take_action(self, parsed_args): 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_id: - 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, ignore_missing=False - ) - 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, @@ -318,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', @@ -337,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', @@ -397,13 +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 + 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( @@ -434,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', @@ -446,8 +484,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' + ) endpoint = identity_client.find_endpoint( parsed_args.endpoint, ignore_missing=False ) diff --git a/openstackclient/identity/v3/endpoint_group.py b/openstackclient/identity/v3/endpoint_group.py index 3965f31978..abbb39eb13 100644 --- a/openstackclient/identity/v3/endpoint_group.py +++ b/openstackclient/identity/v3/endpoint_group.py @@ -13,8 +13,11 @@ """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 import exceptions from osc_lib import utils @@ -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 850ec0ac7f..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 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 a2c2fd3367..766f31886b 100644 --- a/openstackclient/identity/v3/group.py +++ b/openstackclient/identity/v3/group.py @@ -15,9 +15,14 @@ """Group action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any 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 @@ -25,11 +30,12 @@ from openstackclient.i18n import _ from openstackclient.identity import common - LOG = logging.getLogger(__name__) -def _format_group(group): +def _format_group( + group: _group.Group, +) -> tuple[tuple[str, ...], Iterable[Any]]: columns = ( 'description', 'domain_id', @@ -51,7 +57,7 @@ def _format_group(group): 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', @@ -71,8 +77,10 @@ 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) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) group_id = common.find_group_id_sdk( identity_client, parsed_args.group, parsed_args.group_domain @@ -111,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', @@ -127,8 +135,10 @@ 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) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) user_id = common.find_user_id_sdk( identity_client, @@ -168,7 +178,7 @@ def take_action(self, parsed_args): 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', @@ -192,8 +202,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' + ) kwargs = {} if parsed_args.name: @@ -230,7 +244,7 @@ def take_action(self, parsed_args): 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', @@ -245,8 +259,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 group in parsed_args.groups: @@ -277,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', @@ -298,8 +314,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' + ) domain = None if parsed_args.domain: @@ -307,25 +327,21 @@ def take_action(self, parsed_args): identity_client, parsed_args.domain ) - data = [] - if parsed_args.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, ) - if domain: - # NOTE(0weng): The API doesn't actually support filtering - # additionally by domain_id, so this doesn't really do - # anything. - data = identity_client.user_groups(user, domain_id=domain) - else: - data = identity_client.user_groups(user) - else: - if domain: - data = identity_client.groups(domain_id=domain) - else: - data = identity_client.groups() + # 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') @@ -348,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', @@ -368,8 +384,10 @@ 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) -> None: + identity_client = sdk_utils.ensure_service_version( + self.app.client_manager.sdk_connection.identity, '3' + ) group_id = common.find_group_id_sdk( identity_client, parsed_args.group, parsed_args.group_domain @@ -408,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', @@ -432,8 +450,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' + ) group = common.find_group_id_sdk( identity_client, parsed_args.group, parsed_args.domain ) @@ -449,7 +469,7 @@ def take_action(self, parsed_args): 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', @@ -463,8 +483,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' + ) if parsed_args.domain: domain = common.find_domain_id_sdk( diff --git a/openstackclient/identity/v3/identity_provider.py b/openstackclient/identity/v3/identity_provider.py index f1af03f05c..bf28a8dcfc 100644 --- a/openstackclient/identity/v3/identity_provider.py +++ b/openstackclient/identity/v3/identity_provider.py @@ -13,8 +13,13 @@ """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 import exceptions from osc_lib import utils @@ -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', @@ -98,26 +130,34 @@ def get_parser(self, prog_name): ) return parser - def take_action(self, parsed_args): - identity_client = self.app.client_manager.identity - remote_ids: list[str] | None = None + 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)) + kwargs['remote_ids'] = list(map(str.strip, remote_ids)) elif parsed_args.remote_ids: - remote_ids = 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,26 +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) - idp._info['remote_ids'] = format_columns.ListColumn( - idp._info.pop('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', @@ -156,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}, @@ -183,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, @@ -225,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', @@ -279,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: @@ -297,13 +335,14 @@ def take_action(self, parsed_args): 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 + 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 @@ -318,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 ) @@ -326,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', @@ -335,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 c1236ad019..4587c450e4 100644 --- a/openstackclient/identity/v3/implied_role.py +++ b/openstackclient/identity/v3/implied_role.py @@ -15,7 +15,10 @@ """Identity v3 Implied Role action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any from openstackclient import command @@ -25,7 +28,9 @@ 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 4671c5acb7..c4f1c6bbe1 100644 --- a/openstackclient/identity/v3/limit.py +++ b/openstackclient/identity/v3/limit.py @@ -13,8 +13,13 @@ """Limits action implementations.""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +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 @@ -25,7 +30,9 @@ LOG = logging.getLogger(__name__) -def _format_limit(limit): +def _format_limit( + limit: _limit.Limit, +) -> tuple[tuple[str, ...], Iterable[Any]]: columns = ( "description", "id", @@ -50,7 +57,7 @@ def _format_limit(limit): 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', @@ -68,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='', @@ -88,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' + ) kwargs = { "resource_name": parsed_args.resource_name, @@ -98,12 +110,12 @@ def take_action(self, parsed_args): if parsed_args.description: kwargs["description"] = parsed_args.description - # TODO(0weng): Add --project-domain option - # to support filtering project domain - kwargs["project_id"] = common_utils._find_sdk_id( - identity_client.find_project, - name_or_id=parsed_args.project, + kwargs["project_id"] = common_utils.find_project_id_sdk( + identity_client, + parsed_args.project, + domain_name_or_id=parsed_args.project_domain, ) + kwargs["service_id"] = common_utils.find_service_sdk( identity_client, parsed_args.service ).id @@ -121,7 +133,7 @@ def take_action(self, parsed_args): 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', @@ -144,28 +156,39 @@ 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.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 = {} + kwargs: dict[str, object] = {} if parsed_args.service: kwargs["service_id"] = common_utils.find_service_sdk( identity_client, parsed_args.service - ) + ).id if parsed_args.region: kwargs["region_id"] = identity_client.get_region( parsed_args.region ).id - # TODO(0weng): Add --project-domain option - # to support filtering project domain if 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, ) if parsed_args.resource_name: @@ -191,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', @@ -200,8 +223,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' + ) limit = identity_client.get_limit(parsed_args.limit_id) return _format_limit(limit) @@ -209,7 +236,7 @@ def take_action(self, parsed_args): 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', @@ -230,13 +257,17 @@ 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 = {} if parsed_args.description: kwargs["description"] = parsed_args.description - if parsed_args.resource_limit: + if parsed_args.resource_limit is not None: kwargs["resource_limit"] = parsed_args.resource_limit limit = identity_client.update_limit(parsed_args.limit_id, **kwargs) @@ -246,7 +277,7 @@ def take_action(self, parsed_args): 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', @@ -259,8 +290,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 limit_id in parsed_args.limit_id: diff --git a/openstackclient/identity/v3/mapping.py b/openstackclient/identity/v3/mapping.py index a041f19e58..00cb84a5eb 100644 --- a/openstackclient/identity/v3/mapping.py +++ b/openstackclient/identity/v3/mapping.py @@ -15,9 +15,14 @@ """Identity v3 federation mapping action implementations""" +import argparse +from collections.abc import Iterable, Sequence import json import logging +from typing import Any +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 @@ -28,10 +33,21 @@ 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 3554903952..238af13260 100644 --- a/openstackclient/identity/v3/policy.py +++ b/openstackclient/identity/v3/policy.py @@ -15,8 +15,13 @@ """Identity v3 Policy action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +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 @@ -27,10 +32,18 @@ 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 e70a8a5011..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 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', @@ -89,23 +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' + ) kwargs = {} + if parsed_args.properties: kwargs = parsed_args.properties.copy() + if 'is_domain' in kwargs.keys(): if kwargs['is_domain'].lower() == "true": kwargs['is_domain'] = True @@ -114,35 +132,55 @@ 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)) - options = {} if parsed_args.immutable is not None: - options['immutable'] = parsed_args.immutable + 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=parsed_args.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): @@ -155,7 +193,7 @@ class DeleteProject(command.Command): "regardless." ) - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( 'projects', @@ -170,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( @@ -210,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', @@ -265,82 +301,100 @@ def get_parser(self, prog_name): 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 - - kwargs['user'] = user_id + user = common.find_user_id_sdk( + identity_client, + parsed_args.user, + ) 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', @@ -391,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 = {} @@ -409,15 +461,56 @@ def take_action(self, parsed_args): kwargs['options'] = {'immutable': parsed_args.immutable} if parsed_args.properties: kwargs.update(parsed_args.properties) - tag.update_tags_in_args(parsed_args, project, kwargs) - 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', @@ -443,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 4882c9e9cb..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 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 e0afb4133f..f5890cb9ec 100644 --- a/openstackclient/identity/v3/registered_limit.py +++ b/openstackclient/identity/v3/registered_limit.py @@ -13,8 +13,13 @@ """Registered limits action implementations.""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +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 @@ -25,10 +30,35 @@ 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', @@ -63,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 = identity_client.create_registered_limit(**kwargs) - registered_limit._info.pop('links', None) - return zip(*sorted(registered_limit._info.items())) + 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_limits', metavar='', nargs="+", - help=_('Registered limit(s) 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_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 @@ -147,7 +173,7 @@ 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', @@ -169,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', @@ -222,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', @@ -272,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', @@ -325,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 3c580d6f8b..1875c56481 100644 --- a/openstackclient/identity/v3/role.py +++ b/openstackclient/identity/v3/role.py @@ -15,9 +15,14 @@ """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 openstack.identity.v3 import role as _role +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils @@ -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', @@ -83,9 +90,11 @@ def _add_identity_and_resource_options_to_parser(parser): 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 = ( common._find_sdk_id( identity_client.find_domain, @@ -102,7 +111,7 @@ def _find_user(): domain_id=domain_id, ) - def _find_group(): + def _find_group() -> Any: domain_id = ( common._find_sdk_id( identity_client.find_domain, @@ -119,7 +128,7 @@ def _find_group(): domain_id=domain_id, ) - def _find_project(): + def _find_project() -> Any: domain_id = ( common._find_sdk_id( identity_client.find_domain, @@ -180,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', @@ -191,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 @@ -278,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', @@ -303,10 +314,14 @@ 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'] = common._find_sdk_id( identity_client.find_domain, name_or_id=parsed_args.domain @@ -341,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', @@ -356,8 +371,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_id = None if parsed_args.domain: @@ -395,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', @@ -404,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[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( @@ -416,8 +437,10 @@ def take_action(self, parsed_args): 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 ), ) @@ -435,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', @@ -447,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 @@ -527,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', @@ -552,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: @@ -585,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', @@ -599,8 +626,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' + ) domain_id = None if parsed_args.domain: diff --git a/openstackclient/identity/v3/role_assignment.py b/openstackclient/identity/v3/role_assignment.py index 78c010f0e1..e0d702d074 100644 --- a/openstackclient/identity/v3/role_assignment.py +++ b/openstackclient/identity/v3/role_assignment.py @@ -13,13 +13,23 @@ """Identity v3 Assignment action implementations""" +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'] @@ -34,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 @@ -52,7 +62,7 @@ def _get_ids(attr): 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', @@ -117,8 +127,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 role_id = None @@ -151,6 +165,8 @@ def take_action(self, parsed_args): ) elif parsed_args.authuser: if auth_ref: + 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, @@ -185,6 +201,8 @@ def take_action(self, parsed_args): ) elif parsed_args.authproject: if auth_ref: + 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, @@ -233,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 53a706299e..3339843793 100644 --- a/openstackclient/identity/v3/service.py +++ b/openstackclient/identity/v3/service.py @@ -15,8 +15,13 @@ """Identity v3 Service action implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +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 @@ -24,11 +29,12 @@ 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 02aae66bf8..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 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 41493c9936..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', @@ -123,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_tags: - args['tags'] = sorted(set(obj.tags) - set(parsed_args.remove_tags)) - 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 05e374caf0..eaf1219c21 100644 --- a/openstackclient/identity/v3/token.py +++ b/openstackclient/identity/v3/token.py @@ -15,6 +15,10 @@ """Identity v3 Token action implementations""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib import exceptions from osc_lib import utils @@ -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', @@ -48,7 +52,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 # NOTE(stevemar): We want a list of role ids @@ -64,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', @@ -104,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, @@ -113,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', @@ -148,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: @@ -168,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): @@ -177,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( @@ -207,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', @@ -222,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 80808aa12b..a475b9bb12 100644 --- a/openstackclient/identity/v3/trust.py +++ b/openstackclient/identity/v3/trust.py @@ -13,11 +13,16 @@ """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 openstack.identity.v3 import trust as _trust +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils @@ -25,11 +30,12 @@ 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 @@ -203,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', @@ -213,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: @@ -245,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', @@ -275,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( @@ -357,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 = ( @@ -395,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', @@ -404,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 e1efc15cfa..9ac65a3359 100644 --- a/openstackclient/identity/v3/unscoped_saml.py +++ b/openstackclient/identity/v3/unscoped_saml.py @@ -17,6 +17,10 @@ the user can list domains and projects they are allowed to access, and request a scoped token.""" +import argparse +from collections.abc import Iterable +from typing import Any + from osc_lib import utils from openstackclient import command @@ -26,7 +30,9 @@ 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 bec1c620c5..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 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', @@ -60,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: @@ -91,7 +97,7 @@ def _get_options_for_user(identity_client, parsed_args): return options -def _add_user_options(parser): +def _add_user_options(parser: argparse.ArgumentParser) -> None: # Add additional user options parser.add_argument( @@ -191,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', @@ -250,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: @@ -333,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', @@ -348,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: @@ -393,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', @@ -437,10 +449,21 @@ def get_parser(self, prog_name): '--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: @@ -460,6 +483,15 @@ def take_action(self, parsed_args): 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( @@ -489,21 +521,28 @@ 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: if parsed_args.is_enabled is not None: - data = identity_client.users( - domain_id=domain, - is_enabled=enabled, + data = list( + identity_client.users( + domain_id=domain, + is_enabled=enabled, + **pagination_kwargs, + ) ) else: - data = identity_client.users( - domain_id=domain, + data = list( + identity_client.users( + domain_id=domain, + **pagination_kwargs, + ) ) # Column handling @@ -541,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', @@ -603,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) @@ -676,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', @@ -690,15 +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 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.identity) + 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: # @@ -740,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, @@ -750,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', @@ -764,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 0ea7eca710..569b782545 100644 --- a/openstackclient/image/v1/image.py +++ b/openstackclient/image/v1/image.py @@ -19,7 +19,8 @@ import logging import os import sys -import typing as ty +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 @@ -51,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', @@ -69,7 +70,7 @@ def _get_columns(item): class HumanReadableSizeColumn(cliff_columns.FormattableColumn[int]): - def human_readable(self): + def human_readable(self) -> str: """Return a formatted visibility string :rtype: @@ -83,7 +84,7 @@ def human_readable(self): class VisibilityColumn(cliff_columns.FormattableColumn[bool]): - def human_readable(self): + def human_readable(self) -> str: """Return a formatted visibility string :rtype: @@ -99,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", @@ -239,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 @@ -295,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 @@ -314,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: @@ -351,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", @@ -367,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: @@ -396,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( @@ -453,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 = {} @@ -498,7 +503,7 @@ def take_action(self, parsed_args): 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( images, attr=attr, @@ -527,7 +532,7 @@ 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", @@ -551,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 @@ -572,7 +577,7 @@ def take_action(self, parsed_args): 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", @@ -702,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 = {} @@ -758,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 @@ -785,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: @@ -819,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", @@ -834,15 +838,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[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[ty.Any]] - ] = { + formatters: dict[str, type[cliff_columns.FormattableColumn[Any]]] = { 'properties': format_columns.DictColumn, } if parsed_args.human_readable: diff --git a/openstackclient/image/v2/cache.py b/openstackclient/image/v2/cache.py index 952d9ed01b..7d7516f628 100644 --- a/openstackclient/image/v2/cache.py +++ b/openstackclient/image/v2/cache.py @@ -13,9 +13,12 @@ # 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 import exceptions from osc_lib import utils @@ -27,7 +30,7 @@ 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 = [] @@ -38,14 +41,14 @@ def _format_image_cache(cached_images): image_obj['state'] = 'cached' image_obj['last_accessed'] = ( datetime.datetime.fromtimestamp( - image['last_accessed'], tz=datetime.timezone.utc + image['last_accessed'], tz=datetime.UTC ) .replace(tzinfo=None) .isoformat() ) image_obj['last_modified'] = ( datetime.datetime.fromtimestamp( - image['last_modified'], tz=datetime.timezone.utc + image['last_modified'], tz=datetime.UTC ) .replace(tzinfo=None) .isoformat() @@ -70,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 @@ -111,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", @@ -121,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 @@ -151,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", @@ -161,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: @@ -190,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", @@ -210,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 cbb6d874de..e6062122cf 100644 --- a/openstackclient/image/v2/image.py +++ b/openstackclient/image/v2/image.py @@ -17,15 +17,17 @@ 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 @@ -73,11 +75,13 @@ 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 = [ @@ -105,25 +109,25 @@ def _format_image(image, human_readable=False): # 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: @@ -137,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( @@ -147,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 ... <&- @@ -166,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: @@ -177,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", @@ -195,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", @@ -241,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", @@ -256,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, @@ -287,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. @@ -440,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', @@ -483,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 @@ -592,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 @@ -622,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, } @@ -643,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, @@ -659,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 = _( @@ -673,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", @@ -696,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: @@ -732,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( @@ -861,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, @@ -885,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 @@ -962,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", @@ -972,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") @@ -998,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", @@ -1013,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, @@ -1034,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", @@ -1049,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( @@ -1071,7 +1094,7 @@ 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", @@ -1096,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, @@ -1120,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 @@ -1296,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): @@ -1316,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 @@ -1340,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, @@ -1421,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", @@ -1436,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( @@ -1445,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", @@ -1482,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, @@ -1501,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: @@ -1512,7 +1539,7 @@ 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 @@ -1569,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( @@ -1599,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( @@ -1619,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 @@ -1645,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( @@ -1719,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 " @@ -1755,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: @@ -1885,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): @@ -1893,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", @@ -1906,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 68848136ea..b31871dae8 100644 --- a/openstackclient/image/v2/info.py +++ b/openstackclient/image/v2/info.py @@ -11,6 +11,10 @@ # under the License. +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib.cli import format_columns from openstackclient import command @@ -20,7 +24,9 @@ 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 af30f718f9..0de82899a1 100644 --- a/openstackclient/image/v2/metadef_namespaces.py +++ b/openstackclient/image/v2/metadef_namespaces.py @@ -15,8 +15,12 @@ """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 import exceptions from osc_lib import utils @@ -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 d5cbec1cd9..2467e1d003 100644 --- a/openstackclient/image/v2/metadef_objects.py +++ b/openstackclient/image/v2/metadef_objects.py @@ -15,8 +15,12 @@ """Image V2 Action Implementations""" +import argparse +from collections.abc import Iterable, Sequence import logging +from typing import Any +from openstack.image.v2 import metadef_object as _metadef_object from osc_lib import exceptions from osc_lib import utils @@ -27,7 +31,9 @@ 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", @@ -131,13 +138,14 @@ 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 namespace = parsed_args.namespace if not parsed_args.objects: - return image_client.delete_all_metadef_objects(namespace) + image_client.delete_all_metadef_objects(namespace) + return result = 0 for obj in parsed_args.objects: @@ -166,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", @@ -175,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 @@ -198,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", @@ -216,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( @@ -236,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", @@ -254,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 @@ -274,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 3a923c5226..1f1667a026 100644 --- a/openstackclient/image/v2/metadef_properties.py +++ b/openstackclient/image/v2/metadef_properties.py @@ -12,9 +12,13 @@ # 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 openstack.image.v2 import metadef_property as _metadef_property from osc_lib import exceptions from osc_lib import utils @@ -25,10 +29,12 @@ 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", @@ -133,7 +142,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 if not parsed_args.properties: @@ -174,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", @@ -183,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'] @@ -202,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", @@ -230,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 @@ -267,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", @@ -281,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, @@ -289,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 4d3ee46689..da3f33762f 100644 --- a/openstackclient/image/v2/metadef_resource_type_association.py +++ b/openstackclient/image/v2/metadef_resource_type_association.py @@ -10,7 +10,10 @@ # 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 import exceptions from osc_lib import utils @@ -21,7 +24,7 @@ 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 88001b7e1c..4df71c950b 100644 --- a/openstackclient/image/v2/metadef_resource_types.py +++ b/openstackclient/image/v2/metadef_resource_types.py @@ -12,6 +12,10 @@ """Image V2 Action Implementations""" +import argparse +from collections.abc import Iterable, Sequence +from typing import Any + from osc_lib import utils from openstackclient import command @@ -21,7 +25,9 @@ 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 a0f1d35ded..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 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 bc110e0b12..01d5f0bf62 100644 --- a/openstackclient/network/common.py +++ b/openstackclient/network/common.py @@ -11,12 +11,12 @@ # under the License. # -import abc +import argparse +from collections.abc import Generator import contextlib import logging -import typing as ty +from typing import Any -from cliff import _argparse import openstack.exceptions from osc_lib.cli import parseractions from osc_lib import exceptions @@ -37,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 @@ -57,233 +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. - """ - - @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(): # type: ignore - 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, # type: ignore - parsed_args, - ) - elif self.is_nova_network: - return self.take_action_compute( - self.app.client_manager.compute, # type: ignore - 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. - """ - - resource: str - - 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. @@ -300,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: @@ -316,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', @@ -349,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 178c3afbf3..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 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,7 +156,7 @@ 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( @@ -162,10 +173,13 @@ def get_parser(self, prog_name): ), ) 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', @@ -185,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 ( @@ -210,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', @@ -236,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 @@ -261,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', @@ -271,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 @@ -285,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', @@ -304,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 8a38dab4d5..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 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,12 +158,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 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( @@ -182,7 +189,7 @@ def get_parser(self, prog_name): ), ) 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', @@ -194,9 +201,12 @@ def get_parser(self, prog_name): action='store_true', 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', @@ -222,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 ( @@ -244,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', @@ -273,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 @@ -294,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', @@ -304,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 24475852fb..8649baab7c 100644 --- a/openstackclient/network/v2/default_security_group_rule.py +++ b/openstackclient/network/v2/default_security_group_rule.py @@ -13,8 +13,14 @@ """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 import exceptions from osc_lib import 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,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]]: client = self.app.client_manager.network # Build the create attributes. attrs = {} @@ -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,7 +256,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 client = self.app.client_manager.network for r in parsed_args.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( @@ -334,7 +347,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 column_headers = ( 'ID', @@ -394,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', @@ -403,7 +418,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_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 76b91a0ecd..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,35 +205,49 @@ 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='', dest='networks', action='append', - help=self.enhance_help_neutron( - _( - "List only floating IP(s) with the specified network " - "(name or ID) " - "(repeat option to fiter on multiple networks)" - ) + help=_( + "List only floating IP(s) with the specified network " + "(name or ID) " + "(repeat option to fiter on multiple networks)" ), ) parser.add_argument( @@ -258,53 +255,42 @@ def update_parser_network(self, parser): metavar='', dest='ports', action='append', - help=self.enhance_help_neutron( - _( - "List only floating IP(s) with the specified port " - "(name or ID) " - "(repeat option to fiter on multiple ports)" - ) + 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 only floating IP(s) with the specified 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 only floating IP(s) with the specified floating IP " - "address" - ) + 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 only floating IP(s) with the specified 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 only floating IP(s) with the specified 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) @@ -313,31 +299,26 @@ def update_parser_network(self, parser): metavar='', dest='routers', action='append', - help=self.enhance_help_neutron( - _( - "List only floating IP(s) with the specified router " - "(name or ID) " - "(repeat option to fiter on multiple routers)" - ) + 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=self.enhance_help_neutron( - _("List additional fields in output") - ), + 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', @@ -373,7 +354,7 @@ def take_action_network(self, client, parsed_args): 'DNS Domain', ) - query = {} + query: dict[str, object] = {} if parsed_args.networks is not None: network_ids = [] @@ -403,12 +384,13 @@ def take_action_network(self, client, parsed_args): 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 + query['project_id'] = project_id if parsed_args.routers is not None: router_ids = [] @@ -418,11 +400,17 @@ def take_action_network(self, client, parsed_args): ).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 = [] @@ -438,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', @@ -512,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 @@ -548,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="", @@ -559,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, @@ -568,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', @@ -601,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 cf770c2cdc..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 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', @@ -286,10 +298,12 @@ def get_parser(self, prog_name): "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 = ( @@ -315,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) @@ -330,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, @@ -354,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', @@ -419,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, @@ -454,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', @@ -471,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 f78c7ec866..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 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', @@ -60,9 +66,12 @@ def get_parser(self, prog_name): ), ) 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 = ( @@ -81,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, @@ -106,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', @@ -115,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 742c263964..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 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', @@ -171,10 +180,13 @@ def get_parser(self, prog_name): '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', @@ -190,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 ( @@ -209,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', @@ -241,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: @@ -255,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', @@ -270,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 dde6ccd0cd..80b66be033 100644 --- a/openstackclient/network/v2/local_ip.py +++ b/openstackclient/network/v2/local_ip.py @@ -15,8 +15,12 @@ """Node Local IP action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any +from openstack.network.v2 import local_ip as _local_ip from osc_lib import exceptions from osc_lib import utils @@ -27,15 +31,19 @@ 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,7 +202,7 @@ 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( @@ -236,7 +246,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', @@ -262,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( @@ -301,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', @@ -311,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 123faa67ce..e41db1d6d5 100644 --- a/openstackclient/network/v2/local_ip_association.py +++ b/openstackclient/network/v2/local_ip_association.py @@ -15,8 +15,12 @@ """Node Local IP action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any +from openstack.network.v2 import local_ip_association as _local_ip_association from osc_lib import exceptions from osc_lib import utils @@ -27,7 +31,9 @@ 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( @@ -170,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[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 78a7ae9117..a3622d3377 100644 --- a/openstackclient/network/v2/ndp_proxy.py +++ b/openstackclient/network/v2/ndp_proxy.py @@ -15,8 +15,12 @@ """Router NDP proxy action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any +from openstack.network.v2 import ndp_proxy as _ndp_proxy from osc_lib import exceptions from osc_lib import utils @@ -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,7 +145,7 @@ 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', @@ -180,9 +188,10 @@ 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 - identity_client = self.app.client_manager.identity columns = ( 'id', @@ -212,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 @@ -239,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', @@ -259,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: @@ -276,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', @@ -285,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 c8e1f360a7..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[bool]): - def human_readable(self): + def human_readable(self) -> str: return 'UP' if self._value else 'DOWN' class RouterExternalColumn(cliff_columns.FormattableColumn[bool]): - def human_readable(self): + 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,14 @@ 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 @@ -425,81 +419,83 @@ 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 only external networks")), + help=_("List only external networks"), ) router_ext_group.add_argument( '--internal', action='store_true', - help=self.enhance_help_neutron(_("List only 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 only networks with the specified 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 only enabled networks")), + help=_("List only enabled networks"), ) admin_state_group.add_argument( '--disable', action='store_true', - help=self.enhance_help_neutron(_("List only disabled networks")), + help=_("List only disabled networks"), ) parser.add_argument( '--project', @@ -508,86 +504,81 @@ def update_parser_network(self, parser): "List only networks with the specified project (name or ID)" ), ) - identity_common.add_project_domain_option_to_parser( - parser, enhance_help=self.enhance_help_neutron + pvlan_group = parser.add_mutually_exclusive_group() + pvlan_group.add_argument( + '--pvlan', + action='store_true', + help=_("List only networks with PVLAN enabled"), ) + 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 only 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 only 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 only networks with the specified 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 only networks with the specified 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 only networks with the specified physical network " - "name" - ) + 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 only networks with the specified provider segment " - "ID (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 only networks hosted the specified 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', @@ -615,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', @@ -624,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, @@ -634,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 @@ -665,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 @@ -691,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, ( @@ -704,43 +704,15 @@ def take_action_network(self, client, parsed_args): columns, formatters=_formatters, ) - for s in data - ), - ) - - 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 + for s in client.networks(**args) ), ) -# 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', @@ -835,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 @@ -857,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="", @@ -868,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', @@ -894,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 806422b102..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 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[bool]): - def human_readable(self): + def human_readable(self) -> str: return ":-)" if self._value else "XXX" class AdminStateColumn(cliff_columns.FormattableColumn[bool]): - def human_readable(self): + 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( @@ -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 1107cb709d..a7781d3b0d 100644 --- a/openstackclient/network/v2/network_auto_allocated_topology.py +++ b/openstackclient/network/v2/network_auto_allocated_topology.py @@ -13,8 +13,14 @@ """Auto-allocated Topology Implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any +from openstack.network.v2 import ( + auto_allocated_topology as _auto_allocated_topology, +) from osc_lib import utils from openstackclient import command @@ -24,33 +30,37 @@ 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 99993f64e8..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 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 9792b010a3..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 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 bb3bf94a18..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 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 7a7c231dd7..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 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 ab620b8490..75c6f0be91 100644 --- a/openstackclient/network/v2/network_qos_policy.py +++ b/openstackclient/network/v2/network_qos_policy.py @@ -13,14 +13,18 @@ # License for the specific language governing permissions and limitations # under the License. +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.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 @@ -28,8 +32,8 @@ LOG = logging.getLogger(__name__) -class RulesColumn(cliff_columns.FormattableColumn[ty.Any]): - 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) @@ -38,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', } @@ -48,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 @@ -67,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") @@ -129,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( @@ -144,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', @@ -154,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 @@ -180,12 +188,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 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', @@ -207,9 +213,12 @@ def get_parser(self, prog_name): action='store_true', 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', @@ -225,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, @@ -240,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', @@ -284,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) @@ -297,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', @@ -306,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 b2ddd823ab..dee153285a 100644 --- a/openstackclient/network/v2/network_qos_rule.py +++ b/openstackclient/network/v2/network_qos_rule.py @@ -13,8 +13,18 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse +from collections.abc import Iterable, Sequence import itertools - +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 @@ -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 c79c5fe017..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. +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 200175ac94..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 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', @@ -263,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, ...] = ( @@ -288,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) @@ -312,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', @@ -343,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) @@ -366,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', @@ -375,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 c2e7becfa8..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 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', @@ -164,9 +173,12 @@ def get_parser(self, prog_name): '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 c8b9a0e4c4..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 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', @@ -342,9 +360,12 @@ def get_parser(self, prog_name): '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. @@ -358,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', @@ -402,18 +431,25 @@ def take_action(self, parsed_args): 'available', ) - display_props: tuple[ty.Any, ...] = 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: @@ -426,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', @@ -452,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. @@ -490,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', @@ -499,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 1433c097ae..8cd5115ef0 100644 --- a/openstackclient/network/v2/network_service_provider.py +++ b/openstackclient/network/v2/network_service_provider.py @@ -13,6 +13,10 @@ """Network Service Providers Implementation""" +import argparse +from collections.abc import Iterable +from typing import Any + from osc_lib import utils from openstackclient import command @@ -22,7 +26,9 @@ 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 974a997636..c644e88bc8 100644 --- a/openstackclient/network/v2/network_trunk.py +++ b/openstackclient/network/v2/network_trunk.py @@ -16,10 +16,16 @@ """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 @@ -28,6 +34,7 @@ from openstackclient import command from openstackclient.i18n import _ +from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) @@ -37,14 +44,14 @@ class AdminStateColumn(cliff_columns.FormattableColumn[bool]): - def human_readable(self): + 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,9 +94,12 @@ def get_parser(self, prog_name): identity_utils.add_project_owner_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]]: network_client = self.app.client_manager.network - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity + attrs = _get_attrs_for_trunk( network_client, identity_client, parsed_args ) @@ -104,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', @@ -114,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: network_client = self.app.client_manager.network result = 0 for trunk in parsed_args.trunk: @@ -145,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', @@ -155,7 +165,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, ...]]]: network_client = self.app.client_manager.network data = network_client.trunks() headers: tuple[str, ...] = ('ID', 'Name', 'Parent Port', 'Description') @@ -184,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)") @@ -220,9 +232,10 @@ 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 - identity_client = self.app.client_manager.identity + identity_client = self.app.client_manager.sdk_connection.identity + trunk_id = network_client.find_trunk( parsed_args.trunk, ignore_missing=False, @@ -255,14 +268,16 @@ 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): + 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, @@ -279,7 +294,7 @@ 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', @@ -289,7 +304,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, ...]]]: network_client = self.app.client_manager.network trunk_id = network_client.find_trunk( parsed_args.trunk, @@ -321,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', @@ -341,7 +358,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 attrs = _get_attrs_for_subports(network_client, parsed_args) trunk_id = network_client.find_trunk( @@ -357,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(network_client, identity_client, 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: @@ -387,17 +410,19 @@ def _get_attrs_for_trunk(network_client, identity_client, parsed_args): # "trunk set" command doesn't support setting project. if 'project' in parsed_args and parsed_args.project is not None: - 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(network_client, subports): +def _format_subports( + network_client: Any, subports: list[dict[str, Any]] +) -> list[dict[str, Any]]: attrs = [] for subport in subports: subport_attrs = {} @@ -424,7 +449,9 @@ def _format_subports(network_client, subports): return attrs -def _get_attrs_for_subports(network_client, 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(network_client, parsed_args.set_subports) diff --git a/openstackclient/network/v2/port.py b/openstackclient/network/v2/port.py index e1205153e6..ea0e00127f 100644 --- a/openstackclient/network/v2/port.py +++ b/openstackclient/network/v2/port.py @@ -14,12 +14,14 @@ """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 import exceptions @@ -27,6 +29,7 @@ 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,22 +38,22 @@ class AdminStateColumn(cliff_columns.FormattableColumn[bool]): - def human_readable(self): + 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='', @@ -338,20 +396,24 @@ def _add_updatable_args(parser, create=False): '--vnic-type', metavar='', choices=( + 'accelerator-direct', + 'accelerator-direct-physical', 'direct', 'direct-physical', 'macvtap', 'normal', 'baremetal', 'virtio-forwarder', + 'smart-nic', 'vdpa', 'remote-managed', ), help=_( - "VNIC type for this port (direct | direct-physical | " - "macvtap | normal | baremetal | virtio-forwarder | vdpa | " - "remote-managed) " - "(default: normal)" + "VNIC type for this port (accelerator-direct | " + "accelerator-direct-physical | direct | " + "direct-physical | macvtap | normal | baremetal | " + "virtio-forwarder | smart-nic | vdpa | " + "remote-managed) (default: normal)" ), ) parser.add_argument( @@ -430,11 +492,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 +527,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 +544,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 +561,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 +573,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,7 +719,9 @@ def get_parser(self, prog_name): _tag.add_tag_option_to_parser_for_create(parser, _('port')) 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 network = network_client.find_network( parsed_args.network, ignore_missing=False @@ -711,6 +797,8 @@ def take_action(self, parsed_args): self._parse_extra_properties(parsed_args.extra_properties) ) + _validate_pvlan_network_port(attrs, network) + with common.check_missing_extension_if_error(network_client, attrs): obj = network_client.create_port(**attrs) @@ -727,7 +815,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', @@ -737,7 +825,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 @@ -764,12 +852,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', @@ -823,6 +909,29 @@ def get_parser(self, prog_name): metavar='', 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='', @@ -865,11 +974,13 @@ def get_parser(self, prog_name): ), ) _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', @@ -887,6 +998,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'] @@ -894,7 +1013,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: @@ -923,11 +1041,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 @@ -937,11 +1056,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 @@ -961,12 +1095,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() @@ -1120,7 +1252,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) @@ -1209,6 +1341,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 @@ -1222,14 +1358,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) @@ -1237,12 +1375,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', @@ -1330,6 +1466,13 @@ def get_parser(self, prog_name): 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', @@ -1339,7 +1482,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 @@ -1400,6 +1543,8 @@ def take_action(self, parsed_args): 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 939167d850..2f21d8dc17 100644 --- a/openstackclient/network/v2/router.py +++ b/openstackclient/network/v2/router.py @@ -14,13 +14,15 @@ """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 import exceptions @@ -28,6 +30,7 @@ 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 @@ -36,25 +39,25 @@ class AdminStateColumn(cliff_columns.FormattableColumn[bool]): - def human_readable(self): + def human_readable(self) -> str: return 'UP' if self._value else 'DOWN' -class RouterInfoColumn(cliff_columns.FormattableColumn[ty.Any]): - 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[ty.Any]): - 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,19 +81,21 @@ 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') or item.evpn_vni is None: + 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): +def is_multiple_gateways_supported(n_client: Any) -> bool: return ( n_client.find_extension( "external-gateway-multihoming", ignore_missing=True @@ -97,7 +104,9 @@ def is_multiple_gateways_supported(n_client): ) -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 = _( @@ -109,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 = '' @@ -206,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: @@ -229,12 +242,12 @@ 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)) @@ -249,7 +262,11 @@ def _get_attrs(client_manager, parsed_args): 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 @@ -257,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', @@ -298,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 @@ -318,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', @@ -330,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', @@ -354,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', @@ -398,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') @@ -419,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', @@ -449,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') @@ -465,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") @@ -594,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) @@ -661,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', @@ -671,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 @@ -698,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', @@ -739,11 +803,13 @@ def get_parser(self, prog_name): ), ) _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, ...] = ( @@ -774,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) @@ -834,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: @@ -851,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', @@ -865,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', @@ -893,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', @@ -1051,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) @@ -1141,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', @@ -1150,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 = [] @@ -1177,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', @@ -1217,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) @@ -1272,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', @@ -1304,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 = _( @@ -1339,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', @@ -1371,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 c6930de78d..9f0b76c5fd 100644 --- a/openstackclient/network/v2/security_group.py +++ b/openstackclient/network/v2/security_group.py @@ -9,29 +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 -import typing as ty +from collections.abc import Iterable, Sequence +import logging +from typing import Any, cast from cliff import columns as cliff_columns +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: @@ -39,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[ty.Any]): - 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[ty.Any]): - 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', @@ -97,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") ) @@ -113,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( @@ -134,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 @@ -158,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) @@ -175,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='', @@ -210,18 +159,34 @@ 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', @@ -232,30 +197,17 @@ class ListSecurityGroup(common.NetworkAndComputeLister): 'shared', ] - 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, - ) - + 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 only security groups with the specified 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 - ) + identity_common.add_project_domain_option_to_parser(parser) shared_group = parser.add_mutually_exclusive_group() shared_group.add_argument( @@ -273,35 +225,32 @@ def update_parser_network(self, parser): help=_("List only security groups not shared between projects"), ) - _tag.add_tag_filtering_option_to_parser( - parser, _('security group'), enhance_help=self.enhance_help_neutron - ) - 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)") - ), - ) + _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( @@ -335,36 +284,12 @@ def take_action_network(self, client, parsed_args): ), ) - 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, - ) - 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( - s, - columns, - ) - for s in data - ), - ) - - -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='', @@ -391,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 ) @@ -423,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='', @@ -453,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) @@ -476,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', @@ -486,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 f6baac3cb7..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,131 +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 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))" - ) + 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 only rules with the specified 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 only 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 only 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"), ) parser.add_argument( '--project', metavar='', - help=self.enhance_help_neutron( - _("List only rules with the specified project (name or ID)") - ), - ) - identity_common.add_project_domain_option_to_parser( - parser, enhance_help=self.enhance_help_neutron + help=_("List only rules 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 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)") - ), - ) - 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, - ) - 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', @@ -472,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" @@ -518,15 +411,21 @@ def take_action_network(self, client, parsed_args): 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.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 - query['tenant_id'] = project_id + ) 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) for r in client.security_group_rules(**query) @@ -543,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="", @@ -601,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 ) @@ -613,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 3357f8930e..fd548a9541 100644 --- a/openstackclient/network/v2/subnet.py +++ b/openstackclient/network/v2/subnet.py @@ -13,11 +13,14 @@ """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 import exceptions @@ -25,6 +28,7 @@ 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[ty.Any]): - 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[ty.Any]): - 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[list[ty.Any]]): +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', @@ -564,12 +581,16 @@ def get_parser(self, prog_name): ), ) _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: @@ -581,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( @@ -603,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') @@ -645,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', @@ -707,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( @@ -743,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', @@ -752,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 ) @@ -764,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', @@ -827,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 399ce483f3..9da9a59bca 100644 --- a/openstackclient/network/v2/subnet_pool.py +++ b/openstackclient/network/v2/subnet_pool.py @@ -13,8 +13,12 @@ """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 import exceptions @@ -22,15 +26,17 @@ 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', @@ -325,12 +333,14 @@ def get_parser(self, prog_name): ), ) _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 @@ -342,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 @@ -355,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') @@ -389,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', @@ -440,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 @@ -465,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', @@ -474,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 @@ -487,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', @@ -497,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/tap_flow.py b/openstackclient/network/v2/taas/tap_flow.py index 206fbde7fa..9f29886a4f 100644 --- a/openstackclient/network/v2/taas/tap_flow.py +++ b/openstackclient/network/v2/taas/tap_flow.py @@ -12,8 +12,12 @@ # 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 @@ -22,8 +26,7 @@ from openstackclient import command from openstackclient.i18n import _ -from openstackclient.identity import common -from openstackclient.network.v2.taas import tap_service +from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) @@ -45,17 +48,27 @@ } -def _add_updatable_args(parser): +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): + 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) @@ -92,7 +105,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 = {} if parsed_args.name is not None: @@ -114,13 +129,15 @@ def take_action(self, parsed_args): 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: - attrs['project_id'] = common.find_project( - self.app.client_manager.identity, + 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 obj = client.create_tap_flow(**attrs) - display_columns, columns = tap_service._get_columns(obj) + display_columns, columns = _get_columns(obj) data = osc_utils.get_dict_properties(obj, columns) return display_columns, data @@ -128,21 +145,25 @@ def take_action(self, parsed_args): class ListTapFlow(command.Lister): _description = _("List tap flows.") - def get_parser(self, prog_name): + 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): + 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: - params['project_id'] = common.find_project( - self.app.client_manager.identity, + 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 + ) + 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 @@ -161,7 +182,7 @@ def take_action(self, parsed_args): class ShowTapFlow(command.ShowOne): _description = _("Show tap flow 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( TAP_FLOW, @@ -170,13 +191,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[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 = tap_service._get_columns(obj) + display_columns, columns = _get_columns(obj) data = osc_utils.get_dict_properties(obj, columns) return display_columns, data @@ -184,7 +207,7 @@ def take_action(self, parsed_args): class DeleteTapFlow(command.Command): _description = _("Delete a tap flow.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( TAP_FLOW, @@ -194,7 +217,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 fails = 0 for id_or_name in parsed_args.tap_flow: @@ -219,7 +242,7 @@ def take_action(self, parsed_args): class UpdateTapFlow(command.ShowOne): _description = _("Update a tap flow.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( TAP_FLOW, @@ -229,7 +252,9 @@ def get_parser(self, prog_name): _add_updatable_args(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 original_t_f = client.find_tap_flow( parsed_args.tap_flow, ignore_missing=False diff --git a/openstackclient/network/v2/taas/tap_mirror.py b/openstackclient/network/v2/taas/tap_mirror.py index 876109dc46..0111bf2ead 100644 --- a/openstackclient/network/v2/taas/tap_mirror.py +++ b/openstackclient/network/v2/taas/tap_mirror.py @@ -10,8 +10,12 @@ # 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 @@ -19,7 +23,7 @@ from openstackclient import command from openstackclient.i18n import _ -from openstackclient.identity import common +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 @@ -39,7 +43,9 @@ ] -def _get_columns(item): +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( @@ -50,7 +56,7 @@ def _get_columns(item): class CreateTapMirror(command.ShowOne): _description = _("Create a new tap mirror.") - def get_parser(self, prog_name): + 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) @@ -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]]: client = self.app.client_manager.network attrs = {} if parsed_args.name is not None: @@ -107,13 +115,15 @@ def take_action(self, parsed_args): 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: - attrs['project_id'] = common.find_project( - self.app.client_manager.identity, + 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 obj = client.create_tap_mirror(**attrs) - display_columns, columns = tap_service._get_columns(obj) + display_columns, columns = _get_columns(obj) data = osc_utils.get_dict_properties(obj, columns) return display_columns, data @@ -121,21 +131,25 @@ def take_action(self, parsed_args): class ListTapMirror(command.Lister): _description = _("List tap mirrors.") - def get_parser(self, prog_name): + 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): + 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: - params['project_id'] = common.find_project( - self.app.client_manager.identity, + 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 + ) + 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 @@ -149,7 +163,7 @@ def take_action(self, parsed_args): class ShowTapMirror(command.ShowOne): _description = _("Show tap mirror 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( TAP_MIRROR, @@ -158,13 +172,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[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 = tap_service._get_columns(obj) + display_columns, columns = _get_columns(obj) data = osc_utils.get_dict_properties(obj, columns) return display_columns, data @@ -172,7 +188,7 @@ def take_action(self, parsed_args): class DeleteTapMirror(command.Command): _description = _("Delete a tap mirror.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( TAP_MIRROR, @@ -182,7 +198,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 fails = 0 for id_or_name in parsed_args.tap_mirror: @@ -211,7 +227,7 @@ def take_action(self, parsed_args): class UpdateTapMirror(command.ShowOne): _description = _("Update a tap mirror.") - def get_parser(self, prog_name): + def get_parser(self, prog_name: str) -> argparse.ArgumentParser: parser = super().get_parser(prog_name) parser.add_argument( TAP_MIRROR, @@ -221,7 +237,9 @@ def get_parser(self, prog_name): tap_service._add_updatable_args(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 original_t_s = client.find_tap_mirror( parsed_args.tap_mirror, ignore_missing=False @@ -232,6 +250,6 @@ def take_action(self, parsed_args): if parsed_args.description is not None: attrs['description'] = parsed_args.description obj = client.update_tap_mirror(original_t_s, **attrs) - display_columns, columns = tap_service._get_columns(obj) + 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 index df27658f5d..8f2a7173fb 100644 --- a/openstackclient/network/v2/taas/tap_service.py +++ b/openstackclient/network/v2/taas/tap_service.py @@ -12,8 +12,12 @@ # 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 @@ -21,7 +25,7 @@ from openstackclient import command from openstackclient.i18n import _ -from openstackclient.identity import common +from openstackclient.identity import common as identity_common LOG = logging.getLogger(__name__) @@ -37,14 +41,16 @@ ] -def _add_updatable_args(parser): +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): +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( @@ -55,7 +61,7 @@ def _get_columns(item): class CreateTapService(command.ShowOne): _description = _("Create a new tap service.") - def get_parser(self, prog_name): + 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) @@ -68,7 +74,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 = {} if parsed_args.name is not None: @@ -81,11 +89,13 @@ def take_action(self, parsed_args): ).id attrs['port_id'] = port_id if 'project' in parsed_args and parsed_args.project is not None: - attrs['project_id'] = common.find_project( - self.app.client_manager.identity, + 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 obj = client.create_tap_service(**attrs) display_columns, columns = _get_columns(obj) data = osc_utils.get_dict_properties(obj, columns) @@ -95,21 +105,25 @@ def take_action(self, parsed_args): class ListTapService(command.Lister): _description = _("List tap services.") - def get_parser(self, prog_name): + 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): + 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: - params['project_id'] = common.find_project( - self.app.client_manager.identity, + 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 + ) + 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 @@ -123,7 +137,7 @@ def take_action(self, parsed_args): class ShowTapService(command.ShowOne): _description = _("Show tap 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( TAP_SERVICE, @@ -132,7 +146,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 id = client.find_tap_service( parsed_args.tap_service, ignore_missing=False @@ -146,7 +162,7 @@ def take_action(self, parsed_args): class DeleteTapService(command.Command): _description = _("Delete a tap 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( TAP_SERVICE, @@ -156,7 +172,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 fails = 0 for id_or_name in parsed_args.tap_service: @@ -185,7 +201,7 @@ def take_action(self, parsed_args): class UpdateTapService(command.ShowOne): _description = _("Update a tap 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( TAP_SERVICE, @@ -195,7 +211,9 @@ def get_parser(self, prog_name): _add_updatable_args(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 original_t_s = client.find_tap_service( parsed_args.tap_service, ignore_missing=False 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 199e5222fd..26e2a6aedc 100644 --- a/openstackclient/object/v1/account.py +++ b/openstackclient/object/v1/account.py @@ -13,6 +13,10 @@ """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 @@ -23,7 +27,7 @@ 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 b0f92c7619..0333d3ee4e 100644 --- a/openstackclient/object/v1/container.py +++ b/openstackclient/object/v1/container.py @@ -15,7 +15,10 @@ """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 @@ -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 e8ee0fc692..f3bc249a6b 100644 --- a/openstackclient/object/v1/object.py +++ b/openstackclient/object/v1/object.py @@ -15,7 +15,10 @@ """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 @@ -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 743ed2bc82..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 @@ -31,14 +33,21 @@ # 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): client_manager: clientmanager.ClientManager - def __init__(self): + def __init__(self) -> None: command_manager = commandmanager.CommandManager( 'openstack.cli', ignored_modules=IGNORED_MODULES ) @@ -59,7 +68,12 @@ def __init__(self): # about them warnings.filterwarnings('ignore', module='openstack') - def build_option_parser(self, description, version, argparse_kwargs=None): + 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 ) @@ -67,7 +81,7 @@ def build_option_parser(self, description, version, argparse_kwargs=None): 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 @@ -77,7 +91,7 @@ def _final_defaults(self): else: self._auth_type = 'password' - def _load_plugins(self): + def _load_plugins(self) -> None: """Load plugins via stevedore.""" # Loop through extensions to get API versions for mod in clientmanager.PLUGIN_MODULES: @@ -128,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 @@ -157,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 @@ -168,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_application_credential.py b/openstackclient/tests/functional/identity/v3/test_application_credential.py index 20315c4e76..129d82a61d 100644 --- a/openstackclient/tests/functional/identity/v3/test_application_credential.py +++ b/openstackclient/tests/functional/identity/v3/test_application_credential.py @@ -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_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 1255841afe..373f7a8ef9 100644 --- a/openstackclient/tests/functional/identity/v3/test_role_assignment.py +++ b/openstackclient/tests/functional/identity/v3/test_role_assignment.py @@ -79,14 +79,14 @@ def test_role_assignment_list_group_domain(self): ) raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + 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'--project {self.PROJECT_NAME} ' f'--group {group_name} --group-domain {domain_name_A} ' f'{role_name}', ) @@ -108,20 +108,20 @@ def test_role_assignment_list_domain(self): 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) @@ -141,14 +141,14 @@ def test_role_assignment_list_user_domain(self): ) raw_output = self.openstack( 'role add ' - f'--project {self.project_name} ' + 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'--project {self.PROJECT_NAME} ' f'--user {username} --user-domain {domain_name_A} ' f'{role_name}', ) @@ -214,20 +214,20 @@ def test_role_assignment_list_project(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}', ) 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) @@ -302,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}' @@ -310,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/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 32b60dfaac..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") @@ -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 07a7959167..2380c947d2 100644 --- a/openstackclient/tests/functional/volume/v3/test_volume.py +++ b/openstackclient/tests/functional/volume/v3/test_volume.py @@ -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 b84bb0368b..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,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 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() @@ -47,7 +51,7 @@ def test_volume_snapshot(self): cmd_output = self.openstack( 'volume snapshot create ' + '--volume ' - + self.VOLLY + + self.VOLUME_NAME + ' --description aaaa ' + '--property Alpha=a ' + name, @@ -83,7 +87,7 @@ def test_volume_snapshot(self): # 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] 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 index 046d1cb9ba..34fe1bbb42 100644 --- a/openstackclient/tests/unit/api/test_volume_v2.py +++ b/openstackclient/tests/unit/api/test_volume_v2.py @@ -17,7 +17,7 @@ from unittest import mock import uuid -from openstack.block_storage.v2 import _proxy +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 @@ -29,7 +29,10 @@ class TestConsistencyGroup(utils.TestCase): def setUp(self): super().setUp() - self.volume_sdk_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.volume_client = mock.Mock(spec=block_storage_v2.Proxy) def test_find_consistency_group_by_id(self): cg_id = uuid.uuid4().hex @@ -45,13 +48,13 @@ def test_find_consistency_group_by_id(self): 'volume_types': ['123456'], } } - self.volume_sdk_client.get.side_effect = [ + self.volume_client.get.side_effect = [ fakes.FakeResponse(data=data), ] - result = volume.find_consistency_group(self.volume_sdk_client, cg_id) + result = volume.find_consistency_group(self.volume_client, cg_id) - self.volume_sdk_client.get.assert_has_calls( + self.volume_client.get.assert_has_calls( [ mock.call(f'/consistencygroups/{cg_id}'), ] @@ -69,14 +72,14 @@ def test_find_consistency_group_by_name(self): } ], } - self.volume_sdk_client.get.side_effect = [ + 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_sdk_client, cg_name) + result = volume.find_consistency_group(self.volume_client, cg_name) - self.volume_sdk_client.get.assert_has_calls( + self.volume_client.get.assert_has_calls( [ mock.call(f'/consistencygroups/{cg_name}'), mock.call('/consistencygroups'), @@ -86,14 +89,14 @@ def test_find_consistency_group_by_name(self): def test_find_consistency_group_not_found(self): data = {'consistencygroups': []} - self.volume_sdk_client.get.side_effect = [ + 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_sdk_client, + self.volume_client, 'invalid-cg', ) @@ -111,7 +114,7 @@ def test_find_consistency_group_by_name_duplicate(self): }, ], } - self.volume_sdk_client.get.side_effect = [ + self.volume_client.get.side_effect = [ fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND), fakes.FakeResponse(data=data), ] @@ -119,6 +122,6 @@ def test_find_consistency_group_by_name_duplicate(self): self.assertRaises( osc_lib_exceptions.NotFound, volume.find_consistency_group, - self.volume_sdk_client, + 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 index d70f899334..5743d938bf 100644 --- a/openstackclient/tests/unit/api/test_volume_v3.py +++ b/openstackclient/tests/unit/api/test_volume_v3.py @@ -17,7 +17,7 @@ from unittest import mock import uuid -from openstack.block_storage.v3 import _proxy +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 @@ -29,7 +29,10 @@ class TestConsistencyGroup(utils.TestCase): def setUp(self): super().setUp() - self.volume_sdk_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.volume_client = mock.Mock(spec=block_storage_v3.Proxy) def test_find_consistency_group_by_id(self): cg_id = uuid.uuid4().hex @@ -45,13 +48,13 @@ def test_find_consistency_group_by_id(self): 'volume_types': ['123456'], } } - self.volume_sdk_client.get.side_effect = [ + self.volume_client.get.side_effect = [ fakes.FakeResponse(data=data), ] - result = volume.find_consistency_group(self.volume_sdk_client, cg_id) + result = volume.find_consistency_group(self.volume_client, cg_id) - self.volume_sdk_client.get.assert_has_calls( + self.volume_client.get.assert_has_calls( [ mock.call(f'/consistencygroups/{cg_id}'), ] @@ -69,14 +72,14 @@ def test_find_consistency_group_by_name(self): } ], } - self.volume_sdk_client.get.side_effect = [ + 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_sdk_client, cg_name) + result = volume.find_consistency_group(self.volume_client, cg_name) - self.volume_sdk_client.get.assert_has_calls( + self.volume_client.get.assert_has_calls( [ mock.call(f'/consistencygroups/{cg_name}'), mock.call('/consistencygroups'), @@ -86,14 +89,14 @@ def test_find_consistency_group_by_name(self): def test_find_consistency_group_not_found(self): data = {'consistencygroups': []} - self.volume_sdk_client.get.side_effect = [ + 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_sdk_client, + self.volume_client, 'invalid-cg', ) @@ -111,7 +114,7 @@ def test_find_consistency_group_by_name_duplicate(self): }, ], } - self.volume_sdk_client.get.side_effect = [ + self.volume_client.get.side_effect = [ fakes.FakeResponse(status_code=http.HTTPStatus.NOT_FOUND), fakes.FakeResponse(data=data), ] @@ -119,6 +122,6 @@ def test_find_consistency_group_by_name_duplicate(self): self.assertRaises( osc_lib_exceptions.NotFound, volume.find_consistency_group, - self.volume_sdk_client, + 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_extension.py b/openstackclient/tests/unit/common/test_extension.py index dd684312c1..8ae3da3f5b 100644 --- a/openstackclient/tests/unit/common/test_extension.py +++ b/openstackclient/tests/unit/common/test_extension.py @@ -11,6 +11,10 @@ # under the License. +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 from openstackclient.tests.unit.identity.v2_0 import fakes as identity_fakes @@ -40,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 @@ -102,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', @@ -120,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, ), ( @@ -149,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): @@ -169,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 = [ @@ -269,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): @@ -294,7 +325,7 @@ def setUp(self): self.cmd = extension.ShowExtension(self.app, None) - self.app.client_manager.network.find_extension.return_value = ( + self.network_client.find_extension.return_value = ( self.extension_details ) @@ -322,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_module.py b/openstackclient/tests/unit/common/test_module.py index 8396203681..5f5d17d681 100644 --- a/openstackclient/tests/unit/common/test_module.py +++ b/openstackclient/tests/unit/common/test_module.py @@ -22,42 +22,61 @@ class FakeModule: - def __init__(self, name, version): + def __init__(self, name): self.name = name - self.__version__ = version - # Workaround for openstacksdk case - self.version = mock.Mock() - self.version.__version__ = version # 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: FakeModule(module_name_1, module_version_1), - module_name_2: FakeModule(module_name_2, module_version_2), - module_name_3: FakeModule(module_name_3, module_version_3), - module_name_4: FakeModule(module_name_4, module_version_4), - module_name_5: 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): @@ -128,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() @@ -150,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 = [ @@ -177,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 7bfb2e7f2f..477ff76f30 100644 --- a/openstackclient/tests/unit/common/test_quota.py +++ b/openstackclient/tests/unit/common/test_quota.py @@ -68,6 +68,7 @@ class TestQuotaList(TestQuota): 'Networks', 'Ports', 'RBAC Policies', + 'Router Routes', 'Routers', 'Security Groups', 'Security Group Rules', @@ -80,6 +81,7 @@ class TestQuotaList(TestQuota): 'Backups', 'Backup Gigabytes', 'Gigabytes', + 'Groups', 'Per Volume Gigabytes', 'Snapshots', 'Volumes', @@ -133,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, @@ -148,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 = ( @@ -156,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, @@ -333,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', @@ -352,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, ] @@ -382,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 @@ -392,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', @@ -418,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', @@ -429,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', @@ -442,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), @@ -455,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, } @@ -524,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 ) @@ -581,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) @@ -729,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, @@ -748,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) @@ -827,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, @@ -846,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) @@ -903,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( @@ -946,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( @@ -966,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}, @@ -988,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 ) @@ -1032,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, ) @@ -1059,7 +1044,7 @@ def test_quota_show__missing_services(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_not_called() def test_quota_show__with_compute(self): @@ -1079,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): @@ -1096,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, ) @@ -1116,7 +1101,7 @@ 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, @@ -1155,7 +1140,7 @@ def test_quota_show__with_network_and_usage(self): self.assertEqual( [ - ('floating-ips', 30, 7, 20), + ('floating_ips', 30, 7, 20), ('health_monitors', 0, 0, 0), ('l7_policies', 0, 0, 0), ('listeners', 0, 0, 0), @@ -1164,9 +1149,10 @@ def test_quota_show__with_network_and_usage(self): ('pools', 0, 0, 0), ('ports', 0, 0, 0), ('rbac_policies', 0, 0, 0), + ('router_routes', 0, 0, 0), ('routers', 0, 0, 0), - ('secgroup-rules', 9, 5, 7), - ('secgroups', 0, 0, 0), + ('security_group_rules', 9, 5, 7), + ('security_groups', 0, 0, 0), ('subnet_pools', 0, 0, 0), ('subnets', 0, 0, 0), ], @@ -1174,7 +1160,7 @@ def test_quota_show__with_network_and_usage(self): ) 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=True, @@ -1197,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( @@ -1222,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, ) @@ -1231,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 = [ @@ -1243,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( @@ -1261,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) @@ -1287,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( @@ -1316,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): @@ -1339,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() @@ -1364,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 b68e76edc5..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', @@ -326,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 8d9d36ccd2..84918e91e7 100644 --- a/openstackclient/tests/unit/compute/v2/test_console.py +++ b/openstackclient/tests/unit/compute/v2/test_console.py @@ -22,7 +22,7 @@ from openstackclient.tests.unit import utils -class TestConsoleLog(compute_fakes.TestComputev2): +class TestConsoleLog(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -77,7 +77,7 @@ def test_show_lines(self): ) -class TestConsoleUrlShow(compute_fakes.TestComputev2): +class TestConsoleUrlShow(compute_fakes.TestCompute): def setUp(self): super().setUp() 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 25bc8eaa76..746436b62d 100644 --- a/openstackclient/tests/unit/compute/v2/test_flavor.py +++ b/openstackclient/tests/unit/compute/v2/test_flavor.py @@ -26,15 +26,7 @@ from openstackclient.tests.unit import utils as tests_utils -class TestFlavor(compute_fakes.TestComputev2): - def setUp(self): - super().setUp() - - self.projects_mock = self.identity_client.projects - self.projects_mock.reset_mock() - - -class TestFlavorCreate(TestFlavor): +class TestFlavorCreate(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -86,7 +78,7 @@ def setUp(self): self.flavor.vcpus, ) - 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) @@ -106,7 +98,6 @@ def test_flavor_create_default_options(self): 'id': None, 'ephemeral': 0, 'swap': 0, - 'rxtx_factor': 1.0, 'is_public': True, } @@ -395,8 +386,34 @@ 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') + + 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) -class TestFlavorDelete(TestFlavor): + self.assertRaises( + exceptions.CommandError, self.cmd.take_action, parsed_args + ) + + +class TestFlavorDelete(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -477,7 +494,7 @@ def test_multi_flavors_delete_with_exception(self): self.compute_client.delete_flavor.assert_has_calls(delete_calls) -class TestFlavorList(TestFlavor): +class TestFlavorList(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -494,7 +511,8 @@ def setUp(self): 'VCPUs', 'Is Public', ) - self.columns_long = self.columns + ( + self.columns_long = ( + *self.columns, 'Swap', 'RXTX Factor', 'Properties', @@ -742,7 +760,7 @@ def test_flavor_list_min_disk_min_ram(self): self.assertEqual(tuple(self.data), tuple(data)) -class TestFlavorSet(TestFlavor): +class TestFlavorSet(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -752,7 +770,8 @@ def setUp(self): self.project = sdk_fakes.generate_fake_resource(_project.Project) self.compute_client.find_flavor.return_value = self.flavor - 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): @@ -956,7 +975,7 @@ def test_flavor_set_description_using_name_pre_v255(self): ) -class TestFlavorShow(TestFlavor): +class TestFlavorShow(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -1067,7 +1086,7 @@ def test_private_flavor_show(self): self.assertCountEqual(data_with_project, data) -class TestFlavorUnset(TestFlavor): +class TestFlavorUnset(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -1077,7 +1096,7 @@ def setUp(self): self.project = sdk_fakes.generate_fake_resource(_project.Project) self.compute_client.find_flavor.return_value = self.flavor - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = self.project self.cmd = flavor.UnsetFlavor(self.app, None) @@ -1203,6 +1222,31 @@ def test_flavor_unset_with_unexist_flavor(self): exceptions.CommandError, self.cmd.take_action, parsed_args ) + def test_flavor_unset_project_deleted_project(self): + self.identity_sdk_client.find_project.side_effect = [ + sdk_exceptions.ResourceNotFound() + ] + + deleted_project_id = 'deleted-project-uuid' + arglist = [ + '--project', + deleted_project_id, + self.flavor.id, + ] + verifylist = [ + ('project', deleted_project_id), + ('flavor', self.flavor.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + result = self.cmd.take_action(parsed_args) + self.assertIsNone(result) + + self.compute_client.flavor_remove_tenant_access.assert_called_with( + self.flavor.id, + deleted_project_id, + ) + def test_flavor_unset_nothing(self): arglist = [ self.flavor.id, 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 89d4d459fe..7e75b3a5b2 100644 --- a/openstackclient/tests/unit/compute/v2/test_hypervisor_stats.py +++ b/openstackclient/tests/unit/compute/v2/test_hypervisor_stats.py @@ -53,7 +53,7 @@ def create_one_hypervisor_stats(attrs=None): return stats_info -class TestHypervisorStatsShow(compute_fakes.TestComputev2): +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 0597312510..16130b461a 100644 --- a/openstackclient/tests/unit/compute/v2/test_server.py +++ b/openstackclient/tests/unit/compute/v2/test_server.py @@ -20,6 +20,8 @@ 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 @@ -36,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): @@ -64,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() @@ -83,6 +84,41 @@ def setup_sdk_servers_mock(self, count): return servers +class TestAddressesColumn(test_utils.TestCommand): + def setUp(self): + super().setUp() + + self.addr_a = server.AddressesColumn( + {'demo-net': [{'addr': '10.0.0.144', 'version': 4}]} + ) + self.addr_b = server.AddressesColumn( + {'network1': [{'addr': '192.168.1.254', 'version': 4}]} + ) + + def test_sorting(self): + """Verify AddressesColumn objects can be sorted.""" + addresses = [ + self.addr_b, + self.addr_a, + ] + + self.assertEqual( + [self.addr_a, self.addr_b], + sorted(addresses), + ) + + def test_comparison(self): + """Verify AddressesColumn comparison uses human-readable values.""" + addr_a_dup = server.AddressesColumn( + {'demo-net': [{'addr': '10.0.0.144', 'version': 4}]} + ) + + self.assertLess(self.addr_a, self.addr_b) + self.assertGreater(self.addr_b, self.addr_a) + self.assertEqual(self.addr_a, addr_a_dup) + self.assertNotEqual(self.addr_a, self.addr_b) + + class TestServerAddFixedIP(TestServer): def setUp(self): super().setUp() @@ -341,61 +377,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, ): @@ -700,8 +682,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, @@ -1021,8 +1003,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, ) @@ -1118,7 +1100,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() @@ -1294,8 +1276,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) @@ -1360,6 +1342,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 @@ -1608,8 +1655,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', @@ -1728,7 +1775,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', @@ -2456,7 +2503,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', @@ -2474,8 +2521,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, @@ -2498,7 +2545,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', @@ -2516,8 +2563,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, @@ -2569,7 +2616,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='', @@ -2593,7 +2640,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,' @@ -2647,7 +2694,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, @@ -2729,7 +2776,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, @@ -2886,7 +2933,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', @@ -2919,8 +2966,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, @@ -2951,7 +2998,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', @@ -2983,8 +3030,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, @@ -3014,7 +3061,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', @@ -3046,8 +3093,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, @@ -3077,7 +3124,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', @@ -3111,8 +3158,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, @@ -3144,8 +3191,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', @@ -3179,8 +3226,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, @@ -3212,7 +3259,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', @@ -3254,8 +3301,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, @@ -3392,6 +3439,86 @@ def test_server_create_volume_boot_from_volume_conflict(self): ) self.compute_client.create_server.assert_not_called() + def test_server_create_delete_on_termination_without_boot_from_volume( + self, + ): + # Fail if --delete-on-termination is used without --boot-from-volume + arglist = [ + '--image', + self.image.id, + '--flavor', + self.flavor.id, + '--delete-on-termination', + self.server.name, + ] + verifylist = [ + ('image', self.image.id), + ('flavor', self.flavor.id), + ('delete_on_termination', True), + ('config_drive', False), + ('server_name', self.server.name), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + exc = self.assertRaises( + exceptions.CommandError, + self.cmd.take_action, + parsed_args, + ) + + self.assertIn( + '--delete-on-termination can only be used with --boot-from-volume', + str(exc), + ) + + self.compute_client.create_server.assert_not_called() + + def test_server_create_boot_from_volume_delete_on_termination(self): + arglist = [ + '--image', + self.image.id, + '--flavor', + self.flavor.id, + '--boot-from-volume', + '10', + '--delete-on-termination', + self.server.name, + ] + verifylist = [ + ('image', self.image.id), + ('flavor', self.flavor.id), + ('boot_from_volume', 10), + ('delete_on_termination', True), + ('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='', + 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': 'volume', + 'volume_size': 10, + 'delete_on_termination': True, + }, + ], + ) + + self.assertEqual(self.columns, columns) + self.assertEqual(self.datalist(), data) + def test_server_create_boot_from_volume_no_image(self): # Test --boot-from-volume option without --image or # --image-property. @@ -4372,7 +4499,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() @@ -4865,7 +4992,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) @@ -5329,7 +5456,7 @@ def test_server_list_long_with_host_status_v216(self): ] # 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, @@ -5560,7 +5687,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) @@ -5708,7 +5835,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) @@ -5860,7 +5987,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) @@ -5879,7 +6006,7 @@ def test_server_list_column_option(self): self.assertCountEqual(columns, set(columns)) -class TestServerAction(compute_fakes.TestComputev2): +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 @@ -7540,7 +7667,7 @@ def test_evacuate_with_wait_ok_shutoff(self, mock_wait_for_status): ) -class TestServerRemoveFixedIP(compute_fakes.TestComputev2): +class TestServerRemoveFixedIP(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -7569,7 +7696,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() @@ -7593,7 +7720,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) @@ -7620,7 +7747,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) @@ -7643,43 +7770,12 @@ 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): - 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): +class TestServerRemoveFloatingIP(network_fakes.TestNetworkV2): def setUp(self): super().setUp() @@ -7871,7 +7967,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() @@ -7896,15 +7992,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 = [ @@ -8079,7 +8173,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() @@ -8111,7 +8205,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() @@ -8149,7 +8243,7 @@ def test_migrate_confirm(self): ) -class TestServerConfirmMigration(compute_fakes.TestComputev2): +class TestServerConfirmMigration(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -8180,7 +8274,7 @@ def test_migration_confirm(self): self.assertIsNone(result) -class TestServerResizeRevert(compute_fakes.TestComputev2): +class TestServerResizeRevert(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -8212,7 +8306,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() @@ -8250,7 +8344,7 @@ def test_migrate_revert(self): ) -class TestServerRevertMigration(compute_fakes.TestComputev2): +class TestServerRevertMigration(compute_fakes.TestCompute): def setUp(self): super().setUp() @@ -8690,6 +8784,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): @@ -8898,7 +9035,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 @@ -9315,7 +9452,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() @@ -9493,6 +9630,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): diff --git a/openstackclient/tests/unit/compute/v2/test_server_backup.py b/openstackclient/tests/unit/compute/v2/test_server_backup.py index cc8acfb34f..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', 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 a47ea7298f..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() @@ -95,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() @@ -261,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/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 bb6a643743..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() 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 c42ed47315..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,14 +647,15 @@ 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, ): ... @@ -714,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 ) @@ -734,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 ) 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 3a3a80e4a5..31b2b1782b 100644 --- a/openstackclient/tests/unit/identity/v3/test_application_credential.py +++ b/openstackclient/tests/unit/identity/v3/test_application_credential.py @@ -30,7 +30,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestApplicationCredentialCreate(identity_fakes.TestIdentityv3): +class TestApplicationCredentialCreate(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -235,7 +235,7 @@ def test_application_credential_create_with_access_rules_file( self.assertEqual(self.data, data) -class TestApplicationCredentialDelete(identity_fakes.TestIdentityv3): +class TestApplicationCredentialDelete(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -316,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() @@ -399,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() 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 cc0593d1fe..0ae8b4335e 100644 --- a/openstackclient/tests/unit/identity/v3/test_domain.py +++ b/openstackclient/tests/unit/identity/v3/test_domain.py @@ -16,7 +16,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestDomainCreate(identity_fakes.TestIdentityv3): +class TestDomainCreate(identity_fakes.TestIdentity): columns = ( 'id', 'name', @@ -209,7 +209,7 @@ def test_domain_create_with_no_immutable(self): self.assertEqual(self.datalist, data) -class TestDomainDelete(identity_fakes.TestIdentityv3): +class TestDomainDelete(identity_fakes.TestIdentity): domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): @@ -239,7 +239,7 @@ def test_domain_delete(self): self.assertIsNone(result) -class TestDomainList(identity_fakes.TestIdentityv3): +class TestDomainList(identity_fakes.TestIdentity): domain = sdk_fakes.generate_fake_resource( resource_type=_domain.Domain, is_enabled=True ) @@ -313,7 +313,7 @@ def test_domain_list_with_option_enabled(self): self.assertEqual(self.datalist, tuple(data)) -class TestDomainSet(identity_fakes.TestIdentityv3): +class TestDomainSet(identity_fakes.TestIdentity): domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): @@ -478,7 +478,7 @@ def test_domain_set_no_immutable_option(self): self.assertIsNone(result) -class TestDomainShow(identity_fakes.TestIdentityv3): +class TestDomainShow(identity_fakes.TestIdentity): columns = ( 'id', 'name', diff --git a/openstackclient/tests/unit/identity/v3/test_endpoint.py b/openstackclient/tests/unit/identity/v3/test_endpoint.py index ba69317f5d..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() @@ -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 598402e070..d6867272c8 100644 --- a/openstackclient/tests/unit/identity/v3/test_group.py +++ b/openstackclient/tests/unit/identity/v3/test_group.py @@ -25,7 +25,7 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestGroupAddUser(identity_fakes.TestIdentityv3): +class TestGroupAddUser(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -103,11 +103,11 @@ 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}: {str(sdk_exc.ResourceNotFound())}" + 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(identity_fakes.TestIdentityv3): +class TestGroupCheckUser(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -156,7 +156,7 @@ def test_group_check_user_server_error(self): ) -class TestGroupCreate(identity_fakes.TestIdentityv3): +class TestGroupCreate(identity_fakes.TestIdentity): domain = sdk_fakes.generate_fake_resource(_domain.Domain) columns = ( @@ -300,7 +300,7 @@ def test_group_create_or_show_with_domain(self): self.assertEqual(datalist, data) -class TestGroupDelete(identity_fakes.TestIdentityv3): +class TestGroupDelete(identity_fakes.TestIdentity): domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): @@ -428,7 +428,7 @@ def test_delete_multi_groups_with_exception(self): ) -class TestGroupList(identity_fakes.TestIdentityv3): +class TestGroupList(identity_fakes.TestIdentity): domain = sdk_fakes.generate_fake_resource(_domain.Domain) columns = ( @@ -553,14 +553,7 @@ def test_group_list_user_domain(self): # containing the data to be listed. columns, data = self.cmd.take_action(parsed_args) - # Set expected values - kwargs = { - 'domain_id': self.domain.id, - } - - self.identity_sdk_client.user_groups.assert_called_with( - self.user.id, **kwargs - ) + self.identity_sdk_client.user_groups.assert_called_with(self.user.id) self.assertEqual(self.columns, columns) @@ -587,10 +580,7 @@ def test_group_list_long(self): self.identity_sdk_client.groups.assert_called_with() - long_columns = self.columns + ( - 'Domain ID', - 'Description', - ) + long_columns = (*self.columns, 'Domain ID', 'Description') datalist = ( ( self.group.id, @@ -609,7 +599,7 @@ def test_group_list_long(self): self.assertEqual(datalist, tuple(data)) -class TestGroupRemoveUser(identity_fakes.TestIdentityv3): +class TestGroupRemoveUser(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -687,11 +677,11 @@ 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}: {str(sdk_exc.ResourceNotFound())}" + 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(identity_fakes.TestIdentityv3): +class TestGroupSet(identity_fakes.TestIdentity): domain = sdk_fakes.generate_fake_resource(_domain.Domain) def setUp(self): @@ -780,7 +770,7 @@ def test_group_set_with_domain(self): self.assertIsNone(result) -class TestGroupShow(identity_fakes.TestIdentityv3): +class TestGroupShow(identity_fakes.TestIdentity): domain = sdk_fakes.generate_fake_resource(_domain.Domain) columns = ( diff --git a/openstackclient/tests/unit/identity/v3/test_identity_provider.py b/openstackclient/tests/unit/identity/v3/test_identity_provider.py index c65e947efa..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_ids', 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_ids', 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_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_ids', 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_ids', 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,20 +642,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', self.new_remote_id, ] verifylist = [ - ('identity_provider', identity_fakes.idp_id), + ('identity_provider', self.idp.id), ('description', None), ('enable', True), ('disable', False), @@ -680,9 +669,9 @@ def prepare(self): 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,35 +719,31 @@ 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_ids', None), @@ -765,15 +755,21 @@ 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_ids', None), @@ -781,23 +777,29 @@ def prepare(self): ] 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_ids', None), @@ -805,15 +807,15 @@ def prepare(self): ] 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_ids', None), @@ -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 a0c045e663..43467ed272 100644 --- a/openstackclient/tests/unit/identity/v3/test_limit.py +++ b/openstackclient/tests/unit/identity/v3/test_limit.py @@ -11,6 +11,7 @@ # under the License. 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 @@ -22,11 +23,14 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestLimitCreate(identity_fakes.TestIdentityv3): +class TestLimitCreate(identity_fakes.TestIdentity): def setUp(self): super().setUp() - self.project = sdk_fakes.generate_fake_resource(_project.Project) + 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) @@ -35,6 +39,7 @@ def setUp(self): 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.limit = sdk_fakes.generate_fake_resource( resource_type=_limit.Limit, @@ -116,6 +121,8 @@ def test_limit_create_with_options(self): arglist = [ '--project', self.project.id, + '--project-domain', + self.domain.name, '--service', self.service.id, '--resource-limit', @@ -128,6 +135,7 @@ def test_limit_create_with_options(self): ] verifylist = [ ('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), @@ -172,7 +180,7 @@ def test_limit_create_with_options(self): self.assertEqual(datalist, data) -class TestLimitDelete(identity_fakes.TestIdentityv3): +class TestLimitDelete(identity_fakes.TestIdentity): def setUp(self): super().setUp() self.cmd = limit.DeleteLimit(self.app, None) @@ -208,7 +216,7 @@ def test_limit_delete_with_exception(self): self.assertEqual('1 of 1 limits failed to delete.', str(e)) -class TestLimitShow(identity_fakes.TestIdentityv3): +class TestLimitShow(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -267,7 +275,7 @@ def test_limit_show(self): self.assertEqual(datalist, data) -class TestLimitSet(identity_fakes.TestIdentityv3): +class TestLimitSet(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -387,8 +395,114 @@ def test_limit_set_resource_limit(self): ) self.assertEqual(datalist, data) + def test_limit_set_resource_limit_zero(self): + resource_limit = 0 + 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), + limit.id, + ] + verifylist = [ + ('resource_limit', resource_limit), + ('limit_id', limit.id), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.update_limit.assert_called_with( + limit.id, + resource_limit=resource_limit, + ) + + collist = ( + 'description', + 'id', + 'project_id', + 'region_id', + 'resource_limit', + 'resource_name', + 'service_id', + ) + self.assertEqual(collist, columns) + datalist = ( + None, + limit.id, + self.project.id, + None, + resource_limit, + limit.resource_name, + self.service.id, + ) + self.assertEqual(datalist, data) + + def test_limit_set_without_resource_limit(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=20, + region_id=None, + ) -class TestLimitList(identity_fakes.TestIdentityv3): + self.identity_sdk_client.update_limit.return_value = limit + + arglist = [ + limit.id, + ] + verifylist = [ + ('limit_id', limit.id), + ] + + parsed_args = self.check_parser( + self.cmd, + arglist, + verifylist, + ) + + columns, data = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.update_limit.assert_called_once_with( + limit.id, + ) + + collist = ( + 'description', + 'id', + 'project_id', + 'region_id', + 'resource_limit', + 'resource_name', + 'service_id', + ) + self.assertEqual(collist, columns) + + datalist = ( + None, + limit.id, + self.project.id, + None, + 20, + limit.resource_name, + self.service.id, + ) + self.assertEqual(datalist, data) + + +class TestLimitList(identity_fakes.TestIdentity): def setUp(self): super().setUp() @@ -442,3 +556,44 @@ def test_limit_list(self): ), ) self.assertEqual(datalist, tuple(data)) + + def test_limit_list_with_service_filter(self): + self.identity_sdk_client.find_service.return_value = self.service + + arglist = [ + '--service', + self.service.name, + ] + verifylist = [ + ('service', self.service.name), + ] + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + columns, data = self.cmd.take_action(parsed_args) + + self.identity_sdk_client.limits.assert_called_with( + service_id=self.service.id, + ) + + collist = ( + 'ID', + 'Project ID', + 'Service ID', + 'Resource Name', + 'Resource Limit', + 'Description', + 'Region ID', + ) + self.assertEqual(collist, columns) + datalist = ( + ( + self.limit.id, + self.project.id, + self.service.id, + self.limit.resource_name, + self.limit.resource_limit, + None, + None, + ), + ) + self.assertEqual(datalist, tuple(data)) 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_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 065a65cb1f..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,39 +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), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('tags', []), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -88,53 +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'), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -145,33 +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), + ('domain', project.domain_id), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -184,63 +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), + ('domain', project.domain_id), ('enabled', True), - ('name', self.project.name), + ('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 = [ ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -253,29 +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 = [ ('enabled', False), - ('name', self.project.name), + ('name', project.name), ('parent', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -287,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 = [ + ('name', project.name), ('properties', {'fee': 'fi', 'fo': 'fum'}), - ('name', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -323,36 +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), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('tags', []), ('properties', {'is_domain': 'false'}), - ('name', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -364,33 +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), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('tags', []), ('properties', {'is_domain': 'true'}), - ('name', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -402,33 +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), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('tags', []), ('properties', {'is_domain': 'none'}), - ('name', self.project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -440,40 +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), + ('domain', project.domain_id), + ('parent', parent.name), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('tags', []), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -481,61 +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'), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -546,17 +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), + ('domain', project.domain_id), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', ['foo']), ] @@ -569,29 +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), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -604,31 +718,44 @@ def test_project_create_with_immutable_option(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], + 'name': project.name, + 'is_enabled': True, 'options': {'immutable': 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, + {'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', - self.project.name, + project.name, ] verifylist = [ ('immutable', False), ('description', None), ('enabled', True), - ('name', self.project.name), + ('name', project.name), ('parent', None), ('tags', []), ] @@ -641,36 +768,121 @@ def test_project_create_with_no_immutable_option(self): # Set expected values kwargs = { - 'name': self.project.name, - 'domain': None, - 'description': None, - 'enabled': True, - 'parent': None, - 'tags': [], + 'name': project.name, + 'is_enabled': True, 'options': {'immutable': 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) + 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': project.name, + 'is_enabled': True, + } + 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 + ) + + arglist = [ + project.name, + ] + verifylist = [ + ('or_show', False), + ('description', None), + ('enabled', True), + ('name', project.name), + ('parent', None), + ('tags', []), + ] + + parsed_args = self.check_parser(self.cmd, arglist, verifylist) + + 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, ] @@ -681,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 = [ @@ -702,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', @@ -746,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) @@ -760,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', ] @@ -778,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) @@ -794,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, @@ -802,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) @@ -810,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, @@ -826,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', @@ -852,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.projects_mock.list.assert_called_with(parent=self.parent.id) + 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.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', @@ -877,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) @@ -896,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, ) @@ -913,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') @@ -928,6 +1256,8 @@ 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) @@ -938,25 +1268,51 @@ def test_project_list_with_option_enabled(self): columns, data = self.cmd.take_action(parsed_args) kwargs = {'is_enabled': True} - self.projects_mock.list.assert_called_with(**kwargs) + 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] -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']} + 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) + + 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) @@ -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): @@ -1024,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): @@ -1047,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): @@ -1070,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): @@ -1097,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): @@ -1112,7 +1477,7 @@ def test_project_set_tags(self): ] verifylist = [ ('name', 'qwerty'), - ('domain', self.project.domain_id), + ('domain', self.domain.id), ('enabled', None), ('project', self.project.name), ('tags', ['foo']), @@ -1126,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): @@ -1149,7 +1514,9 @@ def test_project_remove_tags(self): 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): @@ -1173,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): @@ -1197,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' ) - self.projects_mock.get.return_value = self.project + 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.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 = ( @@ -1314,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 + ) + project = sdk_fakes.generate_fake_resource( + _project.Project, + **dict(self.project_kwargs_no_options, 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, '--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 = ( @@ -1380,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 = ( @@ -1448,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, @@ -1495,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 a120714ec6..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,41 +155,44 @@ 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_limits', [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_limits', ['fake-registered-limit-id'])] @@ -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 90b2d7121c..3c0d0a78e8 100644 --- a/openstackclient/tests/unit/identity/v3/test_role.py +++ b/openstackclient/tests/unit/identity/v3/test_role.py @@ -29,12 +29,12 @@ from openstackclient.tests.unit.identity.v3 import fakes as identity_fakes -class TestRoleInherited(identity_fakes.TestIdentityv3): +class TestRoleInherited(identity_fakes.TestIdentity): def _is_inheritance_testcase(self): return True -class TestRoleAdd(identity_fakes.TestIdentityv3): +class TestRoleAdd(identity_fakes.TestIdentity): def _is_inheritance_testcase(self): return False @@ -361,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) @@ -573,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() @@ -680,7 +680,7 @@ def test_delete_multi_roles_with_exception(self): ) -class TestRoleList(identity_fakes.TestIdentityv3): +class TestRoleList(identity_fakes.TestIdentity): columns = ( 'ID', 'Name', @@ -767,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 @@ -1313,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() @@ -1474,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 134ba5a0bc..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) @@ -777,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): @@ -840,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( @@ -1034,8 +1034,75 @@ def test_user_list_with_option_enabled(self): 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) + + 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) -class TestUserSet(identity_fakes.TestIdentityv3): + 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( @@ -1684,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) @@ -1716,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 ) @@ -1733,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 ) @@ -1758,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): @@ -1773,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 = { 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 2d870ff89e..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 @@ -591,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', @@ -632,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', 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 e6de9f2ebd..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 = ( @@ -946,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, ) @@ -966,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 @@ -982,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): @@ -994,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): @@ -1004,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} @@ -1046,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() @@ -1057,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) @@ -1109,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'} @@ -1169,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 @@ -1329,7 +1314,7 @@ def test_image_set_membership_accept_with_project_no_owner_change(self): self.image_client.update_image.assert_called() call_args = self.image_client.update_image.call_args if call_args: - args, kwargs = call_args + _args, kwargs = call_args self.assertNotIn('owner_id', kwargs) def test_image_set_membership_reject_with_project_no_owner_change(self): @@ -1366,7 +1351,7 @@ def test_image_set_membership_reject_with_project_no_owner_change(self): self.image_client.update_image.assert_called() call_args = self.image_client.update_image.call_args if call_args: - args, kwargs = call_args + _args, kwargs = call_args self.assertNotIn('owner_id', kwargs) def test_image_set_membership_pending_with_project_no_owner_change(self): @@ -1403,7 +1388,7 @@ def test_image_set_membership_pending_with_project_no_owner_change(self): self.image_client.update_image.assert_called() call_args = self.image_client.update_image.call_args if call_args: - args, kwargs = call_args + _args, kwargs = call_args self.assertNotIn('owner_id', kwargs) def test_image_set_options(self): @@ -1453,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', @@ -1823,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() @@ -1891,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() @@ -1994,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): @@ -2048,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', @@ -2085,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, ) @@ -2115,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, ) @@ -2253,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, ) @@ -2285,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, ) @@ -2320,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): @@ -2375,7 +2361,7 @@ def test_save_data_with_chunk_size(self): ) -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") @@ -2401,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/network/test_common.py b/openstackclient/tests/unit/network/test_common.py index cc84c8bdf0..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,81 +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() # noqa: O401 - self.network_client = self.app.client_manager.network # noqa: O401 - self.network_client.network_action.return_value = 'take_action_network' - - self.app.client_manager.compute = mock.Mock() # noqa: O401 - self.compute_client = self.app.client_manager.compute # noqa: O401 - self.compute_client.compute_action.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() 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 16bbfa5ea3..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 @@ -1962,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. @@ -2027,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. @@ -2096,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/test_osc_tap_flow.py b/openstackclient/tests/unit/network/v2/taas/test_tap_flow.py similarity index 85% rename from openstackclient/tests/unit/network/v2/taas/test_osc_tap_flow.py rename to openstackclient/tests/unit/network/v2/taas/test_tap_flow.py index 8e4f185c8d..8cd1c3f158 100644 --- a/openstackclient/tests/unit/network/v2/taas/test_osc_tap_flow.py +++ b/openstackclient/tests/unit/network/v2/taas/test_tap_flow.py @@ -77,13 +77,9 @@ def test_create_tap_flow(self): 'direction': 'BOTH', }, ) - self.app.client_manager.network.create_tap_flow.return_value = ( - fake_tap_flow - ) - self.app.client_manager.network.find_port.return_value = fake_port - self.app.client_manager.network.find_tap_service.return_value = ( - fake_tap_service - ) + 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'], @@ -103,8 +99,7 @@ def test_create_tap_flow(self): parsed_args = self.check_parser(self.cmd, arg_list, verify_list) columns, data = self.cmd.take_action(parsed_args) - mock_create_t_f = self.app.client_manager.network.create_tap_flow - mock_create_t_f.assert_called_once_with( + self.network_client.create_tap_flow.assert_called_once_with( **{ 'name': fake_tap_flow['name'], 'source_port': fake_tap_flow['source_port'], @@ -129,7 +124,7 @@ def test_list_tap_flows(self): fake_tap_flows = list( sdk_fakes.generate_fake_resources(_tap_flow.TapFlow, count=2) ) - self.app.client_manager.network.tap_flows.return_value = fake_tap_flows + self.network_client.tap_flows.return_value = fake_tap_flows arg_list = [] verify_list = [] @@ -137,7 +132,7 @@ def test_list_tap_flows(self): headers, data = self.cmd.take_action(parsed_args) - self.app.client_manager.network.tap_flows.assert_called_once() + self.network_client.tap_flows.assert_called_once() self.assertEqual(headers, list(headers_long)) self.assertCountEqual( list(data), @@ -151,7 +146,7 @@ def test_list_tap_flows(self): class TestDeleteTapFlow(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_flow.side_effect = ( + 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) @@ -171,8 +166,9 @@ def test_delete_tap_flow(self): result = self.cmd.take_action(parsed_args) - mock_delete_tap_flow = self.app.client_manager.network.delete_tap_flow - mock_delete_tap_flow.assert_called_once_with(fake_tap_flow['id']) + self.network_client.delete_tap_flow.assert_called_once_with( + fake_tap_flow['id'] + ) self.assertIsNone(result) @@ -190,7 +186,7 @@ class TestShowTapFlow(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_flow.side_effect = ( + 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) @@ -198,9 +194,7 @@ def setUp(self): def test_show_tap_flow(self): """Test Show tap flow.""" fake_tap_flow = sdk_fakes.generate_fake_resource(_tap_flow.TapFlow) - self.app.client_manager.network.get_tap_flow.return_value = ( - fake_tap_flow - ) + self.network_client.get_tap_flow.return_value = fake_tap_flow arg_list = [ fake_tap_flow['id'], ] @@ -212,7 +206,7 @@ def test_show_tap_flow(self): headers, data = self.cmd.take_action(parsed_args) - self.app.client_manager.network.get_tap_flow.assert_called_once_with( + self.network_client.get_tap_flow.assert_called_once_with( fake_tap_flow['id'] ) self.assertEqual(self.columns, headers) @@ -245,7 +239,7 @@ class TestUpdateTapFlow(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self.cmd = osc_tap_flow.UpdateTapFlow(self.app, None) - self.app.client_manager.network.find_tap_flow.side_effect = ( + self.network_client.find_tap_flow.side_effect = ( lambda name_or_id, ignore_missing: _tap_flow.TapFlow(id=name_or_id) ) @@ -255,9 +249,7 @@ def test_update_tap_flow(self): new_tap_flow = copy.deepcopy(fake_tap_flow) new_tap_flow['name'] = self._new_name - self.app.client_manager.network.update_tap_flow.return_value = ( - new_tap_flow - ) + self.network_client.update_tap_flow.return_value = new_tap_flow arg_list = [ fake_tap_flow['id'], @@ -270,7 +262,8 @@ def test_update_tap_flow(self): columns, data = self.cmd.take_action(parsed_args) attrs = {'name': self._new_name} - mock_update_t_f = self.app.client_manager.network.update_tap_flow - mock_update_t_f.assert_called_once_with(new_tap_flow['id'], **attrs) + 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_osc_tap_mirror.py b/openstackclient/tests/unit/network/v2/taas/test_tap_mirror.py similarity index 83% rename from openstackclient/tests/unit/network/v2/taas/test_osc_tap_mirror.py rename to openstackclient/tests/unit/network/v2/taas/test_tap_mirror.py index 10f3251c36..1d153b51e9 100644 --- a/openstackclient/tests/unit/network/v2/taas/test_osc_tap_mirror.py +++ b/openstackclient/tests/unit/network/v2/taas/test_tap_mirror.py @@ -64,11 +64,9 @@ def test_create_tap_mirror(self): fake_tap_mirror = sdk_fakes.generate_fake_resource( tap_mirror.TapMirror, **{'port_id': port_id, 'directions': 'IN=99'} ) - self.app.client_manager.network.create_tap_mirror.return_value = ( - fake_tap_mirror - ) - self.app.client_manager.network.find_port.return_value = fake_port - self.app.client_manager.network.find_tap_mirror.side_effect = ( + 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 = [ @@ -96,13 +94,10 @@ def test_create_tap_mirror(self): ] parsed_args = self.check_parser(self.cmd, arg_list, verify_list) - self.app.client_manager.network.find_tap_mirror.return_value = ( - fake_tap_mirror - ) + self.network_client.find_tap_mirror.return_value = fake_tap_mirror columns, data = self.cmd.take_action(parsed_args) - create_tap_m_mock = self.app.client_manager.network.create_tap_mirror - create_tap_m_mock.assert_called_once_with( + self.network_client.create_tap_mirror.assert_called_once_with( **{ 'name': fake_tap_mirror['name'], 'port_id': fake_tap_mirror['port_id'], @@ -128,9 +123,7 @@ def test_list_tap_mirror(self): fake_tap_mirrors = list( sdk_fakes.generate_fake_resources(tap_mirror.TapMirror, count=4) ) - self.app.client_manager.network.tap_mirrors.return_value = ( - fake_tap_mirrors - ) + self.network_client.tap_mirrors.return_value = fake_tap_mirrors arg_list = [] verify_list = [] @@ -139,7 +132,7 @@ def test_list_tap_mirror(self): headers, data = self.cmd.take_action(parsed_args) - self.app.client_manager.network.tap_mirrors.assert_called_once() + self.network_client.tap_mirrors.assert_called_once() self.assertEqual(headers, list(headers_long)) self.assertCountEqual( list(data), @@ -153,7 +146,7 @@ def test_list_tap_mirror(self): class TestDeleteTapMirror(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_mirror.side_effect = ( + self.network_client.find_tap_mirror.side_effect = ( lambda name_or_id, ignore_missing: tap_mirror.TapMirror( id=name_or_id ) @@ -177,8 +170,9 @@ def test_delete_tap_mirror(self): parsed_args = self.check_parser(self.cmd, arg_list, verify_list) result = self.cmd.take_action(parsed_args) - mock_delete_tap_m = self.app.client_manager.network.delete_tap_mirror - mock_delete_tap_m.assert_called_once_with(fake_tap_mirror['id']) + self.network_client.delete_tap_mirror.assert_called_once_with( + fake_tap_mirror['id'] + ) self.assertIsNone(result) @@ -196,7 +190,7 @@ class TestShowTapMirror(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_mirror.side_effect = ( + self.network_client.find_tap_mirror.side_effect = ( lambda name_or_id, ignore_missing: tap_mirror.TapMirror( id=name_or_id ) @@ -209,9 +203,7 @@ def test_show_tap_mirror(self): fake_tap_mirror = sdk_fakes.generate_fake_resource( tap_mirror.TapMirror ) - self.app.client_manager.network.get_tap_mirror.return_value = ( - fake_tap_mirror - ) + self.network_client.get_tap_mirror.return_value = fake_tap_mirror arg_list = [ fake_tap_mirror['id'], ] @@ -223,8 +215,9 @@ def test_show_tap_mirror(self): headers, data = self.cmd.take_action(parsed_args) - mock_get_tap_m = self.app.client_manager.network.get_tap_mirror - mock_get_tap_m.assert_called_once_with(fake_tap_mirror['id']) + 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] @@ -248,7 +241,7 @@ class TestUpdateTapMirror(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self.cmd = osc_tap_mirror.UpdateTapMirror(self.app, None) - self.app.client_manager.network.find_tap_mirror.side_effect = ( + self.network_client.find_tap_mirror.side_effect = ( lambda name_or_id, ignore_missing: tap_mirror.TapMirror( id=name_or_id ) @@ -262,9 +255,7 @@ def test_update_tap_mirror(self): new_tap_mirror = copy.deepcopy(fake_tap_mirror) new_tap_mirror['name'] = self._new_name - self.app.client_manager.network.update_tap_mirror.return_value = ( - new_tap_mirror - ) + self.network_client.update_tap_mirror.return_value = new_tap_mirror arg_list = [ fake_tap_mirror['id'], @@ -277,8 +268,7 @@ def test_update_tap_mirror(self): columns, data = self.cmd.take_action(parsed_args) attrs = {'name': self._new_name} - mock_update_tap_m = self.app.client_manager.network.update_tap_mirror - mock_update_tap_m.assert_called_once_with( + self.network_client.update_tap_mirror.assert_called_once_with( fake_tap_mirror['id'], **attrs ) self.assertEqual(self.columns, columns) diff --git a/openstackclient/tests/unit/network/v2/taas/test_osc_tap_service.py b/openstackclient/tests/unit/network/v2/taas/test_tap_service.py similarity index 82% rename from openstackclient/tests/unit/network/v2/taas/test_osc_tap_service.py rename to openstackclient/tests/unit/network/v2/taas/test_tap_service.py index fa766891ee..dac094f90f 100644 --- a/openstackclient/tests/unit/network/v2/taas/test_osc_tap_service.py +++ b/openstackclient/tests/unit/network/v2/taas/test_tap_service.py @@ -65,11 +65,9 @@ def test_create_tap_service(self): fake_tap_service = sdk_fakes.generate_fake_resource( tap_service.TapService, **{'port_id': port_id} ) - self.app.client_manager.network.create_tap_service.return_value = ( - fake_tap_service - ) - self.app.client_manager.network.find_port.return_value = fake_port - self.app.client_manager.network.find_tap_service.side_effect = ( + 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 = [ @@ -85,13 +83,10 @@ def test_create_tap_service(self): ] parsed_args = self.check_parser(self.cmd, arg_list, verify_list) - self.app.client_manager.network.find_tap_service.return_value = ( - fake_tap_service - ) + self.network_client.find_tap_service.return_value = fake_tap_service columns, data = self.cmd.take_action(parsed_args) - create_tap_s_mock = self.app.client_manager.network.create_tap_service - create_tap_s_mock.assert_called_once_with( + self.network_client.create_tap_service.assert_called_once_with( **{ 'name': fake_tap_service['name'], 'port_id': fake_tap_service['port_id'], @@ -114,9 +109,7 @@ def test_list_tap_service(self): fake_tap_services = list( sdk_fakes.generate_fake_resources(tap_service.TapService, count=4) ) - self.app.client_manager.network.tap_services.return_value = ( - fake_tap_services - ) + self.network_client.tap_services.return_value = fake_tap_services arg_list = [] verify_list = [] @@ -125,7 +118,7 @@ def test_list_tap_service(self): headers, data = self.cmd.take_action(parsed_args) - self.app.client_manager.network.tap_services.assert_called_once() + self.network_client.tap_services.assert_called_once() self.assertEqual(headers, list(headers_long)) self.assertCountEqual( list(data), @@ -139,7 +132,7 @@ def test_list_tap_service(self): class TestDeleteTapService(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_service.side_effect = ( + self.network_client.find_tap_service.side_effect = ( lambda name_or_id, ignore_missing: tap_service.TapService( id=name_or_id ) @@ -163,8 +156,9 @@ def test_delete_tap_service(self): parsed_args = self.check_parser(self.cmd, arg_list, verify_list) result = self.cmd.take_action(parsed_args) - mock_delete_tap_s = self.app.client_manager.network.delete_tap_service - mock_delete_tap_s.assert_called_once_with(fake_tap_service['id']) + self.network_client.delete_tap_service.assert_called_once_with( + fake_tap_service['id'] + ) self.assertIsNone(result) @@ -180,7 +174,7 @@ class TestShowTapService(network_fakes.TestNetworkV2): def setUp(self): super().setUp() - self.app.client_manager.network.find_tap_service.side_effect = ( + self.network_client.find_tap_service.side_effect = ( lambda name_or_id, ignore_missing: tap_service.TapService( id=name_or_id ) @@ -193,9 +187,7 @@ def test_show_tap_service(self): fake_tap_service = sdk_fakes.generate_fake_resource( tap_service.TapService ) - self.app.client_manager.network.get_tap_service.return_value = ( - fake_tap_service - ) + self.network_client.get_tap_service.return_value = fake_tap_service arg_list = [ fake_tap_service['id'], ] @@ -207,8 +199,9 @@ def test_show_tap_service(self): headers, data = self.cmd.take_action(parsed_args) - mock_get_tap_s = self.app.client_manager.network.get_tap_service - mock_get_tap_s.assert_called_once_with(fake_tap_service['id']) + 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] @@ -231,7 +224,7 @@ class TestUpdateTapService(network_fakes.TestNetworkV2): def setUp(self): super().setUp() self.cmd = osc_tap_service.UpdateTapService(self.app, None) - self.app.client_manager.network.find_tap_service.side_effect = ( + self.network_client.find_tap_service.side_effect = ( lambda name_or_id, ignore_missing: tap_service.TapService( id=name_or_id ) @@ -245,9 +238,7 @@ def test_update_tap_service(self): new_tap_service = copy.deepcopy(fake_tap_service) new_tap_service['name'] = self._new_name - self.app.client_manager.network.update_tap_service.return_value = ( - new_tap_service - ) + self.network_client.update_tap_service.return_value = new_tap_service arg_list = [ fake_tap_service['id'], @@ -260,8 +251,7 @@ def test_update_tap_service(self): columns, data = self.cmd.take_action(parsed_args) attrs = {'name': self._new_name} - mock_update_tap_s = self.app.client_manager.network.update_tap_service - mock_update_tap_s.assert_called_once_with( + self.network_client.update_tap_service.assert_called_once_with( fake_tap_service['id'], **attrs ) self.assertEqual(self.columns, columns) diff --git a/openstackclient/tests/unit/network/v2/test_address_group.py b/openstackclient/tests/unit/network/v2/test_address_group.py index 48f706631f..11d9a2605a 100644 --- a/openstackclient/tests/unit/network/v2/test_address_group.py +++ b/openstackclient/tests/unit/network/v2/test_address_group.py @@ -9,31 +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.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={ @@ -64,8 +55,7 @@ def setUp(self): # 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 = [] @@ -138,7 +128,7 @@ 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) @@ -224,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 = ( @@ -264,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', @@ -282,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, @@ -301,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, @@ -322,7 +336,7 @@ 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() @@ -419,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 = ( @@ -477,7 +491,7 @@ 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() @@ -519,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) @@ -539,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 6e2c05ed9c..4f13ef8bed 100644 --- a/openstackclient/tests/unit/network/v2/test_address_scope.py +++ b/openstackclient/tests/unit/network/v2/test_address_scope.py @@ -9,14 +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.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 @@ -25,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={ @@ -58,8 +54,7 @@ def setUp(self): # 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 = [] @@ -280,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', @@ -315,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, @@ -334,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, 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 c44e553c76..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,7 +9,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# from unittest.mock import call import uuid 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 95% rename from openstackclient/tests/unit/network/v2/test_floating_ip_network.py rename to openstackclient/tests/unit/network/v2/test_floating_ip.py index ab0ec176a0..6c224d1317 100644 --- a/openstackclient/tests/unit/network/v2/test_floating_ip_network.py +++ b/openstackclient/tests/unit/network/v2/test_floating_ip.py @@ -9,17 +9,17 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# 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 @@ -28,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. @@ -176,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, @@ -201,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, @@ -383,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, @@ -425,7 +420,8 @@ class TestListFloatingIPNetwork(TestFloatingIPNetwork): 'Floating Network', 'Project', ) - columns_long = columns + ( + columns_long = ( + *columns, 'Router', 'Status', 'Description', @@ -607,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, @@ -627,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, @@ -699,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): 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 33b9011c65..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.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, @@ -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, @@ -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, @@ -513,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', @@ -553,7 +578,7 @@ 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, } @@ -675,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, } diff --git a/openstackclient/tests/unit/network/v2/test_ip_availability.py b/openstackclient/tests/unit/network/v2/test_ip_availability.py index def3e17da0..31e9186b80 100644 --- a/openstackclient/tests/unit/network/v2/test_ip_availability.py +++ b/openstackclient/tests/unit/network/v2/test_ip_availability.py @@ -9,29 +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 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', @@ -53,11 +44,13 @@ class TestListIPAvailability(TestIPAvailability): def setUp(self): super().setUp() - self.cmd = ip_availability.ListIPAvailability(self.app, None) + 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 = [] @@ -73,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', @@ -107,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} 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 0769e2e561..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,8 +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 osc_lib import exceptions @@ -155,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 ) @@ -168,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, @@ -178,7 +176,9 @@ def setUp(self): ct_helper.port, ) ) - self.network_client.conntrack_helpers.return_value = ct_helpers + self.network_client.conntrack_helpers.return_value = ( + self.conntrack_helpers + ) # Get the command object to test self.cmd = l3_conntrack_helper.ListConntrackHelper(self.app, None) @@ -201,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): diff --git a/openstackclient/tests/unit/network/v2/test_local_ip.py b/openstackclient/tests/unit/network/v2/test_local_ip.py index 585fec767c..58b6bc56cf 100644 --- a/openstackclient/tests/unit/network/v2/test_local_ip.py +++ b/openstackclient/tests/unit/network/v2/test_local_ip.py @@ -11,31 +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.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. @@ -85,8 +76,7 @@ def setUp(self): # 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, [], []) @@ -140,7 +130,7 @@ 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) @@ -221,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'}) @@ -288,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, @@ -307,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, @@ -389,7 +379,7 @@ 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() @@ -441,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 = ( 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 9efdc295f1..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,7 +26,6 @@ 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.return_value = self.fixed_port diff --git a/openstackclient/tests/unit/network/v2/test_ndp_proxy.py b/openstackclient/tests/unit/network/v2/test_ndp_proxy.py index 0fe8740da5..e84de82aca 100644 --- a/openstackclient/tests/unit/network/v2/test_ndp_proxy.py +++ b/openstackclient/tests/unit/network/v2/test_ndp_proxy.py @@ -9,14 +9,14 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# from 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 @@ -24,10 +24,6 @@ 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.return_value = self.router @@ -290,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, @@ -309,8 +305,8 @@ def test_ndp_proxy_list_project(self): 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, diff --git a/openstackclient/tests/unit/network/v2/test_network.py b/openstackclient/tests/unit/network/v2/test_network.py index 1e923d053f..d36652aec7 100644 --- a/openstackclient/tests/unit/network/v2/test_network.py +++ b/openstackclient/tests/unit/network/v2/test_network.py @@ -9,36 +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.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={ @@ -70,6 +58,7 @@ class TestCreateNetworkIdentityV3(TestNetwork): 'provider:network_type', 'provider:physical_network', 'provider:segmentation_id', + 'pvlan', 'qos_policy_id', 'router:external', 'shared', @@ -99,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, @@ -122,8 +112,7 @@ def setUp(self): # 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.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): @@ -189,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.", @@ -210,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.'), @@ -235,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.', } @@ -326,167 +318,38 @@ def test_create_with_vlan_qinq_and_transparency_enabled(self): exceptions.CommandError, self.cmd.take_action, parsed_args ) - def test_create_with_provider_segment_without_provider_type(self): + def test_create_with_pvlan_and_port_security_disabled(self): arglist = [ - "--provider-segment", - "123", + "--disable-port-security", + "--pvlan", self._network.name, ] - verifylist = [ - ('provider_network_type', None), - ('segmentation_id', "123"), - ('name', self._network.name), - ] + verifylist = [('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 ) - -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.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) - - # 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): - arglist = [ - "--project", - self.project.name, - self._network.name, - ] - verifylist = [ - ('enable', True), - ('share', None), - ('name', self._network.name), - ('project', self.project.name), - ('external', False), - ] - - 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.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() @@ -574,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', @@ -598,7 +461,7 @@ class TestListNetwork(TestNetwork): ) data = [] - for net in _network: + for net in _networks: data.append( ( net.id, @@ -608,7 +471,7 @@ class TestListNetwork(TestNetwork): ) data_long = [] - for net in _network: + for net in _networks: data_long.append( ( net.id, @@ -631,13 +494,13 @@ def setUp(self): # Get the command object to test self.cmd = network.ListNetwork(self.app, None) - self.network_client.networks.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.return_value = ( - self._network + self._networks ) # TestListTagMixin @@ -660,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', ] @@ -681,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', ] @@ -717,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', @@ -770,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, @@ -790,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, @@ -865,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, @@ -886,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, @@ -907,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, @@ -927,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 = [ @@ -976,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( @@ -996,6 +915,23 @@ def setUp(self): # 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, @@ -1166,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 = ( @@ -1189,6 +1125,7 @@ class TestShowNetwork(TestNetwork): 'provider:network_type', 'provider:physical_network', 'provider:segmentation_id', + 'pvlan', 'qos_policy_id', 'router:external', 'shared', @@ -1218,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, @@ -1265,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( diff --git a/openstackclient/tests/unit/network/v2/test_network_agent.py b/openstackclient/tests/unit/network/v2/test_network_agent.py index 48b394d7a5..a7cae620f6 100644 --- a/openstackclient/tests/unit/network/v2/test_network_agent.py +++ b/openstackclient/tests/unit/network/v2/test_network_agent.py @@ -9,7 +9,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# from unittest.mock import call @@ -109,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) @@ -249,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', @@ -340,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))) @@ -450,6 +498,66 @@ 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() @@ -541,8 +649,9 @@ class TestShowNetworkAgent(TestNetworkAgent): 'configuration', 'created_at', 'description', - 'host', + 'ha_chassis_priority', 'ha_state', + 'host', 'id', 'last_heartbeat_at', 'resources_synced', @@ -558,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, @@ -601,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 d13bd8cf95..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,20 +13,15 @@ # License for the specific language governing permissions and limitations # under the License. +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( @@ -130,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( @@ -167,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 @@ -185,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 @@ -206,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( 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 10038e3933..20aa2d1393 100644 --- a/openstackclient/tests/unit/network/v2/test_network_flavor.py +++ b/openstackclient/tests/unit/network/v2/test_network_flavor.py @@ -12,29 +12,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 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() @@ -71,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 = ( @@ -107,8 +98,7 @@ def setUp(self): # 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 = [] @@ -210,7 +200,7 @@ 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) @@ -295,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 = ( @@ -335,8 +325,32 @@ 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() @@ -377,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 = ( @@ -440,7 +454,7 @@ 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() 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 c8235bfef8..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 = ( @@ -190,7 +180,7 @@ 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) @@ -287,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) @@ -332,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 = ( @@ -380,7 +394,7 @@ 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() diff --git a/openstackclient/tests/unit/network/v2/test_network_meter.py b/openstackclient/tests/unit/network/v2/test_network_meter.py index f13839a626..008bcee8b3 100644 --- a/openstackclient/tests/unit/network/v2/test_network_meter.py +++ b/openstackclient/tests/unit/network/v2/test_network_meter.py @@ -15,26 +15,19 @@ 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', @@ -56,7 +49,7 @@ def setUp(self): super().setUp() self.network_client.create_metering_label.return_value = self.new_meter - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = self.project self.cmd = network_meter.CreateMeter(self.app, None) def test_create_no_options(self): @@ -124,7 +117,7 @@ def test_create_all_options(self): self.assertEqual(self.data, data) -class TestDeleteMeter(TestMeter): +class TestDeleteMeter(network_fakes.TestNetworkV2): def setUp(self): super().setUp() @@ -209,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 = ( @@ -250,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) + + 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(TestMeter): +class TestShowMeter(network_fakes.TestNetworkV2): new_meter = network_fakes.FakeNetworkMeter.create_one_meter() columns = ( 'description', 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 407fb04bb4..4212bd336a 100644 --- a/openstackclient/tests/unit/network/v2/test_network_meter_rule.py +++ b/openstackclient/tests/unit/network/v2/test_network_meter_rule.py @@ -15,26 +15,19 @@ 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 = ( @@ -63,14 +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.return_value = ( self.new_rule ) + self.identity_sdk_client.find_project.return_value = self.project + self.network_client.find_metering_label.return_value = fake_meter - self.projects_mock.get.return_value = self.project self.cmd = network_meter_rule.CreateMeterRule(self.app, None) - self.network_client.find_metering_label.return_value = fake_meter def test_create_no_options(self): arglist = [] @@ -138,7 +130,7 @@ 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( @@ -225,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 = ( @@ -271,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 = ( 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 17f40ef6b9..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( @@ -70,7 +69,7 @@ def setUp(self): # 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 = [] @@ -280,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', @@ -314,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, 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 4300304022..3448589fa2 100644 --- a/openstackclient/tests/unit/network/v2/test_network_qos_rule.py +++ b/openstackclient/tests/unit/network/v2/test_network_qos_rule.py @@ -226,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, @@ -613,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) 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 1c50b9e938..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,18 +13,12 @@ # License for the specific language governing permissions and limitations # under the License. - 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) @@ -70,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) @@ -126,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 d3a7192453..2ff86fe8b1 100644 --- a/openstackclient/tests/unit/network/v2/test_network_rbac.py +++ b/openstackclient/tests/unit/network/v2/test_network_rbac.py @@ -14,10 +14,11 @@ 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 @@ -26,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): @@ -38,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, @@ -85,7 +83,7 @@ def setUp(self): self.network_client.find_address_group.return_value = self.ag_object - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = self.project def test_network_rbac_create_no_type(self): arglist = [ @@ -224,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( **{ @@ -454,8 +452,8 @@ def setUp(self): 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 = [] @@ -469,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', @@ -541,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} ) @@ -555,7 +577,7 @@ def setUp(self): self.network_client.find_rbac_policy.return_value = self.rbac_policy self.network_client.update_rbac_policy.return_value = None - self.projects_mock.get.return_value = self.project + self.identity_sdk_client.find_project.return_value = self.project def test_network_rbac_set_nothing(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 ab71c32547..bd6324c980 100644 --- a/openstackclient/tests/unit/network/v2/test_network_segment.py +++ b/openstackclient/tests/unit/network/v2/test_network_segment.py @@ -9,7 +9,6 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# from unittest.mock import call @@ -250,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: @@ -333,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. 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 9c9c900e36..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() @@ -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,7 +338,7 @@ 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() @@ -434,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 ) @@ -450,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: @@ -518,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', @@ -538,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={ @@ -628,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() 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 f84bcd38d7..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,19 +13,13 @@ # License for the specific language governing permissions and limitations # under the License. - 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 diff --git a/openstackclient/tests/unit/network/v2/test_network_trunk.py b/openstackclient/tests/unit/network/v2/test_network_trunk.py index 1056c21c30..d1177565bd 100644 --- a/openstackclient/tests/unit/network/v2/test_network_trunk.py +++ b/openstackclient/tests/unit/network/v2/test_network_trunk.py @@ -13,31 +13,21 @@ import copy 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']} @@ -83,19 +73,17 @@ class TestCreateNetworkTrunk(TestNetworkTrunk): def setUp(self): super().setUp() - self.network_client.create_trunk.return_value = self.new_trunk + 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, ] - # Get the command object to test self.cmd = network_trunk.CreateNetworkTrunk(self.app, None) - self.projects_mock.get.return_value = self.project - self.domains_mock.get.return_value = self.domain - def test_create_no_options(self): arglist = [] verifylist = [] @@ -286,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']} @@ -312,19 +300,19 @@ class TestDeleteNetworkTrunk(TestNetworkTrunk): def setUp(self): super().setUp() + 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.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.DeleteNetworkTrunk(self.app, None) @@ -386,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 = ( @@ -419,8 +407,8 @@ def setUp(self): 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) @@ -455,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( { @@ -468,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'])) @@ -491,8 +479,8 @@ def setUp(self): super().setUp() 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) @@ -524,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']} @@ -577,8 +565,8 @@ def setUp(self): self.sub_port, ] - 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.SetNetworkTrunk(self.app, None) @@ -815,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'] @@ -863,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']} diff --git a/openstackclient/tests/unit/network/v2/test_port.py b/openstackclient/tests/unit/network/v2/test_port.py index 11a8711c7b..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, @@ -207,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'), @@ -224,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) @@ -247,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', } ) @@ -1156,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. @@ -1235,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']} @@ -1349,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', @@ -1665,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, @@ -1687,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, @@ -1786,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', @@ -2653,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. diff --git a/openstackclient/tests/unit/network/v2/test_router.py b/openstackclient/tests/unit/network/v2/test_router.py index 6ebb7809eb..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}) @@ -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}) @@ -89,6 +79,7 @@ def setUp(self): 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) + + 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(TestRouter): +class TestCreateRouter(network_fakes.TestNetworkV2): # The new router created. new_router = network_fakes.create_one_router() _extensions = {'fake': network_fakes.create_one_extension()} @@ -616,8 +646,152 @@ 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 = [] + + 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(TestRouter): +class TestDeleteRouter(network_fakes.TestNetworkV2): # The routers to delete. _routers = network_fakes.create_routers(count=2) @@ -697,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() @@ -711,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: @@ -810,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 @@ -824,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) @@ -920,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, @@ -939,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, @@ -1027,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}) @@ -1067,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}) @@ -1111,12 +1306,12 @@ 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): @@ -1206,7 +1401,7 @@ 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): @@ -1296,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() @@ -1782,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( @@ -1900,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) @@ -1922,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() @@ -2135,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 = [] 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 92% rename from openstackclient/tests/unit/network/v2/test_security_group_network.py rename to openstackclient/tests/unit/network/v2/test_security_group.py index c5ccf628ee..734462b70e 100644 --- a/openstackclient/tests/unit/network/v2/test_security_group_network.py +++ b/openstackclient/tests/unit/network/v2/test_security_group.py @@ -13,27 +13,19 @@ 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() @@ -72,8 +64,7 @@ def setUp(self): self._security_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 self.network_client.set_tags.return_value = None # Get the command object to test @@ -177,7 +168,7 @@ 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() @@ -261,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) @@ -299,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) @@ -312,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, @@ -352,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, @@ -409,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']} @@ -509,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() @@ -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']} 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 96% 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 920e891413..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,15 +9,16 @@ # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. -# 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 @@ -26,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 @@ -101,8 +97,7 @@ def setUp(self): 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), ] @@ -1153,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) @@ -1167,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,9 +1237,9 @@ def test_list_with_wrong_egress(self): self.assertEqual(self.expected_data_no_group, list(data)) def test_list_with_project(self): - project = identity_fakes.FakeProject.create_one_project() + project = sdk_fakes.generate_fake_resource(_project.Project) self._security_group_rule_tcp.port_range_min = 80 - self.projects_mock.get.return_value = project + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', @@ -1265,7 +1251,7 @@ def test_list_with_project(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id, 'project_id': project.id} + filters = {'project_id': project.id} self.network_client.security_group_rules.assert_called_once_with( **filters @@ -1274,9 +1260,9 @@ def test_list_with_project(self): self.assertEqual(self.expected_data_no_group, list(data)) def test_list_with_project_domain(self): - project = identity_fakes.FakeProject.create_one_project() + project = sdk_fakes.generate_fake_resource(_project.Project) self._security_group_rule_tcp.port_range_min = 80 - self.projects_mock.get.return_value = project + self.identity_sdk_client.find_project.return_value = project arglist = [ '--project', @@ -1291,7 +1277,7 @@ def test_list_with_project_domain(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - filters = {'tenant_id': project.id, 'project_id': project.id} + filters = {'project_id': project.id} self.network_client.security_group_rules.assert_called_once_with( **filters 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 e59168e517..dd3f9ad6be 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet.py +++ b/openstackclient/tests/unit/network/v2/test_subnet.py @@ -13,11 +13,13 @@ 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 @@ -26,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={ @@ -261,8 +258,7 @@ 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.return_value = self._subnet @@ -790,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', @@ -798,7 +794,8 @@ class TestListSubnet(TestSubnet): 'Network', 'Subnet', ) - columns_long = columns + ( + columns_long = ( + *columns, 'Project', 'DHCP', 'Name Servers', @@ -811,7 +808,7 @@ class TestListSubnet(TestSubnet): ) data = [] - for subnet in _subnet: + for subnet in _subnets: data.append( ( subnet.id, @@ -822,7 +819,7 @@ class TestListSubnet(TestSubnet): ) data_long = [] - for subnet in _subnet: + for subnet in _subnets: data_long.append( ( subnet.id, @@ -847,7 +844,7 @@ def setUp(self): # Get the command object to test self.cmd = subnet_v2.ListSubnet(self.app, None) - self.network_client.subnets.return_value = self._subnet + self.network_client.subnets.return_value = self._subnets def test_subnet_list_no_options(self): arglist = [] @@ -877,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', @@ -943,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, @@ -988,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, diff --git a/openstackclient/tests/unit/network/v2/test_subnet_pool.py b/openstackclient/tests/unit/network/v2/test_subnet_pool.py index 013550ec1e..0d0043d285 100644 --- a/openstackclient/tests/unit/network/v2/test_subnet_pool.py +++ b/openstackclient/tests/unit/network/v2/test_subnet_pool.py @@ -12,28 +12,20 @@ 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() @@ -86,8 +78,7 @@ def setUp(self): 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 = [] @@ -378,7 +369,7 @@ 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) @@ -462,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) @@ -471,7 +462,8 @@ class TestListSubnetPool(TestSubnetPool): 'Name', 'Prefixes', ) - columns_long = columns + ( + columns_long = ( + *columns, 'Default Prefix Length', 'Address Scope', 'Default Subnet Pool', @@ -540,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', @@ -605,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, @@ -624,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, @@ -716,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']} @@ -1029,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() @@ -1105,7 +1121,7 @@ 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( 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 614fa9a518..0000000000 --- a/openstackclient/tests/unit/volume/test_find_resource.py +++ /dev/null @@ -1,76 +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 - -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, '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.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, '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.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 e230a39a9a..836c67c398 100644 --- a/openstackclient/tests/unit/volume/v2/test_service.py +++ b/openstackclient/tests/unit/volume/v2/test_service.py @@ -27,7 +27,7 @@ def setUp(self): super().setUp() self.service = sdk_fakes.generate_fake_resource(_service.Service) - self.volume_sdk_client.services.return_value = [self.service] + self.volume_client.services.return_value = [self.service] self.cmd = service.ListService(self.app, None) @@ -66,7 +66,7 @@ def test_service_list(self): ) self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - self.volume_sdk_client.services.assert_called_with( + self.volume_client.services.assert_called_with( host=self.service.host, binary=self.service.binary, ) @@ -110,7 +110,7 @@ def test_service_list_with_long_option(self): ) self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - self.volume_sdk_client.services.assert_called_with( + self.volume_client.services.assert_called_with( host=self.service.host, binary=self.service.binary, ) @@ -123,7 +123,7 @@ def setUp(self): 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_sdk_client.find_service.return_value = self.service + self.volume_client.find_service.return_value = self.service self.cmd = service.SetService(self.app, None) @@ -158,7 +158,7 @@ def test_service_set_enable(self): result = self.cmd.take_action(parsed_args) - self.service.enable.assert_called_with(self.volume_sdk_client) + self.service.enable.assert_called_with(self.volume_client) self.service.disable.assert_not_called() self.assertIsNone(result) @@ -179,7 +179,7 @@ def test_service_set_disable(self): self.service.enable.assert_not_called() self.service.disable.assert_called_with( - self.volume_sdk_client, reason=None + self.volume_client, reason=None ) self.assertIsNone(result) @@ -204,7 +204,7 @@ def test_service_set_disable_with_reason(self): self.service.enable.assert_not_called() self.service.disable.assert_called_with( - self.volume_sdk_client, reason=reason + self.volume_client, reason=reason ) self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v2/test_volume.py b/openstackclient/tests/unit/volume/v2/test_volume.py index b68020fa95..fac09dc6a0 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume.py +++ b/openstackclient/tests/unit/volume/v2/test_volume.py @@ -15,44 +15,23 @@ 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.api import volume_v2 -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 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() - - class TestVolumeCreate(volume_fakes.TestVolume): columns = ( 'attachments', @@ -87,7 +66,7 @@ def setUp(self): super().setUp() self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.create_volume.return_value = self.volume + self.volume_client.create_volume.return_value = self.volume self.datalist = ( self.volume.attachments, @@ -132,7 +111,7 @@ def test_volume_create_min_options(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=None, @@ -184,7 +163,7 @@ def test_volume_create_options(self): ) as mock_find_cg: columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -198,7 +177,7 @@ def test_volume_create_options(self): scheduler_hints={'k': 'v'}, ) mock_find_cg.assert_called_once_with( - self.volume_sdk_client, consistency_group_id + self.volume_client, consistency_group_id ) self.assertEqual(self.columns, columns) @@ -223,7 +202,7 @@ def test_volume_create_properties(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -260,7 +239,7 @@ def test_volume_create_image(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -282,7 +261,7 @@ def test_volume_create_image(self): def test_volume_create_with_snapshot(self): snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) - self.volume_sdk_client.find_snapshot.return_value = snapshot + self.volume_client.find_snapshot.return_value = snapshot arglist = [ '--snapshot', @@ -297,7 +276,7 @@ def test_volume_create_with_snapshot(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=snapshot.size, snapshot_id=snapshot.id, name=self.volume.name, @@ -310,7 +289,7 @@ def test_volume_create_with_snapshot(self): consistency_group_id=None, scheduler_hints=None, ) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( snapshot.id, ignore_missing=False ) @@ -319,7 +298,7 @@ def test_volume_create_with_snapshot(self): def test_volume_create_with_source_volume(self): source_volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.find_volume.return_value = source_volume + self.volume_client.find_volume.return_value = source_volume arglist = [ '--source', @@ -334,7 +313,7 @@ def test_volume_create_with_source_volume(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=source_volume.size, snapshot_id=None, name=self.volume.name, @@ -347,7 +326,7 @@ def test_volume_create_with_source_volume(self): consistency_group_id=None, scheduler_hints=None, ) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( source_volume.id, ignore_missing=False ) @@ -374,7 +353,7 @@ def test_volume_create_with_bootable_and_readonly(self, mock_wait): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -387,10 +366,10 @@ def test_volume_create_with_bootable_and_readonly(self, mock_wait): consistency_group_id=None, scheduler_hints=None, ) - self.volume_sdk_client.set_volume_bootable_status.assert_called_once_with( + self.volume_client.set_volume_bootable_status.assert_called_once_with( self.volume, True ) - self.volume_sdk_client.set_volume_readonly.assert_called_once_with( + self.volume_client.set_volume_readonly.assert_called_once_with( self.volume, True ) @@ -417,7 +396,7 @@ def test_volume_create_with_nonbootable_and_readwrite(self, mock_wait): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -430,10 +409,10 @@ def test_volume_create_with_nonbootable_and_readwrite(self, mock_wait): consistency_group_id=None, scheduler_hints=None, ) - self.volume_sdk_client.set_volume_bootable_status.assert_called_once_with( + self.volume_client.set_volume_bootable_status.assert_called_once_with( self.volume, False ) - self.volume_sdk_client.set_volume_readonly.assert_called_once_with( + self.volume_client.set_volume_readonly.assert_called_once_with( self.volume, False ) @@ -445,10 +424,10 @@ def test_volume_create_with_nonbootable_and_readwrite(self, mock_wait): def test_volume_create_with_bootable_and_readonly_fail( self, mock_wait, mock_error ): - self.volume_sdk_client.set_volume_bootable_status.side_effect = ( + self.volume_client.set_volume_bootable_status.side_effect = ( sdk_exceptions.NotFoundException('foo') ) - self.volume_sdk_client.set_volume_readonly.side_effect = ( + self.volume_client.set_volume_readonly.side_effect = ( sdk_exceptions.NotFoundException('foo') ) @@ -470,7 +449,7 @@ def test_volume_create_with_bootable_and_readonly_fail( columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -483,10 +462,10 @@ def test_volume_create_with_bootable_and_readonly_fail( consistency_group_id=None, scheduler_hints=None, ) - self.volume_sdk_client.set_volume_bootable_status.assert_called_once_with( + self.volume_client.set_volume_bootable_status.assert_called_once_with( self.volume, True ) - self.volume_sdk_client.set_volume_readonly.assert_called_once_with( + self.volume_client.set_volume_readonly.assert_called_once_with( self.volume, True ) @@ -517,7 +496,7 @@ def test_volume_create_non_available_with_readonly( columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -618,7 +597,7 @@ def test_volume_create_hints(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -646,8 +625,8 @@ def setUp(self): super().setUp() self.volumes = list(sdk_fakes.generate_fake_resources(_volume.Volume)) - self.volume_sdk_client.find_volume.side_effect = self.volumes - self.volume_sdk_client.delete_volume.return_value = None + self.volume_client.find_volume.side_effect = self.volumes + self.volume_client.delete_volume.return_value = None self.cmd = volume.DeleteVolume(self.app, None) @@ -655,7 +634,7 @@ 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) @@ -663,10 +642,10 @@ def test_volume_delete_one_volume(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.volumes[0].id, ignore_missing=False ) - self.volume_sdk_client.delete_volume.assert_called_once_with( + self.volume_client.delete_volume.assert_called_once_with( self.volumes[0].id, cascade=False, force=False ) @@ -674,7 +653,7 @@ 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) @@ -682,15 +661,15 @@ def test_volume_delete_multi_volumes(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_volume.assert_has_calls( + self.volume_client.find_volume.assert_has_calls( [mock.call(v.id, ignore_missing=False) for v in self.volumes] ) - self.volume_sdk_client.delete_volume.assert_has_calls( + 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_sdk_client.find_volume.side_effect = [ + self.volume_client.find_volume.side_effect = [ self.volumes[0], sdk_exceptions.NotFoundException(), ] @@ -701,7 +680,7 @@ def test_volume_delete_multi_volumes_with_exception(self): ] verifylist = [ ('force', False), - ('purge', False), + ('cascade', False), ('volumes', [self.volumes[0].id, 'unexist_volume']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -713,13 +692,13 @@ def test_volume_delete_multi_volumes_with_exception(self): ) self.assertEqual('1 of 2 volumes failed to delete.', str(exc)) - self.volume_sdk_client.find_volume.assert_has_calls( + 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_sdk_client.delete_volume.assert_has_calls( + self.volume_client.delete_volume.assert_has_calls( [ mock.call(self.volumes[0].id, cascade=False, force=False), ] @@ -732,7 +711,7 @@ 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) @@ -740,10 +719,10 @@ def test_volume_delete_with_purge(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.volumes[0].id, ignore_missing=False ) - self.volume_sdk_client.delete_volume.assert_called_once_with( + self.volume_client.delete_volume.assert_called_once_with( self.volumes[0].id, cascade=True, force=False ) @@ -754,7 +733,7 @@ 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) @@ -762,37 +741,35 @@ def test_volume_delete_with_force(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.volumes[0].id, ignore_missing=False ) - self.volume_sdk_client.delete_volume.assert_called_once_with( + 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.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_user.return_value = self.user + self.identity_sdk_client.find_project.return_value = self.project - # Get the command object to test self.cmd = volume.ListVolume(self.app, None) def test_volume_list_no_options(self): @@ -809,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)) @@ -852,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)) @@ -898,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)) @@ -941,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)) @@ -986,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)) @@ -1015,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), @@ -1029,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)) @@ -1058,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), ] @@ -1072,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)) @@ -1114,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)) @@ -1157,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', @@ -1179,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", ] @@ -1208,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): @@ -1256,51 +1120,14 @@ 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): def setUp(self): super().setUp() self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.migrate_volume.return_value = None + self.volume_client.find_volume.return_value = self.volume + self.volume_client.migrate_volume.return_value = None self.cmd = volume.MigrateVolume(self.app, None) @@ -1321,10 +1148,10 @@ def test_volume_migrate(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - 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.migrate_volume.assert_called_once_with( + self.volume_client.migrate_volume.assert_called_once_with( self.volume.id, host="host@backend-name#pool", force_host_copy=False, @@ -1350,10 +1177,10 @@ def test_volume_migrate_with_option(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - 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.migrate_volume.assert_called_once_with( + self.volume_client.migrate_volume.assert_called_once_with( self.volume.id, host="host@backend-name#pool", force_host_copy=True, @@ -1378,19 +1205,18 @@ def test_volume_migrate_without_host(self): verifylist, ) - self.volume_sdk_client.find_volume.assert_not_called() - self.volume_sdk_client.migrate_volume.assert_not_called() - + 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) @@ -1401,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): @@ -1422,205 +1253,249 @@ 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(volume_fakes.TestVolume): @@ -1628,7 +1503,7 @@ def setUp(self): super().setUp() 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 self.columns = ( 'attachments', @@ -1698,67 +1573,70 @@ def test_volume_show(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - self.volume_sdk_client.find_volume.assert_called_with( + 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 - - # 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) + + 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 = [ @@ -1766,27 +1644,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), - ] - parsed_args = self.check_parser(self.cmd_unset, 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 - ) - self.volumes_mock.delete_metadata.assert_called_with( - self.new_volume.id, parsed_args.properties + ('volume', self.volume.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 unset operations failed', str(exc) + ) + 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 ) 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 e7bbb69999..bba14f5dcb 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_backup.py @@ -35,15 +35,15 @@ def setUp(self): super().setUp() 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 self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) - self.volume_sdk_client.find_snapshot.return_value = self.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_sdk_client.create_backup.return_value = self.backup + self.volume_client.create_backup.return_value = self.backup self.data = ( self.backup.id, @@ -80,7 +80,7 @@ def test_backup_create(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( + self.volume_client.create_backup.assert_called_with( volume_id=self.backup.volume_id, container=self.backup.container, name=self.backup.name, @@ -109,7 +109,7 @@ def test_backup_create_without_name(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( + self.volume_client.create_backup.assert_called_with( volume_id=self.backup.volume_id, container=self.backup.container, name=None, @@ -126,8 +126,8 @@ def setUp(self): super().setUp() self.backups = list(sdk_fakes.generate_fake_resources(_backup.Backup)) - self.volume_sdk_client.find_backup.side_effect = self.backups - self.volume_sdk_client.delete_backup.return_value = None + self.volume_client.find_backup.side_effect = self.backups + self.volume_client.delete_backup.return_value = None self.cmd = volume_backup.DeleteVolumeBackup(self.app, None) @@ -138,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) @@ -153,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) @@ -172,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): @@ -187,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) @@ -195,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, @@ -220,26 +220,22 @@ class TestBackupList(volume_fakes.TestVolume): 'Incremental', 'Created At', ) - columns_long = columns + ( - 'Availability Zone', - 'Volume', - 'Container', - ) + columns_long = (*columns, 'Availability Zone', 'Volume', 'Container') def setUp(self): super().setUp() self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.volumes.return_value = [self.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_sdk_client.backups.return_value = self.backups - self.volume_sdk_client.find_backup.return_value = self.backups[0] + 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: @@ -288,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)) @@ -329,19 +326,20 @@ 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)) @@ -358,12 +356,12 @@ def setUp(self): super().setUp() 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 self.backup = sdk_fakes.generate_fake_resource( _backup.Backup, volume_id=self.volume.id ) - self.volume_sdk_client.find_backup.return_value = self.backup - self.volume_sdk_client.restore_backup.return_value = { + 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'], @@ -378,9 +376,7 @@ def setUp(self): 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), @@ -389,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, @@ -413,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) @@ -436,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) @@ -470,7 +458,7 @@ def setUp(self): self.backup = sdk_fakes.generate_fake_resource( _backup.Backup, metadata={'wow': 'cool'} ) - self.volume_sdk_client.find_backup.return_value = self.backup + self.volume_client.find_backup.return_value = self.backup self.cmd = volume_backup.SetVolumeBackup(self.app, None) @@ -483,15 +471,15 @@ def test_backup_set_state(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_backup.assert_called_with( + self.volume_client.find_backup.assert_called_with( self.backup.id, ignore_missing=False ) - self.volume_sdk_client.reset_backup_status.assert_called_with( + self.volume_client.reset_backup_status.assert_called_with( self.backup, status='error' ) def test_backup_set_state_failed(self): - self.volume_sdk_client.reset_backup_status.side_effect = ( + self.volume_client.reset_backup_status.side_effect = ( sdk_exceptions.NotFoundException('foo') ) @@ -504,10 +492,10 @@ def test_backup_set_state_failed(self): ) self.assertEqual('One or more of the set operations failed', str(exc)) - self.volume_sdk_client.find_backup.assert_called_with( + self.volume_client.find_backup.assert_called_with( self.backup.id, ignore_missing=False ) - self.volume_sdk_client.reset_backup_status.assert_called_with( + self.volume_client.reset_backup_status.assert_called_with( self.backup, status='error' ) @@ -536,7 +524,7 @@ def setUp(self): super().setUp() self.backup = sdk_fakes.generate_fake_resource(_backup.Backup) - self.volume_sdk_client.find_backup.return_value = self.backup + self.volume_client.find_backup.return_value = self.backup self.data = ( self.backup.availability_zone, @@ -565,7 +553,7 @@ 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.volume_client.find_backup.assert_called_with( self.backup.id, ignore_missing=False ) 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 0df379bb24..122cddf5ab 100644 --- a/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py +++ b/openstackclient/tests/unit/volume/v2/test_volume_snapshot.py @@ -16,11 +16,11 @@ 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 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 @@ -42,12 +42,12 @@ def setUp(self): super().setUp() 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 self.snapshot = sdk_fakes.generate_fake_resource( _snapshot.Snapshot, volume_id=self.volume.id ) - self.volume_sdk_client.create_snapshot.return_value = self.snapshot - self.volume_sdk_client.manage_snapshot.return_value = self.snapshot + self.volume_client.create_snapshot.return_value = self.snapshot + self.volume_client.manage_snapshot.return_value = self.snapshot self.data = ( self.snapshot.created_at, @@ -88,10 +88,10 @@ def test_snapshot_create(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.snapshot.volume_id, ignore_missing=False ) - self.volume_sdk_client.create_snapshot.assert_called_with( + self.volume_client.create_snapshot.assert_called_with( volume_id=self.snapshot.volume_id, force=True, name=self.snapshot.name, @@ -133,10 +133,10 @@ def test_snapshot_create_without_volume(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.snapshot.name, ignore_missing=False ) - self.volume_sdk_client.create_snapshot.assert_called_once_with( + self.volume_client.create_snapshot.assert_called_once_with( volume_id=self.snapshot.volume_id, force=True, name=self.snapshot.name, @@ -169,17 +169,17 @@ def test_snapshot_create_with_remote_source(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.snapshot.volume_id, ignore_missing=False ) - self.volume_sdk_client.manage_snapshot.assert_called_with( + 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_sdk_client.create_snapshot.assert_not_called() + self.volume_client.create_snapshot.assert_not_called() class TestVolumeSnapshotDelete(volume_fakes.TestVolume): @@ -189,8 +189,8 @@ def setUp(self): self.snapshots = list( sdk_fakes.generate_fake_resources(_snapshot.Snapshot) ) - self.volume_sdk_client.find_snapshot.side_effect = self.snapshots - self.volume_sdk_client.delete_snapshot.return_value = None + self.volume_client.find_snapshot.side_effect = self.snapshots + self.volume_client.delete_snapshot.return_value = None self.cmd = volume_snapshot.DeleteVolumeSnapshot(self.app, None) @@ -202,10 +202,10 @@ def test_snapshot_delete(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( self.snapshots[0].id, ignore_missing=False ) - self.volume_sdk_client.delete_snapshot.assert_called_once_with( + self.volume_client.delete_snapshot.assert_called_once_with( self.snapshots[0].id, force=False ) @@ -217,10 +217,10 @@ def test_snapshot_delete_with_force(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( self.snapshots[0].id, ignore_missing=False ) - self.volume_sdk_client.delete_snapshot.assert_called_once_with( + self.volume_client.delete_snapshot.assert_called_once_with( self.snapshots[0].id, force=True ) @@ -236,15 +236,15 @@ def test_delete_multiple_snapshots(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_has_calls( + self.volume_client.find_snapshot.assert_has_calls( [mock.call(x.id, ignore_missing=False) for x in self.snapshots] ) - self.volume_sdk_client.delete_snapshot.assert_has_calls( + 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_sdk_client.find_snapshot.side_effect = [ + self.volume_client.find_snapshot.side_effect = [ self.snapshots[0], sdk_exceptions.NotFoundException(), ] @@ -266,13 +266,13 @@ def test_delete_multiple_snapshots_with_exception(self): ) self.assertEqual('1 of 2 snapshots failed to delete.', str(exc)) - self.volume_sdk_client.find_snapshot.assert_has_calls( + 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_sdk_client.delete_snapshot.assert_has_calls( + self.volume_client.delete_snapshot.assert_has_calls( [ mock.call(self.snapshots[0].id, force=False), ] @@ -289,15 +289,13 @@ def setUp(self): _snapshot.Snapshot, attrs={'volume_id': self.volume.name} ) ) - self.project = project_fakes.FakeProject.create_one_project() - self.volume_sdk_client.volumes.return_value = [self.volume] - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.snapshots.return_value = self.snapshots - self.project_mock = self.identity_client.projects - self.project_mock.get.return_value = self.project + 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 + ( + self.columns_long = ( + *self.columns, "Created At", "Volume", "Properties", @@ -339,9 +337,10 @@ def test_snapshot_list_without_options(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, + max_items=None, all_projects=False, name=None, status=None, @@ -352,19 +351,22 @@ def test_snapshot_list_without_options(self): 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), ] @@ -372,11 +374,12 @@ def test_snapshot_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=2, marker=self.snapshots[0].id, + max_items=None, all_projects=True, - project_id=self.project.id, + project_id=project.id, name=None, status=None, volume_id=None, @@ -393,9 +396,10 @@ def test_snapshot_list_all_projects(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, + max_items=None, all_projects=True, name=None, status=None, @@ -419,9 +423,10 @@ def test_snapshot_list_name_option(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + 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, @@ -445,9 +450,10 @@ def test_snapshot_list_status_option(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, + max_items=None, all_projects=False, name=None, status='available', @@ -471,9 +477,10 @@ def test_snapshot_list_volumeid_option(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, + max_items=None, all_projects=False, name=None, status=None, @@ -507,10 +514,10 @@ def setUp(self): self.snapshot = sdk_fakes.generate_fake_resource( _snapshot.Snapshot, metadata={'foo': 'bar'} ) - self.volume_sdk_client.find_snapshot.return_value = self.snapshot - self.volume_sdk_client.delete_snapshot_metadata.return_value = None - self.volume_sdk_client.set_snapshot_metadata.return_value = None - self.volume_sdk_client.update_snapshot.return_value = None + 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) @@ -526,12 +533,12 @@ def test_snapshot_set_no_option(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( parsed_args.snapshot, ignore_missing=False ) - self.volume_sdk_client.reset_snapshot_status.assert_not_called() - self.volume_sdk_client.update_snapshot.assert_not_called() - self.volume_sdk_client.set_snapshot_metadata.assert_not_called() + 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 = [ @@ -553,10 +560,10 @@ def test_snapshot_set_name_and_property(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.update_snapshot.assert_called_with( + self.volume_client.update_snapshot.assert_called_with( self.snapshot.id, name="new_snapshot" ) - self.volume_sdk_client.set_snapshot_metadata.assert_called_with( + self.volume_client.set_snapshot_metadata.assert_called_with( self.snapshot.id, x="y", foo="foo" ) @@ -574,13 +581,13 @@ def test_snapshot_set_with_no_property(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( parsed_args.snapshot, ignore_missing=False ) - self.volume_sdk_client.reset_snapshot_status.assert_not_called() - self.volume_sdk_client.update_snapshot.assert_not_called() - self.volume_sdk_client.set_snapshot_metadata.assert_not_called() - self.volume_sdk_client.delete_snapshot_metadata.assert_called_with( + 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"] ) @@ -601,15 +608,15 @@ def test_snapshot_set_with_no_property_and_property(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( parsed_args.snapshot, ignore_missing=False ) - self.volume_sdk_client.reset_snapshot_status.assert_not_called() - self.volume_sdk_client.update_snapshot.assert_not_called() - self.volume_sdk_client.delete_snapshot_metadata.assert_called_with( + 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_sdk_client.set_snapshot_metadata.assert_called_once_with( + self.volume_client.set_snapshot_metadata.assert_called_once_with( self.snapshot.id, foo_1="bar_1", ) @@ -622,12 +629,12 @@ def test_snapshot_set_state_to_error(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.reset_snapshot_status.assert_called_with( + self.volume_client.reset_snapshot_status.assert_called_with( self.snapshot.id, "error" ) def test_volume_set_state_failed(self): - self.volume_sdk_client.reset_snapshot_status.side_effect = ( + self.volume_client.reset_snapshot_status.side_effect = ( exceptions.CommandError() ) arglist = ['--state', 'error', self.snapshot.id] @@ -639,12 +646,12 @@ def test_volume_set_state_failed(self): exceptions.CommandError, self.cmd.take_action, parsed_args ) self.assertEqual('One or more of the set operations failed', str(exc)) - self.volume_sdk_client.reset_snapshot_status.assert_called_once_with( + 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_sdk_client.reset_snapshot_status.side_effect = ( + self.volume_client.reset_snapshot_status.side_effect = ( exceptions.CommandError() ) arglist = [ @@ -668,10 +675,10 @@ def test_volume_set_name_and_state_failed(self): ) self.assertEqual('One or more of the set operations failed', str(exc)) - self.volume_sdk_client.update_snapshot.assert_called_once_with( + self.volume_client.update_snapshot.assert_called_once_with( self.snapshot.id, name="new_snapshot" ) - self.volume_sdk_client.reset_snapshot_status.assert_called_once_with( + self.volume_client.reset_snapshot_status.assert_called_once_with( self.snapshot.id, 'error' ) @@ -703,7 +710,7 @@ def setUp(self): self.snapshot.volume_id, ) - self.volume_sdk_client.find_snapshot.return_value = self.snapshot + self.volume_client.find_snapshot.return_value = self.snapshot self.cmd = volume_snapshot.ShowVolumeSnapshot(self.app, None) @@ -713,7 +720,7 @@ def test_snapshot_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.find_snapshot.assert_called_with( + self.volume_client.find_snapshot.assert_called_with( self.snapshot.id, ignore_missing=False ) @@ -726,8 +733,8 @@ def setUp(self): super().setUp() self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) - self.volume_sdk_client.find_snapshot.return_value = self.snapshot - self.volume_sdk_client.delete_snapshot_metadata.return_value = None + 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) @@ -747,6 +754,6 @@ def test_snapshot_unset(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.delete_snapshot_metadata.assert_called_with( + 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 9f27197c56..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 @@ -28,7 +28,7 @@ def setUp(self): self.log_level = sdk_fakes.generate_fake_resource( _service.LogLevel, binary='cinder-scheduler' ) - self.volume_sdk_client.get_service_log_levels.return_value = [ + self.volume_client.get_service_log_levels.return_value = [ self.log_level ] @@ -72,7 +72,7 @@ def test_block_storage_log_level_list(self): self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - self.volume_sdk_client.get_service_log_levels.assert_called_with( + self.volume_client.get_service_log_levels.assert_called_with( server=self.log_level.host, binary=self.log_level.binary, prefix='cinder.', @@ -135,7 +135,7 @@ def setUp(self): self.log_level = sdk_fakes.generate_fake_resource( _service.LogLevel, binary='cinder-api' ) - self.volume_sdk_client.set_service_log_levels.return_value = None + self.volume_client.set_service_log_levels.return_value = None self.cmd = service.BlockStorageLogLevelSet(self.app, None) @@ -162,7 +162,7 @@ def test_block_storage_log_level_set(self): ret = self.cmd.take_action(parsed_args) self.assertIsNone(ret) - self.volume_sdk_client.set_service_log_levels.assert_called_with( + self.volume_client.set_service_log_levels.assert_called_with( level='ERROR', server=self.log_level.host, binary=self.log_level.binary, @@ -254,7 +254,7 @@ def test_block_storage_log_level_set_log_level(self, log_level): self.cmd.take_action(parsed_args) - self.volume_sdk_client.set_service_log_levels.assert_called_with( + self.volume_client.set_service_log_levels.assert_called_with( level=log_level.upper(), server=self.log_level.host, binary=self.log_level.binary, 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 53027fcb58..3cac2eece7 100644 --- a/openstackclient/tests/unit/volume/v3/test_service.py +++ b/openstackclient/tests/unit/volume/v3/test_service.py @@ -27,7 +27,7 @@ def setUp(self): super().setUp() self.service = sdk_fakes.generate_fake_resource(_service.Service) - self.volume_sdk_client.services.return_value = [self.service] + self.volume_client.services.return_value = [self.service] self.cmd = service.ListService(self.app, None) @@ -66,7 +66,7 @@ def test_service_list(self): ) self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - self.volume_sdk_client.services.assert_called_with( + self.volume_client.services.assert_called_with( host=self.service.host, binary=self.service.binary, ) @@ -110,7 +110,7 @@ def test_service_list_with_long_option(self): ) self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - self.volume_sdk_client.services.assert_called_with( + self.volume_client.services.assert_called_with( host=self.service.host, binary=self.service.binary, ) @@ -154,7 +154,7 @@ def test_service_list_with_cluster(self): ) self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - self.volume_sdk_client.services.assert_called_with( + self.volume_client.services.assert_called_with( host=self.service.host, binary=self.service.binary, ) @@ -200,7 +200,7 @@ def test_service_list_with_backend_state(self): ) self.assertEqual(expected_columns, columns) self.assertEqual(datalist, tuple(data)) - self.volume_sdk_client.services.assert_called_with( + self.volume_client.services.assert_called_with( host=self.service.host, binary=self.service.binary, ) @@ -213,7 +213,7 @@ def setUp(self): 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_sdk_client.find_service.return_value = self.service + self.volume_client.find_service.return_value = self.service self.cmd = service.SetService(self.app, None) @@ -248,7 +248,7 @@ def test_service_set_enable(self): result = self.cmd.take_action(parsed_args) - self.service.enable.assert_called_with(self.volume_sdk_client) + self.service.enable.assert_called_with(self.volume_client) self.service.disable.assert_not_called() self.assertIsNone(result) @@ -269,7 +269,7 @@ def test_service_set_disable(self): self.service.enable.assert_not_called() self.service.disable.assert_called_with( - self.volume_sdk_client, reason=None + self.volume_client, reason=None ) self.assertIsNone(result) @@ -294,7 +294,7 @@ def test_service_set_disable_with_reason(self): self.service.enable.assert_not_called() self.service.disable.assert_called_with( - self.volume_sdk_client, reason=reason + self.volume_client, reason=reason ) self.assertIsNone(result) diff --git a/openstackclient/tests/unit/volume/v3/test_volume.py b/openstackclient/tests/unit/volume/v3/test_volume.py index 33dcfe5a47..135075466c 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume.py +++ b/openstackclient/tests/unit/volume/v3/test_volume.py @@ -18,15 +18,17 @@ 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.api import volume_v3 -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 import utils as test_utils from openstackclient.tests.unit.volume.v3 import fakes as volume_fakes @@ -74,7 +76,7 @@ def setUp(self): super().setUp() self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.create_volume.return_value = self.volume + self.volume_client.create_volume.return_value = self.volume self.datalist = ( self.volume.attachments, @@ -127,7 +129,7 @@ def test_volume_create_min_options(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=None, @@ -180,7 +182,7 @@ def test_volume_create_options(self): ) as mock_find_cg: columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -195,7 +197,7 @@ def test_volume_create_options(self): backup_id=None, ) mock_find_cg.assert_called_once_with( - self.volume_sdk_client, consistency_group_id + self.volume_client, consistency_group_id ) self.assertEqual(self.columns, columns) @@ -220,7 +222,7 @@ def test_volume_create_properties(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -258,7 +260,7 @@ def test_volume_create_image_id(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -296,7 +298,7 @@ def test_volume_create_image_name(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -316,7 +318,7 @@ def test_volume_create_image_name(self): def test_volume_create_with_snapshot(self): snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) - self.volume_sdk_client.find_snapshot.return_value = snapshot + self.volume_client.find_snapshot.return_value = snapshot arglist = [ '--snapshot', @@ -331,7 +333,7 @@ def test_volume_create_with_snapshot(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=snapshot.size, snapshot_id=snapshot.id, name=self.volume.name, @@ -345,7 +347,7 @@ def test_volume_create_with_snapshot(self): scheduler_hints=None, backup_id=None, ) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( snapshot.id, ignore_missing=False ) @@ -356,7 +358,7 @@ def test_volume_create_with_backup(self): self.set_volume_api_version('3.47') backup = sdk_fakes.generate_fake_resource(_backup.Backup) - self.volume_sdk_client.find_backup.return_value = backup + self.volume_client.find_backup.return_value = backup arglist = [ '--backup', @@ -371,7 +373,7 @@ def test_volume_create_with_backup(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=backup.size, snapshot_id=None, name=self.volume.name, @@ -385,7 +387,7 @@ def test_volume_create_with_backup(self): scheduler_hints=None, backup_id=backup.id, ) - self.volume_sdk_client.find_backup.assert_called_once_with( + self.volume_client.find_backup.assert_called_once_with( backup.id, ignore_missing=False ) @@ -394,7 +396,7 @@ def test_volume_create_with_backup(self): def test_volume_create_with_backup_pre_v347(self): backup = sdk_fakes.generate_fake_resource(_backup.Backup) - self.volume_sdk_client.find_backup.return_value = backup + self.volume_client.find_backup.return_value = backup arglist = [ '--backup', @@ -412,11 +414,11 @@ def test_volume_create_with_backup_pre_v347(self): ) self.assertIn("--os-volume-api-version 3.47 or greater", str(exc)) - self.volume_sdk_client.create_volume.assert_not_called() + self.volume_client.create_volume.assert_not_called() def test_volume_create_with_source_volume(self): source_volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.find_volume.return_value = source_volume + self.volume_client.find_volume.return_value = source_volume arglist = [ '--source', @@ -431,7 +433,7 @@ def test_volume_create_with_source_volume(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=source_volume.size, snapshot_id=None, name=self.volume.name, @@ -445,7 +447,7 @@ def test_volume_create_with_source_volume(self): scheduler_hints=None, backup_id=None, ) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( source_volume.id, ignore_missing=False ) @@ -472,7 +474,7 @@ def test_volume_create_with_bootable_and_readonly(self, mock_wait): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -486,10 +488,10 @@ def test_volume_create_with_bootable_and_readonly(self, mock_wait): scheduler_hints=None, backup_id=None, ) - self.volume_sdk_client.set_volume_bootable_status.assert_called_once_with( + self.volume_client.set_volume_bootable_status.assert_called_once_with( self.volume, True ) - self.volume_sdk_client.set_volume_readonly.assert_called_once_with( + self.volume_client.set_volume_readonly.assert_called_once_with( self.volume, True ) @@ -516,7 +518,7 @@ def test_volume_create_with_nonbootable_and_readwrite(self, mock_wait): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -530,10 +532,10 @@ def test_volume_create_with_nonbootable_and_readwrite(self, mock_wait): scheduler_hints=None, backup_id=None, ) - self.volume_sdk_client.set_volume_bootable_status.assert_called_once_with( + self.volume_client.set_volume_bootable_status.assert_called_once_with( self.volume, False ) - self.volume_sdk_client.set_volume_readonly.assert_called_once_with( + self.volume_client.set_volume_readonly.assert_called_once_with( self.volume, False ) @@ -545,10 +547,10 @@ def test_volume_create_with_nonbootable_and_readwrite(self, mock_wait): def test_volume_create_with_bootable_and_readonly_fail( self, mock_wait, mock_error ): - self.volume_sdk_client.set_volume_bootable_status.side_effect = ( + self.volume_client.set_volume_bootable_status.side_effect = ( sdk_exceptions.NotFoundException('foo') ) - self.volume_sdk_client.set_volume_readonly.side_effect = ( + self.volume_client.set_volume_readonly.side_effect = ( sdk_exceptions.NotFoundException('foo') ) @@ -570,7 +572,7 @@ def test_volume_create_with_bootable_and_readonly_fail( columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -584,10 +586,10 @@ def test_volume_create_with_bootable_and_readonly_fail( scheduler_hints=None, backup_id=None, ) - self.volume_sdk_client.set_volume_bootable_status.assert_called_once_with( + self.volume_client.set_volume_bootable_status.assert_called_once_with( self.volume, True ) - self.volume_sdk_client.set_volume_readonly.assert_called_once_with( + self.volume_client.set_volume_readonly.assert_called_once_with( self.volume, True ) @@ -618,7 +620,7 @@ def test_volume_create_non_available_with_readonly( columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -720,7 +722,7 @@ def test_volume_create_hints(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_volume.assert_called_with( + self.volume_client.create_volume.assert_called_with( size=self.volume.size, snapshot_id=None, name=self.volume.name, @@ -744,7 +746,7 @@ def test_volume_create_hints(self): self.assertEqual(self.datalist, data) def test_volume_create_remote_source(self): - self.volume_sdk_client.manage_volume.return_value = self.volume + self.volume_client.manage_volume.return_value = self.volume arglist = [ '--remote-source', @@ -762,7 +764,7 @@ def test_volume_create_remote_source(self): 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, @@ -897,13 +899,10 @@ class TestVolumeDelete(volume_fakes.TestVolume): def setUp(self): super().setUp() - self.volumes_mock = self.volume_client.volumes - self.volumes_mock.reset_mock() - self.volumes = list(sdk_fakes.generate_fake_resources(_volume.Volume)) - self.volume_sdk_client.find_volume.side_effect = self.volumes - self.volume_sdk_client.delete_volume.return_value = None - self.volume_sdk_client.unmanage_volume.return_value = None + 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) @@ -912,7 +911,7 @@ 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) @@ -920,10 +919,10 @@ def test_volume_delete_one_volume(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.volumes[0].id, ignore_missing=False ) - self.volume_sdk_client.delete_volume.assert_called_once_with( + self.volume_client.delete_volume.assert_called_once_with( self.volumes[0].id, cascade=False, force=False ) @@ -931,7 +930,7 @@ 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) @@ -939,15 +938,15 @@ def test_volume_delete_multi_volumes(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_volume.assert_has_calls( + self.volume_client.find_volume.assert_has_calls( [mock.call(v.id, ignore_missing=False) for v in self.volumes] ) - self.volume_sdk_client.delete_volume.assert_has_calls( + 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_sdk_client.find_volume.side_effect = [ + self.volume_client.find_volume.side_effect = [ self.volumes[0], sdk_exceptions.NotFoundException(), ] @@ -958,7 +957,7 @@ def test_volume_delete_multi_volumes_with_exception(self): ] verifylist = [ ('force', False), - ('purge', False), + ('cascade', False), ('volumes', [self.volumes[0].id, 'unexist_volume']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -970,13 +969,13 @@ def test_volume_delete_multi_volumes_with_exception(self): ) self.assertEqual('1 of 2 volumes failed to delete.', str(exc)) - self.volume_sdk_client.find_volume.assert_has_calls( + 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_sdk_client.delete_volume.assert_has_calls( + self.volume_client.delete_volume.assert_has_calls( [ mock.call(self.volumes[0].id, cascade=False, force=False), ] @@ -989,7 +988,7 @@ 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) @@ -997,10 +996,32 @@ def test_volume_delete_with_purge(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.volumes[0].id, ignore_missing=False ) - self.volume_sdk_client.delete_volume.assert_called_once_with( + 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 ) @@ -1011,7 +1032,7 @@ 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) @@ -1019,10 +1040,10 @@ def test_volume_delete_with_force(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.volumes[0].id, ignore_missing=False ) - self.volume_sdk_client.delete_volume.assert_called_once_with( + self.volume_client.delete_volume.assert_called_once_with( self.volumes[0].id, cascade=False, force=True ) @@ -1031,7 +1052,7 @@ def test_volume_delete_remote(self): verifylist = [ ("remote", True), ("force", False), - ("purge", False), + ("cascade", False), ("volumes", [self.volumes[0].id]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1039,11 +1060,11 @@ def test_volume_delete_remote(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.volumes[0].id, ignore_missing=False ) - self.volume_sdk_client.delete_volume.assert_not_called() - self.volume_sdk_client.unmanage_volume.assert_called_once_with( + self.volume_client.delete_volume.assert_not_called() + self.volume_client.unmanage_volume.assert_called_once_with( self.volumes[0].id ) @@ -1052,7 +1073,7 @@ def test_volume_delete_multi_volumes_remote(self): verifylist = [ ('remote', True), ('force', False), - ('purge', False), + ('cascade', False), ('volumes', arglist[1:]), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) @@ -1060,11 +1081,11 @@ def test_volume_delete_multi_volumes_remote(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_volume.assert_has_calls( + self.volume_client.find_volume.assert_has_calls( [mock.call(v.id, ignore_missing=False) for v in self.volumes] ) - self.volume_sdk_client.delete_volume.assert_not_called() - self.volume_sdk_client.unmanage_volume.assert_has_calls( + 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] ) @@ -1077,7 +1098,6 @@ def test_volume_delete_remote_with_purge(self): verifylist = [ ('remote', True), ('force', False), - ('purge', True), ('volumes', [self.volumes[0].id]), ] @@ -1086,14 +1106,14 @@ 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), ) - self.volume_sdk_client.find_volume.assert_not_called() - self.volume_sdk_client.delete_volume.assert_not_called() - self.volume_sdk_client.unmanage_volume.assert_not_called() + 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 = [ @@ -1104,7 +1124,7 @@ def test_volume_delete_remote_with_force(self): verifylist = [ ('remote', True), ('force', True), - ('purge', False), + ('cascade', False), ('volumes', [self.volumes[0].id]), ] @@ -1113,48 +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_sdk_client.find_volume.assert_not_called() - self.volume_sdk_client.delete_volume.assert_not_called() - self.volume_sdk_client.unmanage_volume.assert_not_called() + 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): @@ -1171,29 +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, - 'metadata': 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)) @@ -1215,29 +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, - 'metadata': 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)) @@ -1262,29 +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, - 'metadata': 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 +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, - 'metadata': 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)) @@ -1352,29 +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, - 'metadata': 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)) @@ -1382,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), @@ -1396,29 +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, - 'metadata': 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)) @@ -1426,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), ] @@ -1440,29 +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, - 'metadata': 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( + 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)) @@ -1483,29 +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, - 'metadata': 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)) @@ -1527,21 +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, - 'metadata': 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', @@ -1550,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", ] @@ -1579,37 +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, - 'metadata': None, - }, - ) self.assertCountEqual(datalist, tuple(data)) def test_volume_list_negative_limit(self): @@ -1628,52 +1514,14 @@ 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, - 'metadata': 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): def setUp(self): super().setUp() self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.migrate_volume.return_value = None + self.volume_client.find_volume.return_value = self.volume + self.volume_client.migrate_volume.return_value = None self.cmd = volume.MigrateVolume(self.app, None) @@ -1694,17 +1542,18 @@ def test_volume_migrate(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - 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.migrate_volume.assert_called_once_with( + 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_option(self): + def test_volume_migrate_with_host(self): arglist = [ "--force-host-copy", "--lock-volume", @@ -1723,17 +1572,74 @@ def test_volume_migrate_with_option(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - 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.migrate_volume.assert_called_once_with( + 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) + + 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 = [ + "--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, ) - def test_volume_migrate_without_host(self): + self.volume_client.migrate_volume.assert_not_called() + + def test_volume_migrate_without_host_and_cluster(self): arglist = [ self.volume.id, ] @@ -1750,26 +1656,18 @@ def test_volume_migrate_without_host(self): arglist, verifylist, ) - - self.volume_sdk_client.find_volume.assert_not_called() - self.volume_sdk_client.migrate_volume.assert_not_called() + 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) @@ -1780,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): @@ -1801,206 +1704,249 @@ 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): @@ -2008,7 +1954,7 @@ def setUp(self): super().setUp() 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 self.columns = ( 'attachments', @@ -2092,7 +2038,7 @@ def test_volume_show(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - self.volume_sdk_client.find_volume.assert_called_with( + self.volume_client.find_volume.assert_called_with( self.volume.id, ignore_missing=False ) @@ -2101,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) + + 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 = [ @@ -2163,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 ) @@ -2203,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) @@ -2220,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) @@ -2253,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']) @@ -2268,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) @@ -2289,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) @@ -2323,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, ) 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 86bde785f6..84d0e56c25 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_backup.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_backup.py @@ -36,15 +36,15 @@ def setUp(self): super().setUp() 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 self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) - self.volume_sdk_client.find_snapshot.return_value = self.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_sdk_client.create_backup.return_value = self.backup + self.volume_client.create_backup.return_value = self.backup self.data = ( self.backup.id, @@ -81,7 +81,7 @@ def test_backup_create(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( + self.volume_client.create_backup.assert_called_with( volume_id=self.backup.volume_id, container=self.backup.container, name=self.backup.name, @@ -111,7 +111,7 @@ def test_backup_create_with_properties(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( + self.volume_client.create_backup.assert_called_with( volume_id=self.backup.volume_id, container=None, name=None, @@ -160,7 +160,7 @@ def test_backup_create_with_availability_zone(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( + self.volume_client.create_backup.assert_called_with( volume_id=self.backup.volume_id, container=None, name=None, @@ -208,7 +208,7 @@ def test_backup_create_without_name(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.create_backup.assert_called_with( + self.volume_client.create_backup.assert_called_with( volume_id=self.backup.volume_id, container=self.backup.container, name=None, @@ -225,8 +225,8 @@ def setUp(self): super().setUp() self.backups = list(sdk_fakes.generate_fake_resources(_backup.Backup)) - self.volume_sdk_client.find_backup.side_effect = self.backups - self.volume_sdk_client.delete_backup.return_value = None + self.volume_client.find_backup.side_effect = self.backups + self.volume_client.delete_backup.return_value = None self.cmd = volume_backup.DeleteVolumeBackup(self.app, None) @@ -237,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) @@ -252,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) @@ -271,7 +271,7 @@ def test_delete_multiple_backups(self): calls = [] for b in self.backups: calls.append(mock.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): @@ -286,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) @@ -294,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, @@ -319,26 +319,22 @@ class TestBackupList(volume_fakes.TestVolume): 'Incremental', 'Created At', ) - columns_long = columns + ( - 'Availability Zone', - 'Volume', - 'Container', - ) + columns_long = (*columns, 'Availability Zone', 'Volume', 'Container') def setUp(self): super().setUp() self.volume = sdk_fakes.generate_fake_resource(_volume.Volume) - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.volumes.return_value = [self.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_sdk_client.backups.return_value = self.backups - self.volume_sdk_client.find_backup.return_value = self.backups[0] + 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: @@ -388,15 +384,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, project_id=None, ) self.assertEqual(self.columns, columns) @@ -435,19 +432,20 @@ 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, project_id=project.id, ) self.assertEqual(self.columns_long, columns) @@ -465,13 +463,13 @@ def setUp(self): super().setUp() 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 self.backup = sdk_fakes.generate_fake_resource( _backup.Backup, volume_id=self.volume.id ) - self.volume_sdk_client.find_backup.return_value = self.backup - self.volume_sdk_client.create_backup.return_value = self.backup - self.volume_sdk_client.restore_backup.return_value = { + 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'], @@ -486,9 +484,7 @@ def setUp(self): 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), @@ -497,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, @@ -521,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) @@ -544,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) @@ -578,7 +566,7 @@ def setUp(self): self.backup = sdk_fakes.generate_fake_resource( _backup.Backup, metadata={'wow': 'cool'} ) - self.volume_sdk_client.find_backup.return_value = self.backup + self.volume_client.find_backup.return_value = self.backup self.cmd = volume_backup.SetVolumeBackup(self.app, None) @@ -599,10 +587,10 @@ def test_backup_set_name(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_backup.assert_called_with( + self.volume_client.find_backup.assert_called_with( self.backup.id, ignore_missing=False ) - self.volume_sdk_client.update_backup.assert_called_once_with( + self.volume_client.update_backup.assert_called_once_with( self.backup, name='new_name' ) @@ -643,10 +631,10 @@ def test_backup_set_description(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_backup.assert_called_with( + self.volume_client.find_backup.assert_called_with( self.backup.id, ignore_missing=False ) - self.volume_sdk_client.update_backup.assert_called_once_with( + self.volume_client.update_backup.assert_called_once_with( self.backup, description='new_description' ) @@ -679,15 +667,15 @@ def test_backup_set_state(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_backup.assert_called_with( + self.volume_client.find_backup.assert_called_with( self.backup.id, ignore_missing=False ) - self.volume_sdk_client.reset_backup_status.assert_called_with( + self.volume_client.reset_backup_status.assert_called_with( self.backup, status='error' ) def test_backup_set_state_failed(self): - self.volume_sdk_client.reset_backup_status.side_effect = ( + self.volume_client.reset_backup_status.side_effect = ( sdk_exceptions.NotFoundException('foo') ) @@ -700,10 +688,10 @@ def test_backup_set_state_failed(self): ) self.assertEqual('One or more of the set operations failed', str(exc)) - self.volume_sdk_client.find_backup.assert_called_with( + self.volume_client.find_backup.assert_called_with( self.backup.id, ignore_missing=False ) - self.volume_sdk_client.reset_backup_status.assert_called_with( + self.volume_client.reset_backup_status.assert_called_with( self.backup, status='error' ) @@ -723,10 +711,10 @@ def test_backup_set_no_property(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_backup.assert_called_with( + self.volume_client.find_backup.assert_called_with( self.backup.id, ignore_missing=False ) - self.volume_sdk_client.update_backup.assert_called_once_with( + self.volume_client.update_backup.assert_called_once_with( self.backup, metadata={} ) @@ -765,10 +753,10 @@ def test_backup_set_property(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_backup.assert_called_with( + self.volume_client.find_backup.assert_called_with( self.backup.id, ignore_missing=False ) - self.volume_sdk_client.update_backup.assert_called_once_with( + self.volume_client.update_backup.assert_called_once_with( self.backup, metadata={'wow': 'cool', 'foo': 'bar'} ) @@ -799,8 +787,8 @@ def setUp(self): self.backup = sdk_fakes.generate_fake_resource( _backup.Backup, metadata={'foo': 'bar', 'wow': 'cool'} ) - self.volume_sdk_client.find_backup.return_value = self.backup - self.volume_sdk_client.delete_backup_metadata.return_value = None + self.volume_client.find_backup.return_value = self.backup + self.volume_client.delete_backup_metadata.return_value = None self.cmd = volume_backup.UnsetVolumeBackup(self.app, None) @@ -821,10 +809,10 @@ def test_backup_unset_property(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_backup.assert_called_with( + self.volume_client.find_backup.assert_called_with( self.backup.id, ignore_missing=False ) - self.volume_sdk_client.delete_backup_metadata.assert_called_once_with( + self.volume_client.delete_backup_metadata.assert_called_once_with( self.backup, keys=['wow'] ) @@ -876,7 +864,7 @@ def setUp(self): super().setUp() self.backup = sdk_fakes.generate_fake_resource(_backup.Backup) - self.volume_sdk_client.find_backup.return_value = self.backup + self.volume_client.find_backup.return_value = self.backup self.data = ( self.backup.availability_zone, @@ -909,7 +897,7 @@ 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.volume_client.find_backup.assert_called_with( self.backup.id, ignore_missing=False ) diff --git a/openstackclient/tests/unit/volume/v3/test_volume_group.py b/openstackclient/tests/unit/volume/v3/test_volume_group.py index 3b64ad95c5..d105f8021d 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.fake_volume_group - ) - self.volume_group_snapshots_mock.get.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 = { + 'id': self.fake_volume_group.id + } + 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 85613603de..2200b002f3 100644 --- a/openstackclient/tests/unit/volume/v3/test_volume_snapshot.py +++ b/openstackclient/tests/unit/volume/v3/test_volume_snapshot.py @@ -16,11 +16,11 @@ 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 openstackclient.tests.unit.identity.v3 import fakes as project_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_snapshot @@ -31,12 +31,12 @@ def setUp(self): super().setUp() 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 self.snapshot = sdk_fakes.generate_fake_resource( _snapshot.Snapshot, volume_id=self.volume.id ) - self.volume_sdk_client.create_snapshot.return_value = self.snapshot - self.volume_sdk_client.manage_snapshot.return_value = self.snapshot + self.volume_client.create_snapshot.return_value = self.snapshot + self.volume_client.manage_snapshot.return_value = self.snapshot self.columns = ( 'created_at', @@ -85,10 +85,10 @@ def test_snapshot_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.snapshot.volume_id, ignore_missing=False ) - self.volume_sdk_client.create_snapshot.assert_called_with( + self.volume_client.create_snapshot.assert_called_with( volume_id=self.snapshot.volume_id, force=True, name=self.snapshot.name, @@ -130,10 +130,10 @@ def test_snapshot_create_without_volume(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.snapshot.name, ignore_missing=False ) - self.volume_sdk_client.create_snapshot.assert_called_with( + self.volume_client.create_snapshot.assert_called_with( volume_id=self.snapshot.volume_id, force=True, name=self.snapshot.name, @@ -168,17 +168,70 @@ def test_snapshot_create_with_remote_source(self): self.assertEqual(self.columns, columns) self.assertEqual(self.data, data) - self.volume_sdk_client.find_volume.assert_called_once_with( + self.volume_client.find_volume.assert_called_once_with( self.snapshot.volume_id, ignore_missing=False ) - self.volume_sdk_client.manage_snapshot.assert_called_with( + 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_sdk_client.create_snapshot.assert_not_called() + 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, + ) + + 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): @@ -188,9 +241,9 @@ def setUp(self): self.snapshots = list( sdk_fakes.generate_fake_resources(_snapshot.Snapshot) ) - self.volume_sdk_client.find_snapshot.side_effect = self.snapshots - self.volume_sdk_client.delete_snapshot.return_value = None - self.volume_sdk_client.unmanage_snapshot.return_value = None + 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 self.cmd = volume_snapshot.DeleteVolumeSnapshot(self.app, None) @@ -202,10 +255,10 @@ def test_snapshot_delete(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( self.snapshots[0].id, ignore_missing=False ) - self.volume_sdk_client.delete_snapshot.assert_called_once_with( + self.volume_client.delete_snapshot.assert_called_once_with( self.snapshots[0].id, force=False ) @@ -217,10 +270,10 @@ def test_snapshot_delete_with_force(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( self.snapshots[0].id, ignore_missing=False ) - self.volume_sdk_client.delete_snapshot.assert_called_once_with( + self.volume_client.delete_snapshot.assert_called_once_with( self.snapshots[0].id, force=True ) @@ -236,15 +289,15 @@ def test_delete_multiple_snapshots(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_has_calls( + self.volume_client.find_snapshot.assert_has_calls( [mock.call(x.id, ignore_missing=False) for x in self.snapshots] ) - self.volume_sdk_client.delete_snapshot.assert_has_calls( + 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_sdk_client.find_snapshot.side_effect = [ + self.volume_client.find_snapshot.side_effect = [ self.snapshots[0], sdk_exceptions.NotFoundException(), ] @@ -266,13 +319,13 @@ def test_delete_multiple_snapshots_with_exception(self): ) self.assertEqual('1 of 2 snapshots failed to delete.', str(exc)) - self.volume_sdk_client.find_snapshot.assert_has_calls( + 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_sdk_client.delete_snapshot.assert_has_calls( + self.volume_client.delete_snapshot.assert_has_calls( [ mock.call(self.snapshots[0].id, force=False), ] @@ -286,7 +339,7 @@ def test_snapshot_delete_remote(self): 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 ) @@ -317,7 +370,7 @@ def test_delete_multiple_snapshots_remote(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.unmanage_snapshot.assert_has_calls( + self.volume_client.unmanage_snapshot.assert_has_calls( [mock.call(s.id) for s in self.snapshots] ) @@ -332,15 +385,13 @@ def setUp(self): _snapshot.Snapshot, attrs={'volume_id': self.volume.name} ) ) - self.project = project_fakes.FakeProject.create_one_project() - self.volume_sdk_client.volumes.return_value = [self.volume] - self.volume_sdk_client.find_volume.return_value = self.volume - self.volume_sdk_client.snapshots.return_value = self.snapshots - self.project_mock = self.identity_client.projects - self.project_mock.get.return_value = self.project + 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 + ( + self.columns_long = ( + *self.columns, "Created At", "Volume", "Properties", @@ -382,9 +433,10 @@ def test_snapshot_list_without_options(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, + max_items=None, all_projects=False, name=None, status=None, @@ -395,19 +447,22 @@ def test_snapshot_list_without_options(self): 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), ] @@ -415,11 +470,12 @@ def test_snapshot_list_with_options(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=2, marker=self.snapshots[0].id, + max_items=None, all_projects=True, - project_id=self.project.id, + project_id=project.id, name=None, status=None, volume_id=None, @@ -436,9 +492,10 @@ def test_snapshot_list_all_projects(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, + max_items=None, all_projects=True, name=None, status=None, @@ -462,9 +519,10 @@ def test_snapshot_list_name_option(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + 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, @@ -488,9 +546,10 @@ def test_snapshot_list_status_option(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, + max_items=None, all_projects=False, name=None, status='available', @@ -514,9 +573,10 @@ def test_snapshot_list_volumeid_option(self): columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.snapshots.assert_called_once_with( + self.volume_client.snapshots.assert_called_once_with( limit=None, marker=None, + max_items=None, all_projects=False, name=None, status=None, @@ -550,9 +610,9 @@ def setUp(self): self.snapshot = sdk_fakes.generate_fake_resource( _snapshot.Snapshot, metadata={'foo': 'bar'} ) - self.volume_sdk_client.find_snapshot.return_value = self.snapshot - self.volume_sdk_client.set_snapshot_metadata.return_value = None - self.volume_sdk_client.update_snapshot.return_value = None + 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) @@ -568,12 +628,12 @@ def test_snapshot_set_no_option(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( parsed_args.snapshot, ignore_missing=False ) - self.volume_sdk_client.reset_snapshot_status.assert_not_called() - self.volume_sdk_client.update_snapshot.assert_not_called() - self.volume_sdk_client.set_snapshot_metadata.assert_not_called() + 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 = [ @@ -595,10 +655,10 @@ def test_snapshot_set_name_and_property(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.update_snapshot.assert_called_with( + self.volume_client.update_snapshot.assert_called_with( self.snapshot.id, name="new_snapshot" ) - self.volume_sdk_client.set_snapshot_metadata.assert_called_with( + self.volume_client.set_snapshot_metadata.assert_called_with( self.snapshot.id, x="y", foo="foo" ) @@ -616,13 +676,13 @@ def test_snapshot_set_with_no_property(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( parsed_args.snapshot, ignore_missing=False ) - self.volume_sdk_client.reset_snapshot_status.assert_not_called() - self.volume_sdk_client.update_snapshot.assert_not_called() - self.volume_sdk_client.set_snapshot_metadata.assert_not_called() - self.volume_sdk_client.delete_snapshot_metadata.assert_called_with( + 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"] ) @@ -643,15 +703,15 @@ def test_snapshot_set_with_no_property_and_property(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.find_snapshot.assert_called_once_with( + self.volume_client.find_snapshot.assert_called_once_with( parsed_args.snapshot, ignore_missing=False ) - self.volume_sdk_client.reset_snapshot_status.assert_not_called() - self.volume_sdk_client.update_snapshot.assert_not_called() - self.volume_sdk_client.delete_snapshot_metadata.assert_called_with( + 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_sdk_client.set_snapshot_metadata.assert_called_once_with( + self.volume_client.set_snapshot_metadata.assert_called_once_with( self.snapshot.id, foo_1="bar_1", ) @@ -664,12 +724,12 @@ def test_snapshot_set_state_to_error(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.reset_snapshot_status.assert_called_with( + self.volume_client.reset_snapshot_status.assert_called_with( self.snapshot.id, "error" ) def test_volume_set_state_failed(self): - self.volume_sdk_client.reset_snapshot_status.side_effect = ( + self.volume_client.reset_snapshot_status.side_effect = ( exceptions.CommandError() ) arglist = ['--state', 'error', self.snapshot.id] @@ -683,12 +743,12 @@ def test_volume_set_state_failed(self): ) self.assertEqual('One or more of the set operations failed', str(exc)) - self.volume_sdk_client.reset_snapshot_status.assert_called_once_with( + 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_sdk_client.reset_snapshot_status.side_effect = ( + self.volume_client.reset_snapshot_status.side_effect = ( exceptions.CommandError() ) arglist = [ @@ -712,10 +772,10 @@ def test_volume_set_name_and_state_failed(self): ) self.assertEqual('One or more of the set operations failed', str(exc)) - self.volume_sdk_client.update_snapshot.assert_called_once_with( + self.volume_client.update_snapshot.assert_called_once_with( self.snapshot.id, name="new_snapshot" ) - self.volume_sdk_client.reset_snapshot_status.assert_called_once_with( + self.volume_client.reset_snapshot_status.assert_called_once_with( self.snapshot.id, 'error' ) @@ -747,7 +807,7 @@ def setUp(self): self.snapshot.volume_id, ) - self.volume_sdk_client.find_snapshot.return_value = self.snapshot + self.volume_client.find_snapshot.return_value = self.snapshot self.cmd = volume_snapshot.ShowVolumeSnapshot(self.app, None) @@ -757,7 +817,7 @@ def test_snapshot_show(self): parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) - self.volume_sdk_client.find_snapshot.assert_called_with( + self.volume_client.find_snapshot.assert_called_with( self.snapshot.id, ignore_missing=False ) @@ -770,8 +830,8 @@ def setUp(self): super().setUp() self.snapshot = sdk_fakes.generate_fake_resource(_snapshot.Snapshot) - self.volume_sdk_client.find_snapshot.return_value = self.snapshot - self.volume_sdk_client.delete_snapshot_metadata.return_value = None + 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) @@ -790,6 +850,6 @@ def test_snapshot_unset(self): result = self.cmd.take_action(parsed_args) self.assertIsNone(result) - self.volume_sdk_client.delete_snapshot_metadata.assert_called_with( + 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 dbef055fac..f5a35a7978 100644 --- a/openstackclient/volume/client.py +++ b/openstackclient/volume/client.py @@ -11,88 +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 = { - '2': 'cinderclient.v2.client.Client', - '3': 'cinderclient.v3.client.Client', -} - -# Save the microversion if in use -_volume_api_version = None +API_VERSIONS = ('2', '3') -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 - - 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('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 - - 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, + LOG.debug( + 'Volume client initialized using OpenStack SDK: %s', + instance.sdk_connection.block_storage, ) + return instance.sdk_connection.block_storage - 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', @@ -104,37 +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 - - _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 93492f87f7..bc25d99e7c 100644 --- a/openstackclient/volume/v2/backup_record.py +++ b/openstackclient/volume/v2/backup_record.py @@ -14,9 +14,12 @@ """Volume v2 Backup action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any -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 4910bb129e..08bae165ff 100644 --- a/openstackclient/volume/v2/consistency_group.py +++ b/openstackclient/volume/v2/consistency_group.py @@ -16,7 +16,12 @@ 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 import exceptions from osc_lib import utils @@ -28,27 +33,46 @@ 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 23c3f1034d..7f35a079e5 100644 --- a/openstackclient/volume/v2/consistency_group_snapshot.py +++ b/openstackclient/volume/v2/consistency_group_snapshot.py @@ -14,8 +14,15 @@ """Volume v2 consistency group snapshot action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any +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 @@ -26,10 +33,26 @@ 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 39aa99eb42..fd5fc50990 100644 --- a/openstackclient/volume/v2/qos_specs.py +++ b/openstackclient/volume/v2/qos_specs.py @@ -15,8 +15,12 @@ """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 import exceptions @@ -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 7777e7e638..fdb42f796c 100644 --- a/openstackclient/volume/v2/service.py +++ b/openstackclient/volume/v2/service.py @@ -14,6 +14,11 @@ """Service 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 @@ -24,7 +29,7 @@ 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,8 +49,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, ...] = ( "binary", @@ -86,7 +95,7 @@ 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", @@ -115,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 " @@ -123,7 +132,9 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - volume_client = self.app.client_manager.sdk_connection.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 diff --git a/openstackclient/volume/v2/volume.py b/openstackclient/volume/v2/volume.py index 61cce04f7b..c71884dd70 100644 --- a/openstackclient/volume/v2/volume.py +++ b/openstackclient/volume/v2/volume.py @@ -15,14 +15,15 @@ """Volume V2 Volume action implementations""" import argparse -import copy import functools import logging -import typing as ty +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 import exceptions @@ -43,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 @@ -61,7 +68,7 @@ def __call__(self, parser, namespace, values, option_string=None): ) -class AttachmentsColumn(cliff_columns.FormattableColumn[list[ty.Any]]): +class AttachmentsColumn(cliff_columns.FormattableColumn[list[Any]]): """Formattable column for attachments column. Unlike the parent FormattableColumn class, the initializer of the @@ -72,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 @@ -92,7 +101,7 @@ def human_readable(self): return msg -def _format_volume(volume: _volume.Volume) -> dict[str, ty.Any]: +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 = { @@ -137,7 +146,7 @@ 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 @@ -151,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", @@ -262,7 +271,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]]: self._check_size_arg(parsed_args) # size is validated in the above call to # _check_size_arg where we check that size @@ -270,7 +281,9 @@ def take_action(self, parsed_args): # volume from snapshot or source volume size = parsed_args.size - volume_client = self.app.client_manager.sdk_connection.volume + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '2' + ) image_client = self.app.client_manager.image source_volume = None @@ -366,13 +379,14 @@ def take_action(self, parsed_args): ) data = _format_volume(volume) - return zip(*sorted(data.items())) + 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", @@ -390,16 +404,25 @@ 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.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 volume in parsed_args.volumes: @@ -410,7 +433,7 @@ def take_action(self, parsed_args): volume_client.delete_volume( volume_obj.id, force=parsed_args.force, - cascade=parsed_args.purge, + cascade=parsed_args.cascade, ) except Exception as e: result += 1 @@ -434,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', @@ -470,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 @@ -548,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, ( @@ -566,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 @@ -578,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', @@ -612,8 +628,11 @@ 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' + ) + volume = volume_client.find_volume( parsed_args.volume, ignore_missing=False ) @@ -628,7 +647,7 @@ def take_action(self, parsed_args): 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', @@ -781,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: @@ -799,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': @@ -811,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) @@ -836,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) @@ -845,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) @@ -861,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) @@ -870,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) @@ -879,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( @@ -890,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' @@ -897,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: @@ -910,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 = {} @@ -925,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( _( @@ -945,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', @@ -954,20 +978,26 @@ 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 = volume_client.find_volume( parsed_args.volume, ignore_missing=False ) data = _format_volume(volume) - return zip(*sorted(data.items())) + 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', @@ -996,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) @@ -1012,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 e51e37bb9f..34e46564ef 100644 --- a/openstackclient/volume/v2/volume_backend.py +++ b/openstackclient/volume/v2/volume_backend.py @@ -14,6 +14,11 @@ """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 @@ -24,7 +29,7 @@ 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 7dbe92c962..4bd3b413a2 100644 --- a/openstackclient/volume/v2/volume_backup.py +++ b/openstackclient/volume/v2/volume_backup.py @@ -14,10 +14,14 @@ """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 openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils @@ -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,8 +437,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' + ) backup = volume_client.find_backup( parsed_args.backup, @@ -442,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", @@ -451,8 +474,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, ignore_missing=False ) diff --git a/openstackclient/volume/v2/volume_host.py b/openstackclient/volume/v2/volume_host.py index 44fd58a5cd..73fb852d31 100644 --- a/openstackclient/volume/v2/volume_host.py +++ b/openstackclient/volume/v2/volume_host.py @@ -14,6 +14,11 @@ """Volume v2 host action implementations""" +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 _ @@ -21,7 +26,7 @@ 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") @@ -37,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") @@ -65,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 3b1dbbabf2..7ef9b16775 100644 --- a/openstackclient/volume/v2/volume_snapshot.py +++ b/openstackclient/volume/v2/volume_snapshot.py @@ -14,12 +14,15 @@ """Volume v2 snapshot action implementations""" +import argparse import functools import logging -import typing as ty +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 import exceptions @@ -45,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 @@ -61,7 +66,7 @@ def human_readable(self): return volume -def _format_snapshot(snapshot: _snapshot.Snapshot) -> dict[str, ty.Any]: +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 = { @@ -100,7 +105,7 @@ def _format_snapshot(snapshot: _snapshot.Snapshot) -> dict[str, ty.Any]: 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", @@ -150,8 +155,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 = parsed_args.volume if not parsed_args.volume: @@ -186,13 +195,14 @@ def take_action(self, parsed_args): ) data = _format_snapshot(snapshot) - return zip(*sorted(data.items())) + 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", @@ -210,8 +220,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 snapshot in parsed_args.snapshots: @@ -220,7 +232,8 @@ def take_action(self, parsed_args): snapshot, ignore_missing=False ).id volume_client.delete_snapshot( - snapshot_id, force=parsed_args.force + snapshot_id, + force=parsed_args.force, ) except Exception as e: result += 1 @@ -228,8 +241,8 @@ def take_action(self, parsed_args): _( "Failed to delete snapshot with " "name or ID '%(snapshot)s': %(e)s" - ) - % {'snapshot': snapshot, 'e': e} + ), + {'snapshot': snapshot, 'e': e}, ) if result > 0: @@ -244,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', @@ -295,9 +308,14 @@ 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.sdk_connection.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', @@ -345,11 +363,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 + ) # set value of 'all_tenants' when using project option all_projects = ( @@ -359,6 +377,7 @@ def take_action(self, parsed_args): 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, @@ -384,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', @@ -440,8 +459,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' + ) snapshot = volume_client.find_snapshot( parsed_args.snapshot, ignore_missing=False @@ -499,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", @@ -508,21 +529,26 @@ 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' + ) snapshot = volume_client.find_snapshot( parsed_args.snapshot, ignore_missing=False ) data = _format_snapshot(snapshot) - return zip(*sorted(data.items())) + 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', @@ -542,8 +568,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' + ) snapshot = volume_client.find_snapshot( parsed_args.snapshot, ignore_missing=False diff --git a/openstackclient/volume/v2/volume_transfer_request.py b/openstackclient/volume/v2/volume_transfer_request.py index dcdc527625..83be03e215 100644 --- a/openstackclient/volume/v2/volume_transfer_request.py +++ b/openstackclient/volume/v2/volume_transfer_request.py @@ -14,8 +14,13 @@ """Volume v2 transfer action implementations""" +import argparse import logging +from collections.abc import Iterable, Sequence +from typing import Any +from openstack import exceptions as sdk_exceptions +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils @@ -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 e7b90af95a..1e60d15e65 100644 --- a/openstackclient/volume/v2/volume_type.py +++ b/openstackclient/volume/v2/volume_type.py @@ -14,11 +14,15 @@ """Volume v2 Type action implementations""" +import argparse +from collections.abc import MutableMapping import functools import logging -import typing as ty +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 import exceptions @@ -32,7 +36,41 @@ LOG = logging.getLogger(__name__) -class EncryptionInfoColumn(cliff_columns.FormattableColumn[ty.Any]): +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 @@ -43,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 @@ -109,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", @@ -243,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") @@ -256,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: @@ -293,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 @@ -318,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", @@ -341,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: @@ -373,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', @@ -392,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( @@ -412,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 = [ @@ -433,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. @@ -484,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', @@ -623,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 @@ -646,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"), @@ -669,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 @@ -719,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", @@ -736,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 = [ @@ -763,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( _( @@ -783,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', @@ -825,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"), @@ -862,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 5208504a35..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 +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 d99ec52b0a..7188e31922 100644 --- a/openstackclient/volume/v3/block_storage_cluster.py +++ b/openstackclient/volume/v3/block_storage_cluster.py @@ -10,7 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. -from cinderclient import api_versions +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 @@ -18,7 +23,9 @@ 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 2e2fdc5138..095cbeb1f1 100644 --- a/openstackclient/volume/v3/block_storage_log_level.py +++ b/openstackclient/volume/v3/block_storage_log_level.py @@ -14,6 +14,9 @@ """Block Storage Service action implementations""" +import argparse +from typing import Any + from openstack import utils as sdk_utils from osc_lib import exceptions @@ -27,7 +30,7 @@ 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", @@ -63,8 +66,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[list[str], list[tuple[Any, ...]]]: + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) columns = [ "Binary", "Host", @@ -98,7 +105,7 @@ 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", @@ -141,8 +148,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.32'): msg = _( diff --git a/openstackclient/volume/v3/block_storage_manage.py b/openstackclient/volume/v3/block_storage_manage.py index 78756385cc..87720ed2e6 100644 --- a/openstackclient/volume/v3/block_storage_manage.py +++ b/openstackclient/volume/v3/block_storage_manage.py @@ -14,8 +14,10 @@ """Block Storage Volume/Snapshot Management implementations""" import argparse +from collections.abc import Iterable +from typing import Any -from cinderclient import api_versions +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils @@ -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 fc564386e2..e760f5cd51 100644 --- a/openstackclient/volume/v3/block_storage_resource_filter.py +++ b/openstackclient/volume/v3/block_storage_resource_filter.py @@ -12,6 +12,10 @@ """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 import exceptions @@ -24,8 +28,12 @@ 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 eecd8e0d06..d85d42d8a1 100644 --- a/openstackclient/volume/v3/service.py +++ b/openstackclient/volume/v3/service.py @@ -14,6 +14,10 @@ """Service action implementations""" +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 @@ -25,7 +29,7 @@ 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", @@ -45,8 +49,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' + ) columns: tuple[str, ...] = ( "binary", @@ -93,7 +101,7 @@ 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", @@ -122,7 +130,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 " @@ -130,7 +138,9 @@ def take_action(self, parsed_args): ) raise exceptions.CommandError(msg) - volume_client = self.app.client_manager.sdk_connection.volume + 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 diff --git a/openstackclient/volume/v3/volume.py b/openstackclient/volume/v3/volume.py index 50ea77fb5a..71cbb4fe00 100644 --- a/openstackclient/volume/v3/volume.py +++ b/openstackclient/volume/v3/volume.py @@ -15,10 +15,10 @@ """Volume V3 Volume action implementations""" import argparse -import copy +from collections.abc import Iterable, Sequence import functools import logging -import typing as ty +from typing import Any from cliff import columns as cliff_columns from openstack.block_storage.v3 import volume as _volume @@ -44,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 @@ -62,7 +68,7 @@ def __call__(self, parser, namespace, values, option_string=None): ) -class AttachmentsColumn(cliff_columns.FormattableColumn[list[ty.Any]]): +class AttachmentsColumn(cliff_columns.FormattableColumn[list[Any]]): """Formattable column for attachments column. Unlike the parent FormattableColumn class, the initializer of the @@ -73,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 @@ -93,7 +101,7 @@ def human_readable(self): return msg -def _format_volume(volume: _volume.Volume) -> dict[str, ty.Any]: +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 = { @@ -143,7 +151,7 @@ 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 @@ -164,7 +172,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", @@ -313,7 +321,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]]: self._check_size_arg(parsed_args) # size is validated in the above call to # _check_size_arg where we check that size @@ -321,7 +331,9 @@ def take_action(self, parsed_args): # volume from snapshot, backup or source volume size = parsed_args.size - volume_client = self.app.client_manager.sdk_connection.volume + volume_client = sdk_utils.ensure_service_version( + self.app.client_manager.volume, '3' + ) image_client = self.app.client_manager.image if ( @@ -386,7 +398,8 @@ def take_action(self, parsed_args): bootable=parsed_args.bootable, ) data = _format_volume(volume) - return zip(*sorted(data.items())) + col_headers, col_data = zip(*sorted(data.items())) + return col_headers, col_data source_volume = None if parsed_args.source: @@ -491,13 +504,14 @@ def take_action(self, parsed_args): ) data = _format_volume(volume) - return zip(*sorted(data.items())) + 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", @@ -515,12 +529,19 @@ 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', + ) parser.add_argument( '--remote', action='store_true', @@ -528,13 +549,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) -> 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) @@ -550,7 +573,7 @@ def take_action(self, parsed_args): volume_client.delete_volume( volume_obj.id, force=parsed_args.force, - cascade=parsed_args.purge, + cascade=parsed_args.cascade, ) except Exception as e: result += 1 @@ -574,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', @@ -620,70 +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, - 'metadata': parsed_args.properties, - } - - 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 @@ -699,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, ( @@ -717,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 @@ -729,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", @@ -761,26 +789,38 @@ def get_parser(self, prog_name): "(possibly by another operation)" ), ) - # TODO(stephenfin): Add --cluster argument 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' + ) 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, 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', @@ -933,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: @@ -965,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) @@ -997,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) @@ -1006,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) @@ -1022,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) @@ -1031,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) @@ -1040,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( @@ -1058,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) @@ -1071,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( _( @@ -1106,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', @@ -1115,20 +1161,25 @@ 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 = volume_client.find_volume( parsed_args.volume, ignore_missing=False ) data = _format_volume(volume) - return zip(*sorted(data.items())) + 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', @@ -1157,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) @@ -1173,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) @@ -1189,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', @@ -1199,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 = _( @@ -1239,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', @@ -1251,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 3201da34bc..9ae70c3937 100644 --- a/openstackclient/volume/v3/volume_attachment.py +++ b/openstackclient/volume/v3/volume_attachment.py @@ -10,9 +10,12 @@ # License for the specific language governing permissions and limitations # under the License. +import argparse +from collections.abc import Iterable, Sequence import logging -import typing as ty +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 import exceptions @@ -32,7 +35,9 @@ ) -def _format_attachment(attachment): +def _format_attachment( + attachment: _attachment.Attachment, +) -> tuple[tuple[str, ...], Iterable[Any]]: columns = ( 'id', 'volume_id', @@ -57,7 +62,7 @@ def _format_attachment(attachment): # VolumeAttachmentManager.create returns a dict while everything else # returns a VolumeAttachment object if isinstance(attachment, dict): - data: tuple[ty.Any, ...] = () + data: tuple[Any, ...] = () for column in columns: if column == 'connection_info': data += (format_columns.DictColumn(attachment[column]),) @@ -88,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', @@ -171,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'): @@ -249,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', @@ -258,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 = _( @@ -280,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', @@ -332,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 = _( @@ -363,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', @@ -372,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 = _( @@ -388,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', @@ -432,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 = _( @@ -445,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, @@ -466,6 +489,7 @@ def take_action(self, parsed_args): search_opts=search_opts, marker=parsed_args.marker, limit=parsed_args.limit, + max_items=parsed_args.max_items, ) column_headers = ( @@ -490,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', @@ -499,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 df9a17eb03..135838196d 100644 --- a/openstackclient/volume/v3/volume_backup.py +++ b/openstackclient/volume/v3/volume_backup.py @@ -14,9 +14,12 @@ """Volume v3 Backup action implementations""" +import argparse +from collections.abc import Iterable, Sequence import copy import functools import logging +from typing import Any from cliff import columns as cliff_columns from openstack import utils as sdk_utils @@ -42,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 @@ -61,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", @@ -125,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, @@ -182,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", @@ -198,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: @@ -218,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: @@ -234,7 +245,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( '--project', @@ -299,9 +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 - identity_client = self.app.client_manager.sdk_connection.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' + ) columns: tuple[str, ...] = ( 'id', @@ -377,6 +394,7 @@ def take_action(self, parsed_args): all_tenants=all_tenants, marker=marker_backup_id, limit=parsed_args.limit, + max_items=parsed_args.max_items, project_id=project_id, ) @@ -396,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", @@ -422,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', @@ -456,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) @@ -468,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", @@ -525,8 +545,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' + ) backup = volume_client.find_backup( parsed_args.backup, @@ -608,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', @@ -627,8 +649,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.43'): msg = _( @@ -660,7 +684,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", @@ -669,8 +693,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' + ) backup = volume_client.find_backup( parsed_args.backup, ignore_missing=False ) diff --git a/openstackclient/volume/v3/volume_group.py b/openstackclient/volume/v3/volume_group.py index 1810feef56..e4e729859e 100644 --- a/openstackclient/volume/v3/volume_group.py +++ b/openstackclient/volume/v3/volume_group.py @@ -11,8 +11,11 @@ # under the License. import argparse +from collections.abc import Iterable, Sequence +from typing import Any -from cinderclient import api_versions +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 @@ -21,7 +24,7 @@ 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.get(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 530b7d5d1a..7e11f989b8 100644 --- a/openstackclient/volume/v3/volume_group_snapshot.py +++ b/openstackclient/volume/v3/volume_group_snapshot.py @@ -10,8 +10,12 @@ # 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 import exceptions from osc_lib import utils @@ -23,7 +27,9 @@ 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 bdedd25a19..4bb394daaa 100644 --- a/openstackclient/volume/v3/volume_group_type.py +++ b/openstackclient/volume/v3/volume_group_type.py @@ -10,9 +10,13 @@ # 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 import exceptions @@ -24,7 +28,9 @@ 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 b39c579469..03a6ca5d23 100644 --- a/openstackclient/volume/v3/volume_message.py +++ b/openstackclient/volume/v3/volume_message.py @@ -14,9 +14,13 @@ """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 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 @@ -25,11 +29,41 @@ 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 f89174c3da..e1d68ed693 100644 --- a/openstackclient/volume/v3/volume_snapshot.py +++ b/openstackclient/volume/v3/volume_snapshot.py @@ -14,12 +14,15 @@ """Volume v3 snapshot action implementations""" +import argparse +from collections.abc import Iterable, Sequence import functools import logging -import typing as ty +from typing import Any 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 @@ -44,11 +47,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,7 +65,7 @@ def human_readable(self): return volume -def _format_snapshot(snapshot: _snapshot.Snapshot) -> dict[str, ty.Any]: +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 = { @@ -99,7 +104,7 @@ def _format_snapshot(snapshot: _snapshot.Snapshot) -> dict[str, ty.Any]: 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", @@ -123,7 +128,9 @@ def get_parser(self, prog_name): action="store_true", default=False, help=_( - "Create a snapshot attached to an instance. Default is False" + "Allow snapshot of in-use (attached) volume. " + "Only needed for microversions prior to 3.66; " + "ignored for 3.66+" ), ) parser.add_argument( @@ -149,8 +156,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 = parsed_args.volume if not parsed_args.volume: @@ -176,22 +187,33 @@ def take_action(self, parsed_args): ) else: # Create a new snapshot from scratch - 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, - ) + # 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) - return zip(*sorted(data.items())) + 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", @@ -217,8 +239,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 if parsed_args.remote: @@ -246,8 +270,8 @@ def take_action(self, parsed_args): _( "Failed to delete snapshot with " "name or ID '%(snapshot)s': %(e)s" - ) - % {'snapshot': snapshot, 'e': e} + ), + {'snapshot': snapshot, 'e': e}, ) if result > 0: @@ -262,7 +286,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', @@ -313,9 +337,15 @@ 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.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' + ) columns: tuple[str, ...] = ( 'id', @@ -363,11 +393,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 + ) # set value of 'all_tenants' when using project option all_projects = ( @@ -377,6 +407,7 @@ def take_action(self, parsed_args): 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, @@ -402,7 +433,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', @@ -458,8 +489,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' + ) snapshot = volume_client.find_snapshot( parsed_args.snapshot, ignore_missing=False @@ -517,7 +550,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", @@ -526,21 +559,26 @@ 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' + ) snapshot = volume_client.find_snapshot( parsed_args.snapshot, ignore_missing=False ) data = _format_snapshot(snapshot) - return zip(*sorted(data.items())) + 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', @@ -560,8 +598,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' + ) snapshot = volume_client.find_snapshot( parsed_args.snapshot, ignore_missing=False diff --git a/openstackclient/volume/v3/volume_transfer_request.py b/openstackclient/volume/v3/volume_transfer_request.py index afd4626038..627b68b850 100644 --- a/openstackclient/volume/v3/volume_transfer_request.py +++ b/openstackclient/volume/v3/volume_transfer_request.py @@ -14,9 +14,13 @@ """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 openstack import exceptions as sdk_exceptions +from openstack import utils as sdk_utils from osc_lib import exceptions from osc_lib import utils @@ -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', @@ -81,7 +87,7 @@ def get_parser(self, prog_name): action='store_true', dest='snapshots', help=_( - 'Allow transfer volumes without snapshots (default) ' + 'Also transfer volume snapshots (default) ' '(supported by --os-volume-api-version 3.55 or later)' ), default=None, @@ -91,7 +97,7 @@ def get_parser(self, prog_name): action='store_false', dest='snapshots', help=_( - 'Disallow transfer volumes without snapshots ' + 'Do not transfer volume snapshots ' '(supported by --os-volume-api-version 3.55 or later)' ), ) @@ -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 fbce2f2c9a..264ea14929 100644 --- a/openstackclient/volume/v3/volume_type.py +++ b/openstackclient/volume/v3/volume_type.py @@ -13,12 +13,17 @@ """Volume v3 Type action implementations""" +import argparse +from collections.abc import Iterable, MutableMapping, Sequence import functools import logging -import typing as ty +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 import exceptions @@ -32,7 +37,41 @@ LOG = logging.getLogger(__name__) -class EncryptionInfoColumn(cliff_columns.FormattableColumn[ty.Any]): +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" + ) + 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) + ) + _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", @@ -243,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") @@ -256,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: @@ -293,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 @@ -318,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", @@ -341,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: @@ -373,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', @@ -392,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( @@ -467,8 +528,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, '3' + ) if parsed_args.long: columns = [ @@ -490,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 = {} @@ -507,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 " @@ -517,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. @@ -567,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', @@ -706,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 @@ -729,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"), @@ -752,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 @@ -802,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", @@ -819,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 = [ @@ -846,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( _( @@ -866,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', @@ -908,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"), @@ -945,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 37fb8d0c26..9964bbb60c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,28 +9,23 @@ authors = [ {name = "OpenStack", email = "openstack-discuss@lists.openstack.org"}, ] readme = {file = "README.rst", content-type = "text/x-rst"} -license = {text = "Apache-2.0"} +license = "Apache-2.0" dynamic = ["version", "dependencies"] -# dependencies = [ ] -requires-python = ">=3.10" +requires-python = ">=3.11" classifiers = [ "Environment :: OpenStack", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", - "License :: OSI Approved :: Apache Software License", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", + "Typing :: Typed", ] -# [project.optional-dependencies] -# test = [ -# ] - [project.urls] Homepage = "https://docs.openstack.org/python-openstackclient/" Repository = "https://opendev.org/openstack/python-openstackclient/" @@ -104,6 +99,7 @@ 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" @@ -124,6 +120,7 @@ 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" @@ -155,6 +152,8 @@ server_migration_abort = "openstackclient.compute.v2.server_migration:AbortMigra 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" @@ -358,6 +357,7 @@ image_metadef_namespace_delete = "openstackclient.image.v2.metadef_namespaces:De 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" @@ -396,7 +396,6 @@ 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" @@ -419,6 +418,7 @@ 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" @@ -527,6 +527,11 @@ default_security_group_rule_create = "openstackclient.network.v2.default_securit 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" @@ -540,7 +545,73 @@ 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" -# Tap-as-a-Service +[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" @@ -557,6 +628,33 @@ 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" @@ -576,6 +674,48 @@ 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" @@ -641,17 +781,17 @@ block_storage_log_level_set = "openstackclient.volume.v3.block_storage_log_level 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.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" +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" @@ -672,10 +812,10 @@ volume_backup_restore = "openstackclient.volume.v3.volume_backup:RestoreVolumeBa 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_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" @@ -691,7 +831,8 @@ volume_group_type_delete = "openstackclient.volume.v3.volume_group_type:DeleteVo 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_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" @@ -712,14 +853,14 @@ 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_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" @@ -730,32 +871,31 @@ volume_transfer_request_show = "openstackclient.volume.v3.volume_transfer_reques volume_summary = "openstackclient.volume.v3.volume:VolumeSummary" volume_revert = "openstackclient.volume.v3.volume:VolumeRevertToSnapshot" -[tool.setuptools] -packages = [ - "openstackclient" -] +[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 - | hacking - | releasenotes - ) -''' +exclude = "(?x)(doc | examples | hacking | releasenotes)" [[tool.mypy.overrides]] -module = ["openstackclient.tests.unit.*"] +module = [ + "openstackclient.tests.functional.*", +] +disallow_untyped_defs = false + +[[tool.mypy.overrides]] +module = [ + "openstackclient.tests.unit.*", +] ignore_errors = true [tool.ruff] @@ -766,7 +906,15 @@ quote-style = "preserve" docstring-code-format = true [tool.ruff.lint] -select = ["E4", "E5", "E7", "E9", "F", "S", "UP"] +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/*" = ["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-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-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-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/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-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-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/bug-2161879-34e839c2ec30e0bf.yaml b/releasenotes/notes/bug-2161879-34e839c2ec30e0bf.yaml new file mode 100644 index 0000000000..384da36495 --- /dev/null +++ b/releasenotes/notes/bug-2161879-34e839c2ec30e0bf.yaml @@ -0,0 +1,6 @@ +--- +fixes: + - | + Fix misleading help text of ``volume transfer request create --snapshots`` + and ``--no-snapshots``. + [Bug `2161879 `_] 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/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/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-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-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-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/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/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 2b28cfb92e..2b9b35f7bc 100644 --- a/releasenotes/source/index.rst +++ b/releasenotes/source/index.rst @@ -6,6 +6,7 @@ OpenStackClient Release Notes :maxdepth: 1 unreleased + 2026.1 2025.2 2025.1 2024.2 diff --git a/requirements.txt b/requirements.txt index fc31d78206..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>=4.13.0 # Apache-2.0 iso8601>=0.1.11 # MIT -openstacksdk>=4.6.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/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 1988ec8236..5c3f49b626 100644 --- a/tox.ini +++ b/tox.ini @@ -4,111 +4,113 @@ 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,-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 @@ -126,4 +128,11 @@ extension = 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