././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.3058963 python-cinderclient-8.3.0/0000775000175000017500000000000000000000000015527 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/.coveragerc0000664000175000017500000000014500000000000017650 0ustar00zuulzuul00000000000000[run] branch = True source = cinderclient omit = cinderclient/tests/* [report] ignore_errors = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/.mailmap0000664000175000017500000000174300000000000017155 0ustar00zuulzuul00000000000000Antony Messerli root Chris Behrens comstud Johannes Erdfelt jerdfelt Andy Smith termie Nikolay Sokolov Nokolay Sokolov Nikolay Sokolov Nokolay Sokolov ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/.stestr.conf0000664000175000017500000000011200000000000017772 0ustar00zuulzuul00000000000000[DEFAULT] test_path=${OS_TEST_PATH:-./cinderclient/tests/unit} top_dir=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/.zuul.yaml0000664000175000017500000000306100000000000017470 0ustar00zuulzuul00000000000000- job: name: python-cinderclient-functional-base abstract: true parent: devstack-tox-functional timeout: 4500 required-projects: - openstack/cinder - openstack/python-cinderclient vars: openrc_enable_export: true devstack_localrc: USE_PYTHON3: true VOLUME_BACKING_FILE_SIZE: 16G CINDER_QUOTA_VOLUMES: 25 CINDER_QUOTA_BACKUPS: 25 CINDER_QUOTA_SNAPSHOTS: 25 irrelevant-files: - ^.*\.rst$ - ^doc/.*$ - ^releasenotes/.*$ - ^cinderclient/tests/unit/.*$ - job: name: python-cinderclient-functional-py36 parent: python-cinderclient-functional-base # need to specify a platform that has python 3.6 available nodeset: devstack-single-node-centos-8-stream vars: python_version: 3.6 tox_envlist: functional-py36 - job: name: python-cinderclient-functional-py39 parent: python-cinderclient-functional-base nodeset: devstack-single-node-centos-9-stream vars: python_version: 3.9 tox_envlist: functional-py39 - project: templates: - check-requirements - lib-forward-testing-python3 - openstack-cover-jobs - openstack-python3-yoga-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: jobs: - python-cinderclient-functional-py36 - python-cinderclient-functional-py39 - openstack-tox-pylint: voting: false gate: jobs: - python-cinderclient-functional-py36 - python-cinderclient-functional-py39 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793255.0 python-cinderclient-8.3.0/AUTHORS0000664000175000017500000003140000000000000016575 0ustar00zuulzuul00000000000000Aaron Rosen Abhijeet Malawade Abhishek Kekane Abijitha Nadagouda Adam Gandelman Adrien Vergé Akira KAMIO Alan Bishop Alessandro Pilotti Alessio Ababilov Alex Gaynor Alex Meade Alex O'Rourke Alexander Ignatov Alexey Ovchinnikov Alexey Stupnikov Anastasia Latynskaya Andreas Jaeger Andreas Jaeger Andres Rodriguez Andrew Kerr Andrey Kurilin Anh Tran Anita Kuno Ankit Agrawal Anthony Young Anton Arefiev Armstrong Liu Atsushi SAKAI Avishay Traeger Avishay Traeger Bhuvan Arumugam Bill Arnold Boris Pavlovic Brandon Palm Brian Rosmaita Brian Waldon Brianna Poulos Cao ShuFeng Cao Shufeng Cedric Brandily Chaozhe.Chen Chaynika Saikia Chen Chmouel Boudjnah Chris Buccella Christian Berendt Christine Wang Chuck Short Cian O'Driscoll Clark Boylan Clay Gerrard Corey Bryant Cory Stone Dan Prince Davanum Srinivas Dave Chen Dean Troyer Deepti Ramakrishna Derrick J. Wippler Diane Fleming Dimitri Mazmanov Dinesh Bhor Dirk Mueller Dmitry Tantsur Dongsheng Yang Doug Hellmann Doug Hellmann Duncan Thomas Ed Balduf Eduardo Santos Edward Hope-Morley Ellen Leahy Eric Fried Eric Harney Flaper Fesp Flavio Percoco Frederic Lepied Gary W. Smith Georgy Dyuldin Geraint North Gerhard Muntingh Ghanshyam Mann Gloria Gu Gorka Eguileor Goutham Pacha Ravi Gábor Antal Haneef Ali Hangdong Zhang Herman Ge Hervé Beraud HiroyukiEguchi Huanxuan Ao Hugh Saunders Ian Cordasco Ian Wienand Igor A. Lukyanenkov Ivan Kolodyazhny Jakub Ruzicka James E. Blair Jamie Lennox Javier Pena Jay Lau Jay S Bryant Jay S. Bryant Jay S. Bryant Jeremy Liu Jeremy Stanley Joe Gordon John Griffith John Griffith John Trowbridge Jon Bernard JordanP Jose Porrua Josh Durgin Joshua Cornutt Juan Manuel Olle Julie Pichon Justin A Wilson KATO Tomoyuki KIYOHIRO ADACHI Kallebe Monteiro Karthik Prabhu Vinod Ken'ichi Ohmichi Kui Shi Kuo-tung Kao Kurt Martin Kyrylo Romanenko Lee Yarwood Liam Kelleher Lin Yang LisaLi LiuNanke Lucas H. Xu Luigi Toscano Manjeet Singh Bhatia Mark McLoughlin Mathieu Gagné Matt Fischer Matt Riedemann Matt Riedemann Matt Thompson Matthew Edmonds Michael Dovgal Michal Dulko Mike Perez Minmin Ren Mitsuhiro Tanino Monty Taylor Mykhailo Dovgal Nam Nguyen Hoai Nate Potter Nathan Reller Nathaniel Potter Neha Alhat Nicolas Simonds Nikolaj Starodubtsev Ollie Leahy Ondřej Nový OpenStack Release Bot Peter Hamilton Petr Kovar Pooja Jadhav PranaliDeore Qiu Yu Qiu Yu Rafael Weingärtner Rajat Dhasmana Rajesh Tailor Rakesh Mishra Robert Myers Rodion Tikunov Ronald Bradford Ronen Kat Rongze Zhu Rui Chen Rushi Agrawal Russell Sim Ryan McNair Sean Dague Sean McGinnis Sean McGinnis Sean McGinnis Seif Lotfy Sergey Gotliv Sergii Turivnyi Sergio Cazzolato Shao Kai Li Shaojiang Deng Sheel Rana Shilpa Jagannath SofiiaAndriichenko SongmingYan Stephen Ahn Stephen Finucane Stephen Mulcahy Steve Martinelli Steve Martinelli Steve Noyes Steven Kaufer Subhadeep De Sushil Kumar Swapnil Kulkarni (coolsvap) Swapnil Kulkarni Swapnil Kulkarni Szymon Borkowski Takashi Kajinami Terry Howe Thang Pham Thomas Bechtold Tom Hancock Tom Jose Kalapura TommyLike Tomoki Sekiyama Tovin Seven Van Hung Pham Vasyl Khomenko Victor Stinner Vieri <15050873171@163.com> Vincent Hou Vipin Balachandran Vishvananda Ishaya Vivek Agrawal Walter A. Boring IV Walter A. Boring IV Wander Way Xavier Queralt Xiao Chen Xing Yang Xu Chen Yaguang Tang Yaguang Tang Yuanbin.Chen YuehuiLei Yuriy Nesenenko Yusuke Hayashi Zhao Chao Zhengguang Zhenguo Niu Zhenguo Niu Zhi Yan Liu ZhiQiang Fan Zhiteng Huang Zhiteng Huang abhishekkekane alex bhagyashris caoyuan chenke chenxing chenying cychiang daiki kato deepak_mourya dengzhaosen dineshbhor drngsl ekudryashova fuzihao git-harry haixin haneef ali haobing1 huang.zhiping huangtianhua j-griffith jacky06 jakedahn jenny-shieh jeremy.zhang jiansong john-griffith john-griffith junboli lihaijing likui lisali liuqing liuyamin liyingjun liyingjun liyuanzhen llg8212 lrqrun ls1175 malei maxinjian melissaml nidhimittalhada obutenko pawnesh.kumar pengyuesheng pooja jadhav poojajadhav qingszhao rajat29 rajiv reedip rico.lin ricolin root saurabh scott scottda scottda scottda scottda seungjin shu-mutou sonu.kumar sri harsha mekala stmcginnis sunjia tpatil tushargite96 unknown venkatamahesh venkatamahesh wanghao wanghao wangxiyuan whoami-rajat wu.chunyang wu.shiming xianming mao xiexs xing-yang xuanyandong yanjun.fu yatin karel yenai yfzhao yrunts yuyafei zhangbailin zhangboye zhangchenchen zhangdaolong zhangyangyang zhangyanxian zheng yin zhengyin zhoulinhui zhu.boxiang zhu.rong zhubx007 zwei 翟小君 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/CONTRIBUTING.rst0000664000175000017500000000120000000000000020161 0ustar00zuulzuul00000000000000The source repository for this project can be found at: https://opendev.org/openstack/python-cinderclient Pull requests submitted through GitHub are not monitored. To start contributing to OpenStack, follow the steps in the contribution guide to set up and use Gerrit: https://docs.openstack.org/contributors/code-and-documentation/quick-start.html Bugs should be filed on Launchpad: https://bugs.launchpad.net/python-cinderclient For more specific information about contributing to this repository, see the cinderclient contributor guide: https://docs.openstack.org/python-cinderclient/latest/contributor/contributing.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793255.0 python-cinderclient-8.3.0/ChangeLog0000664000175000017500000013141200000000000017303 0ustar00zuulzuul00000000000000CHANGES ======= 8.3.0 ----- * Prepare for Yoga cinderclient release * Add volume reimage command * Move tempest requirement to functional env * Add Python 3 only classifier * Updating python testing as per Yoga testing runtime 8.2.0 ----- * Improve help text of volume create command * Correct "Increase default quotas for zuul jobs" * Add Python3 yoga unit tests * Update master for stable/xena 8.1.0 ----- * Prepare for Xena cinderclient release * Support Block Storage API mv 3.66 * Add W503 to flake8 ignores * update some scripts * Remove the unused tool scripts * Add consumes quota field support * Add functional jobs to the gate * Increase default quotas for zuul jobs * Unset tempest.lib timeout in functional tests 8.0.0 ----- * Remove v2 classes * Make instance\_uuid optional in attachment create * Remove v2 support from the shell * Dropping lower constraints testing * Remove skip\_missing\_interpreters * Run functional job on Ubuntu Focal * setup.cfg: Replace dashes with underscores * Add Python3 xena unit tests * Update master for stable/wallaby 7.4.0 ----- * Add note for Wallaby release * Update requirements for wallaby release * Dropping explicit unicode literal * Add flake8-import-order extension * Use TOX\_CONSTRAINTS\_FILE * client: Stop logging request-id twice in DEBUG * Bump API max version to 3.64 * Support passing client certificates for server version requests * Remove more python2 compat code * Remove all usage of six library * Changed minversion in tox to 3.18.0 * Move cinderclient to new hacking 4.0.0 * Doc: Functional Tests in python-cinderclient * Bump pylint to 2.6.0 * Remove install unnecessary packages * Uncap PrettyTable * Stop configuring install\_command in tox * Add MV 3.63 to the max supported version * Support backup-restore to a specific volume type or AZ 7.3.0 ----- * Update requirements and lower-constraints * Fix list resources when use limit and with-count * Fix undesirable raw Python error * doc: Update Py37 instead of py27 * Add Python3 wallaby unit tests * Update master for stable/victoria 7.2.0 ----- * Add functional-py38 job * Add note for Victoria release * Add commands for default type overrides * Python API in python-cinderclient * Remove excess whitespace in ignore-path * Use importlib to take place of imp module * zuul functional job: drop the custom playbooks * [goal] Migrate python-cinderclient jobs to focal * Add support for Cinder API mv3.61 * Bump hacking to 3.1.0 * trivial: Drop references to os-testr * use stevedore to load util plugins 7.1.0 ----- * Fix typo: dow -> down * Add doc linting to pep8 target * Use unittest.mock instead of third party mock * Add directive to document CLI * Clean up some old v1 API references * Stop to use the \_\_future\_\_ module * Switch to newer openstackdocstheme and reno versions * Add py38 package metadata * Fix pygments style * Fix hacking min version to 3.0.1 * Bump default tox env from py37 to py38 * Add py38 package metadata * Remove Babel from requirements * Add Python3 victoria unit tests * Update master for stable/ussuri 7.0.0 ----- * Add release note for Ussuri cinderclient release * Add support for Block Storage API mv 3.60 * Cleanup py27 support * Remove autogen warning * Replace bypass\_url with os\_endpoint * Remove --bypass-url documentation * Pass os\_endpoint to keystone session * Fix doc bug filing link * Ussuri contrib docs community goal * Add filters support for volume transfer 6.0.0 ----- * Drop support for python 2 * Raise hacking version to 2.0.0 * Update revert\_to\_snapshot params * Fix: --poll inconsistency * Add test for subcommands * Update hacking version * Hide cinder CLI errors on bash-completion * Update master for stable/train 5.0.0 ----- * Drop support for --sort\_key and --sort\_dir * Drop support for --allow-multiattach * Update docs to refer to PROJECT instead of TENANT * Drop support for OS\_TENANT\_NAME and OS\_TENANT\_ID * Optional filters parameters should be passed only once * Change PDF file name * Add custom CA support for get\_server\_version * Autonegotiate API version for shell * Add support for building pdf documentation * Flag safe usage of sha1 w/ #nosec * Migrate the functional job to Zuul v3 * Blacklist sphinx 2.1.0 (autodoc bug) 4.3.0 ----- * Update api-ref location * Bump openstackdocstheme to 1.20.0 * Remove the hard-coded version number * Fix: Quota update successfully executes with no params * Add Python 3 Train unit tests * Switch to the new canonical constraints URL on master * Remove promote/reenable replication * Use openstack-python3-train-jobs for python3 test runtime * Add missed 'Server ID' output in attachment-list * Update sphinx dependency * Add transfer-list --sort argument * Correct discover\_version response * Remove some old info from README * OpenDev Migration Patch * Drop 'endpoints' and 'credentials' commands * Drop support for Cinder v1 API * Add release note for major version bump * Handle auth redirects * Enable warnings-as-error for doc builds * Raise API max version for Stein updates * Add support for backup user ID * Raise API max version for Rocky updates * Drop use of git.openstack.org * Add 'is\_public' support in '--filters' option * Fix shell upload-to-image with no volume type * Remove bash-completion calls from base.py * Replace openstack.org git:// URLs with https:// * Update master for stable/stein * Add bash completion for groups * Remove py35 from setup.cfg * Tests: Don't write bash-completion cache files * Drop py35 jobs * Fix bash\_completion cache path * Fix: cinder group-list not working with non-admin user * add python 3.7 unit test job * Remove nonexistent job from gate * Add dependency on requests lib * Remove dsvm-functional-identity-v3-only job * Don't run DSVM tests for doc changes * Fix max version handling for help output * Fix incorrect punctuation * More shell completion cache additions * Fix doc build error * Re-enable shell UUID completion cache * Change bash completion dir permissions to 0750 * Change cache uniqifier from using md5 to sha-1 * Cleanup the home page * Change openstack-dev to openstack-discuss * Add Python 3.6 classifier to setup.cfg * Remove i18n.enable\_lazy() translation * Fix incorrect use of flake8:noqa * Don't quote {posargs} in tox.ini 4.1.0 ----- * Default help output to include MV updates * [Trivial] Add backup-id to 'size' param info * Fix encoding of query parameters * Fix functional error check for invalid volume create size * Use templates for cover and lower-constraints * add lib-forward-testing-python3 test job * add python 3.6 unit test job * switch documentation job to new PTI * import zuul job settings from project-config * Replace assertRaisesRegexp with assertRaisesRegex * \_\_repr\_\_ crashes when empty dict passed * Update reno for stable/rocky * refactor the getid method base.py * Fix endpoint identification for api-version query * Fix backwards compat for volume transfer < 3.55 * update wrong link 4.0.1 ----- * Allow volume-transfer creation < 3.55 microversion 4.0.0 ----- * Reflect multiattach deprecation in help text * Remove replication v1 support * Remove unnecessary parameters from volume create APIs * Update pylint to work with python 3 * Add reno to requirements * Remove deprecated CLI options * Switch from ostestr to stestr * Transfer snapshots with volumes * Add release note for ability to set attachment mode * fix tox python3 overrides * [Optimize] Update help text for hint argument * Add mode option to attachment-create * Remove initialization of logger if logger is None 3.6.1 ----- * Keep is\_public usage backwards compatible * unable to create group from src in cinderclient * Use api version 3 for functional test * Fix failing functional test cases * Remove PyPI downloads 3.6.0 ----- * Unreadable output of upload-to-image * Add the parameter service-id for service cleanup command * Remove useless args in create\_group\_from\_src * update the value of OS\_AUTH\_URL and OS\_VOLUME\_API\_VERSION * Follow the new PTI for document build * Support availability-zone in volume type * Trivial: Update pypi url to new url * Allow --help for specific commands * add lower-constraints job * Updated from global requirements * fix a typo in documentation * Update python usage docs * Correct errors in snapshot-manageable-list help text * Updated from global requirements * Add bindep.txt for system packages * Add api\_version wraps for group snapshot list in v3/shell.py * Update unit\_test.rst doc unit test py34 to py35 * Remove unit tests about run\_test * Remove unused cinderclient/apiclient/client.py module * Updated from global requirements * Update help text for encryption provider * Zuul: Remove project name * Support cross AZ backups * Zuul: Remove project name * Add api\_version wraps for some v3 volume APIs * Fix for v3 volume unit tests * Update reno for stable/queens * Updated from global requirements 3.5.0 ----- * Bump API microversion to 3.50 * Removes unicode 'u' response for "cinder get-capabilities" * Updated from global requirements * Migrate to keystoneauth identity cli opts * Add api\_version wraps for generic volume groups * Add snapshot\_id param note for backup-create * Support for reporting backend state in service list * Fix v2 volume unit tests * Fix 'cluster' paramter using for v3 volume manage 3.4.0 ----- * Updated from global requirements * Follow the new PTI for document build * Deprecate multiattach flag on volume create * Updated from global requirements * Removed unnecessary parameters from group and group\_snapshots create APIs * Backup create is not available from 3.0 to 3.42 * Fix the way to get backup metadata 3.3.0 ----- * Bump Max API version to 3.48 * Support create volume from backup in client * Bump Max version to 3.46 * Updated from global requirements * Support list with 'with\_count' in client * Remove 'end\_version' parameter in backup update * Avoid tox\_install.sh for constraints support * Fix to use "." to source script files * Migrate to Zuul v3 * Remove setting of version/release from releasenotes * Updated from global requirements * Updated from global requirements * Updated from global requirements * Revert "Bump MAX version of client to 3.45" * Use generic user for both zuul v2 and v3 * Add service cleanup command * Add cluster support in manage listings * Add cluster support in migration and manage * Add .stestr.conf configuration * Let keystoneauth set the microversion header * Bump MAX version of client to 3.45 * cleanup test-requirements * Updated from global requirements * Implement UserID in snapshot list response * Add api\_version\_wraps to attachment-complete * Implement metadata for backup create/update * Remove run\_tests.sh * Update old url for cinderclient's document * Updated from global requirements 3.2.0 ----- * Fix attachment\_id returned by create and show volume * Unsupported 'message' Exception attribute in PY3 * Enable F811, F821 * Fix method/module redefinition errors * Updated from global requirements * doc: Remove cruft from conf.py * Explicitly set 'builder' option * Use Sphinx 1.5 warning-is-error * Remove unused attribute when updating quota\_class * Add an attachment\_complete API call * Fix wrong links * Correct sphinx source code syntax * Fix OS\_AUTH\_TYPE env var usage * Fix get\_highest\_client\_server\_version with Cinder API + uWSGI * Enable H306 * Added missing column 'Allocated' * Fix man page build * Update reno for stable/pike * Updated from global requirements 3.1.0 ----- * Clean the redundant code in shell.py * Rearrange existing documentation to fit the new standard layout * import content from cli-reference in openstack-manuals * Make --profile load from environment variables * cinderclient might not return version for V2 API * python-cinderclient doc unclear on Volume.attach * Update cinder.rst and shell.rst * Add cinder create --poll * Updated from global requirements 3.0.0 ----- * Add release note for get\_highest\_client\_server\_version return type change * Update URLs in documentation * Fix highest version supported by client and server * Fix reset state v3 unit tests failures * Updated from global requirements * Support skip-validation for quota update * Remove consistencygroup quota * Switch from oslosphinx to openstackdocstheme * Updated from global requirements * Cinder attachment-\* does not support names * Support volume summary command * Dynamic log level support * cinder show with attachments is a mess * Fix support for Unicode value filters * Add pagination for snapshots, backups * Unicode value support for "--filters" * UnboundLocalError on message-list * Enabled like filter support in client * Fix cmd options for updating a quota class * [Optimize] Adds interval and increase waiting time * Fix PY2/PY3 specific error in testcases * Updated from global requirements * Remove explicit global\_request\_id from keystoneauth subclass * Fix PY2/PY3 specific error in testcases * Fix error in unit testcase * Update visibility help message * Updated from global requirements 2.2.0 ----- * Fix attribute errors in os-auth-\* settings * Do not require network for test\_noauth\_plugin() * Handle AttributeError in \_get\_server\_version\_range * Support generalized resource filter in client 2.1.0 ----- * support global\_request\_id in constructor * Support list-volume for group show * Eliminate function redefined pylint error * Updated from global requirements * Handle dashes in encryption-type-create arguments * Cleared type restrictions for metadata option * Add doc for noauth usage * Updated from global requirements * Cinder client reset-state improvements * Fix the wrong help message of marker * Pretty print 'extra\_specs' and 'group\_specs' * Tiramisu: replication group support * Fix output of update command * Support revert to snapshot in client * Fix client \`retries\` default value * Tests: Add info to assert\_called failure message * Replace http with https * Remove direct dependency on requests * Updated from global requirements * [BugFix] Make 'instance\_id' required in attachment-create CLI * [BugFix] Add 'all\_tenants', 'project\_id' in attachment-list * [BugFix] 'Mountpoint' is missing in attachment CLIs * Fix simple parameter comment error * Add description for function do\_list\_extensions in cinderclient * Replace uuid.uuid4().hex with uuidutils.generate\_uuid() * Fix noauth support * Add a missing left bracket in help message * Remove log translations * Remove duplicate do\_upload\_to\_image() method def * Update README.rst to remain consistent with python-cinderclient 2.0.1 ----- * Fix pep8 errors * Change "--sort" description in help message * Fix service-list command for API v.3.0-3.6 * Add cinder tests for cinder snapshot create commands with parameters * Fix all\_tenants doesn't work for group list 2.0.0 ----- * Make V3 the default and fixup version reporting * Remove duplicate get\_highest\_client\_server\_version * Add get\_highest\_version method * Remove unused and duplicated fake\_client module * Disable functional tests with multiattach * Fix discover\_version * Updated from global requirements * Remove cinder credentials command * Group show command should be in V3 * Update tox to delete py34 * Remove duplicate columns from list output * Updated from global requirements * Add start\_version check for do\_list() image\_metadata option * Handle log message interpolation by the logger * Add --metadata option to API v2 cinder list command again * static method to get\_highest\_client\_server\_version * Bump MAX\_VERSION to 3.27 * Update reno for stable/ocata * Add print\_function import 1.11.0 ------ * Attach/Detach V2 * static method to get\_server\_version * Fix getting metadata attr error in snapshot-list command * Fix test\_auth\_with\_keystone\_v3 test * Support filter volumes by group\_id * Updated from global requirements * Update param docstring to ducument search\_opts * Removed unnecessary 'service\_type' decorator * x-openstack-request-id logged twice in logs * Fix adding non-ascii attrs to Resource objects error * Fix v3 volume list based on image\_metadata * Updated from global requirements * Fix spelling of consistency groups * Metadata based snapshop filtering * \_human\_id\_cache or \_uuid\_cache error about completion\_cache * Enable coverage report in console output * Python3 common patterns * modify the wrong comment of the Client class * Fix the optional argument of cinder api * Add convertation of query parameters to string * (Trival)Modify the version\_header with self.version\_header * Support Keystone V3 with HttpClient 1.10.0 ------ * Updated from global requirements * Refactor v2 and v3 APIs support * Add Constraints support * Remove extra 'u' from cli output * Handle error response for webob>=1.6.0 * Update hacking version * stop adding log handler * Updated from global requirements * Replace six.iteritems(iter) with iter.items() * Show team and repo badges on README * add an alternative way of authenticating client * Minor refactoring for nested try-block * Updated from global requirements * Fix test\_version\_discovery test * Move trace ID print statement to finally * Fix typo in set unicode metadata key * Mask passwords when logging HTTP req/resp bodies * Updated from global requirements * Fix typos in the files * Optimize: add build\_query\_param method to clean code * Updated from global requirements * Move old oslo-incubator code out of openstack/common * Use 'ostestr {posargs}' to run functional tests * Fix help message for 'type-list' command * Updated from global requirements * Update release notes information for reno * Update --endpoint-type dest to os\_endpoint\_type * Remove unused keystone service catalog parse file * Help msg and output info should support il8n * Updated from global requirements * Fix some PEP8 issues and Openstack Licensing * Fix volume type 'is\_public' flag update * Print backtrace for exception when in debug mode * Enable release notes translation * Updated from global requirements * Update to current version of hacking * Replace 'MagicMock' with 'Mock' * Missing client version 3.0 support for "delete\_metadata" method * TrivialFix: Removed redundant 'the' * Import module instead of object * remove raise "e" * Fix error during set unicode metadata key * Removed multiple import from shell.py * Remove white space between print and () * Remove assertTableStruct from ClientTestBase * Parse filter item "name" correctly for snapshot-list * Showing the metadata readonly value as a separate field * Modify assertTrue * Update reno for stable/newton * Add cinder tests for cinder volume create commands with parameters 1.9.0 ----- * Deleting volume metadata keys with a single request * Add v3 user messages with pagination * Remove self.\_\_dict\_\_ for formatting strings * Wrap GroupType class's function with api\_version * Make Resource class's function can be wraped by api\_version * Wrap cluster related function with api\_version * Wrap volume\_backup's update function with api\_version * Wrap group type and group spec with api\_version * Replace functions 'Dict.get' and 'del' with 'Dict.pop' * Changed backup-restore to accept backup name * Enhance help message of upload\_to\_image * Fix NoneType error for cinderclient v1 * Fix useless api\_version of Manager class * Update the home-page with developer documentation * deprecate command \`cinder endpoints\` * Add "start\_version" and "end\_version" support to argparse * Tests for testing volume-create command * Updated from global requirements * Use 'six' instead of oslo\_utils.strutils.six * Add backup-update * Change api-version help to indicate server API * Use self.ks\_logger instead of ks\_logger * print endpoints using new keystoneauth catalog object * Add tenant\_id parameter to limits * Changed backup-restore to accept backup name * AttributeError when print service object * Replace OpenStack LLC with OpenStack Foundation * Switch to keystoneauth * Remove discover from test-requirements * List manageable volumes and snapshots * Add support for group snapshots * Add generic volume groups * Make APIVersion's null check more pythonic * Add Negative tests for cinder volume extend command * Add Negative tests for cinder volume create command * Fix Unicode error printing extra-specs * Fix string interpolation to delayed to be handled by the logging code * Add group types and group specs * Updated from global requirements * Add Python3.5 classifier and venv * Add api-version to get server versions * Updated from global requirements * Fix output error for type-show command * OS\_TENANT\_NAME is not required when we have OS\_PROJECT\_NAME * Fix \_get\_rate\_limit when resp is None * Cinder client should retry with Retry-After value * Fix batch deleting issue in volume\_type.unset\_keys() * base.Resource not define \_\_ne\_\_() built-in function * Log request-id for each api call * Add strict Boolean checking * Delete mox in cinderclient * Fix Service.\_\_repr\_\_ to remove the undefined attribute * Fix Capabilities.\_\_repr\_\_ to remove the undefined attribute * Add cluster related commands * Fix python 2,3 compatibility issue with six * Fixing parsing problem of cascade in client * Fix argument order for assertEqual to (expected, observed) * Make sure --bypass-url honored if specified * Fix "ref[project\_name]" * Updated from global requirements * Support name option for volume restore * Updated from global requirements 1.8.0 ----- * Don't enable lazy translation when loading client * Volume detail support glance\_metadata in CLI * Support for snapshot force delete * Make \_\_repr\_\_ print encryption\_id for VolumeEncryptionType class * Support for cinder backup force delete 1.7.1 ----- * Fix upload\_to\_image method * Remove deprecated tempest\_lib and use tempest.lib * Fix the incorrect alignment * Make dict.keys() PY3 compatible 1.7.0 ----- * Updated from global requirements * Don't reset volume status when resetting migration status * Support --os-key option * Change api\_version to self.api\_version * Updated from global requirements * Fix authentication issue * Remove Python 2.5 compat shim * Add options when uploading images to Glance * Only print volume ID in migration messages * Add docs for running tests * Use six.moves.urllib.parse urlencode * Updated from global requirements * Support api-microversions * Add /v3 endpoint support for cinderclient * Add pylint tox env * Add tests for delete type by name * Fix api v2 so that you can delete more than one volume\_type at a time * Fix wrong request url when retrieving multiple request * Graduate to oslo.i18n and cleanup incubator usage * Revert "Cleanup for Replication v2: remove 'replication-promote'" * Revert "Cleanup for Replication v2: remove 'replication-reenable'." * Removed Extra code * Cleanup for Replication v2: remove 'replication-reenable' * Cleanup for Replication v2: remove 'replication-promote' * fix formatting of return-request-id-to-caller release note * Update reno for stable/mitaka * Fix docstring according to function * Add docstrings for chessecake methods 1.6.0 ----- * Remove replication v2 calls * Fix Resource.\_\_eq\_\_ mismatch semantics of object equal * Update minimum tox version to 1.8 * snapshot-list now supports filtering by tenant * Fix return type in consistencygroups docstring * Updated from global requirements * Use instanceof instead of type * Add --cascade to volume delete * Don't print HTTP codes for non-HTTP errors * Add backup list sorted by data\_timestamp * Add replication v2.1 (cheesecake) calls * Add release notes for return-request-id-to-caller * Remove pypy from tox environment list * Extra 'u' in output of cinder cli commands * Add extra\_specs\_list test * Use ostestr as a tests runner * Add request\_ids attribute to resource objects * Fix return type in backup docstring * Fix omission of request\_ids returned to user * Eliminate unnecessary character * Avoid logging sensitive info in http requests * Add request\_ids attribute to resource objects * Add request\_ids attribute to resource objects * Provide consistency for Wrapper classes * Allow "cinder backup-delete" to delete multiple backups in one request * Updated from global requirements * Fix some flake8 violations * Bootable filter for listening volumes from CLI * Remove debug statement * is\_public=N/A in output of cinder type-update * Return wrapper classes with request\_ids attribute * Pass insecure option to HTTPClient * Add Wrapper classes for list, dict, tuple * Remove argparse from requirements * Fix sort problem in snapshot and backup list * Code is hosted on git.openstack.org * Fix link for OpenStack manual * Update HACKING with current information * Keep py3.X compatibility for urllib * Change extension module naming to a shorter one * Word Misspelling * Replace assertTrue(isinstance()) by optimal assert * Trival: Remove 'MANIFEST.in' * Remove openstack-common.conf * Make \_discover\_extensions public * improve readme contents * Set default service type to 'volumev2' * Updated from global requirements * Fix help message in backup reset-state * Remove the mutable default arguments "[]" * Do not require functional\_creds.conf for functional tests * Fix for 'quota-delete' call to API v2 * Removes MANIFEST.in as it is not needed explicitely by PBR * Drop py33 support * Add to\_dict method to Resource class * Add reno for release notes management * Deprecated tox -downloadcache option removed * Updated from global requirements * Updated from global requirements * Pass proxy environment variable to tox * Add optional argument to list subcommand * Remove py26 support * Delete python bytecode before every test run * support for snapshot management * Fix comma location in comment 1.5.0 ----- * Remove ureachable code in fakes.py * Updated from global requirements * Adds v2 replication support * Fix v2 qos-key command * Add functional tests: backup creation and deletion * Use oslo\_utils encodeutils and strutils * CLI for backup snapshots * Update CONTRIBUTING.md to CONTRIBUTING.rst * Update help message for cinder migrate * Fix volume size units to match the API * Fix functional tests fail on the env with https * Add the version attribute to the Client class * Adding backup-reset-state to CLI * Put py34 first in the env order of tox * Implement cinder type-show * Updated from global requirements * Update release notes for 1.3.1 and 1.4.0 releases * Remove duplicate code in functional tests * Add note for broken cinderclient versions 1.2.? * Adding pagination to snapshots and backups lists * Updated from global requirements * Add commands to show image metadata * Fully support os-endpoint-type * Fix three resources not being deleted by using name * Use dictionary literal for dictionary creation * Change ignore-errors to ignore\_errors * Updated from global requirements * Updating volume type 'is\_public' status support * No longer ignores CINDER\_SERVICE\_NAME 1.4.0 ----- * Fix incorrect exception message in cinderclient * Remove unused code from cinderclient.utils module * Add a period for the description string of a argument * Updated from global requirements * Update path to subunit2html in post\_test\_hook * Adds command to fetch specified backend capabilities * Volume status management for volume migration * Fixed test\_password\_prompted * Fix help message for reset-state commands * Add functional tests for python-cinderclient * Add support '--all-tenants' for cinder backup-list * CLI: Non-disruptive backup * Add tests for python-cinderclient * Replace assertEqual(None, \*) with assertIsNone in tests * CLI: Clone CG * Fix ClientException init when there is no message on py34 * Fixes table when there are multiline in result data * Set default OS\_VOLUME\_API\_VERSION to '2' * Add commands for modifying image metadata * Updated from global requirements * Remove H302 * Show backup and volume info in backup\_restore * Add response message when volume delete * Implement reset-state for attach\_status and migration\_status * Add more details for replication * New mock release(1.1.0) broke unit/function tests * Remove unnecessary check for tenant information * Remove redundant statement and refactor 1.3.1 ----- * Prep for 1.3.0 * Updated from global requirements * Revert "Enable version discovery" * Fix typo in comment message * Use shared shell arguments provided by Session * Add set\_management\_url to cinderclient.client * cinderclient does not honor --os-region-name or ENV[OS\_REGION\_NAME] * Updated from global requirements * Add encryption-type-update to cinderclient * Set max volume size limit for the tenant * Add tests for python-cinderclient and style fix * The is\_public filter in VolumeTypeManager.list broke the find in VolumeTypeAccessManager, which caused all commands which should have taken a volume\_type name to fail. I have choosen to fix this by effectively removing the filter in the client (and the --all argument to type-list). This is OK, since Cinder implements the filtering by user anyway. The consequence of this change is that the Admin user will always see the entire list (including private types) every time they execute type-list * Add volume multi attach support * Fix functional post test\_hook * Fix condition in CheckSizeArgForCreate parser action 1.2.2 ----- * Prep for 1.2.2 * Updated from global requirements * Support host-attach of volumes * Bump hacking to >=0.10.0,<0.11 to fix failure of gate pep8 * Fix functional readonly\_cli tests * Add findall server side filtering * Fix functional tests and tox 2.0 errors * Fixed typos and repeated docstrings * cinderclient deprecated endpoint\_type needs dest= * cinderclient no longer honors --endpoint-type * Add functional post test\_hook * Add --os-endpoint-type to match other services * Find resource refactoring * Updated from global requirements * Avoid \_get\_keystone\_session() if auth\_plugin 1.2.1 ----- * Update release notes for 1.2.0 and 1.2.1 * Add CLI read-only functional tests * Change --force parameter into boolean * Updated from global requirements * Add search\_opts into the method list() for qos specs * Add version removal rule to stop discovery warning * V2:cinder create --image option doesn't work * Create Consistency Group from CG Snapshot CLI * Kilo Consistency Group CLI update * Remove "OPTIONAL:" from optional argument help text (v2) * Support pagination param limit in volume list in V1 * Update README to work with release tools * Add support to incremental backups in cinder 1.2.0 ----- * Enable version discovery * Remove print statement in unit test * Add ability to specify path var to testr * Uncap library requirements for liberty * cinder list now supports filter by tenant * Remove duplicate find request in find\_resource * Allow cinderclient to handle exception response * Move unit tests into test directory * Add covhtml to gitignore * bash\_completion now shows only subcommands when subcommand is "help" * Update to change name for volume type client * cinder list now prints dash '-' when data is None * cinderclient accepts arguments after metadata without -- separator * Updated help on cinder reset-state cli * Fix outdated URLs and some minor fixes * Fixes quota-class-update commands * Adopt CLI sorting argument guidelines * Add missing all-tenants option to transfer-list 1.3.0 ----- * Add -d short option for --debug * Fix volume\_transfers import in v2 * Updated from global requirements * reset-state should warn that it is DB only * Expose cinder's scheduler pool API * Fix up help message for reset-state call * Add tests for consistency groups and cgsnapshots * cinder list fails with 'name' sort key * Remove commented code in cinderclient/v1/volumes.py * Make cinderclient metadata CLI output consistent * Leverage openstack.common.importutils import\_class * v2 error message grammatical error * Add command to show pool information for backends * Client output is not sorted by --sort\_key * Add support for os-volume-type-access extension * Added type description for volume type client * Don't use sessions if third party plugin is used * Workflow documentation is now in infra-manual * List all the request items when the list is over osapi\_max\_limit * Allow CG quota to be showed and updated * Add the parameter bypass\_url to the cinder client * Use newer features from keystoneclient * Support Volume Backup Quota * Updated from global requirements * Add ability to create volume from image by image name * Remove cinderclient/tests from coverage report * Fix 'search\_opts' error with backup delete command * Remove unused methods from utils.py * Fix incorrect variable name * cinderclient does not retry with TimeoutException * Adds tty password entry for cinderclient * Don't git ignore .mailmap and .testr.conf * gitignore /.\* * Add profiling support to cinderclient * Fix volume name support of unmanage and replication commands * Simplify cinder manage command args * Add swap and it's variants to gitignore * Docstring of unmanage subcommand is missing 1.1.1 ----- * Update version to 1.1.1 in index * Remove Python 2.4 compat shim * Enables debug mode for keystone session object * Stop using intersphinx 1.1.0 ----- * Make required option for create cg * Update index file for release of 1.1.0 * client HTTPClient \_\_init\_\_ fails if auth\_url None * Cinder Client for Consistency Groups * Fixed typos found by RETF rules * Work toward Python 3.4 support and testing * Use adapter from keystoneclient * Ability to pass metadata during snapshot create * Updated from global requirements * Add client support in Cinder for volume replication * convert availability zone tests to requests-mock * Convert snapshot tests to requests-mock * Replace httpretty with requests-mock * Fix order of arguments in assertEqual * Avoid extra lookups in extra-specs-list * Support pagination for volume list * Mask passwords in client debug output * Move debug logging to shell * Quotaset update does not return result * Fix the return code of the command reset-state * Update theme for docs * Change "Connection refused" to "Connection error" * Add a tox job for generating docs * Retry when connection to cinder is refused * Add commands for managing and unmanaging volumes * Fix comment in tearDown() * sync latest strutils to python-cinderclient * Optional size parameter for volume creation * Use suitable assert * Use immutable arg rather mutable arg * Fix version discovery and auth\_plugins * Add CONTRIBUTING.rst * Add tenant uuid when running cinder list --all-tenants * Remove deprecated command-line args * Updated from global requirements * Added support for keystone v3client * Updated from global requirements * Remove "OPTIONAL:" from optional argument help text * Mark cinderclient as being a universal wheel * Update help strings for cinder client 1.0.9 ----- * Bump client doc index version to 1.0.9 * Ambiguous option error should not appear if Arg is SUPPRESS * Fix malformed encryption-type body in test cases * Reuse Resource from oslo * Use region\_name in service catalog * Use real timestamps in authentication tests * Accept deleting multiple snapshots in one shot * Add set-bootable command * Add quota-delete command to cinder client * Updated from global requirements * Include the Python 3/3.3 trove classifiers * Fix Volume.extend and Volume.update\_readonly\_flag methods * CLI for disable service reason * Fix usage of v1 and v2 availability zones * Updated from global requirements * replace assertTrue(isinstance) to assertIsInstance * Pretty print of endpoints for ambiguous error * Client support for export and import backup service metadata * When there is no error body return the HTTP reason * Import access module from keystoneclient to handle V3 endpoints * Allow list\_extensions to work in cinderclient v2 * Update service function name for service enable * Update doc string for service disable * Fix type-delete to allow deletion by name and ID * Fix typos in the volumes and snapshots docstrings * Set v2 commands available for v2 service\_type * Updated from global requirements * Wrong hint key in volume create function * Add auth\_plugin support to cinderclient * Remove dependent module py3kcompat * Require ctrl\_location for encryption-type-create * Remove vim header * Remove call to undefined install.post\_process() * Remove tox locale overrides * Fix typos in cinderclient 1.0.8 ----- * Revert "Update cinderclient to skip the additional GET during create" * Update release notes for push to pypi * Fixed image\_name from image-name in upload-to-image * Add shell tests for snapshot\_delete * Sync latest apiclient code from Oslo * Sync up with oslo-incubator * Add retype to index.rst * Add volume retype command * Remove RAX-specific auth in cinderclient * disable/enable a service has no output * Fix 'search\_opts' unexpected keyword argument error * Remove dependencies on pep8, pyflakes and flake8 * Remove copyright from empty files * Unit tests for limits.py in cinderclient/v1 and v2 * Update cinderclient to skip the additional GET during create * Fix RateLimit.\_\_repr\_\_ - self.method is undefined * Fix inappropriate comment for volume update api * Updates .gitignore for environment files * Updates .gitignore * Discrepancy between README.rst and cinder help * Updated from global requirements * Fix broken argument name in v2 volume extend routine * Updates tox.ini to use new features * Reset-state and snapshot-reset-state for multiple objects * Ignore swap files generated during file edting by vim * Add search\_opts into the method list() for VolumeTypeManager * Add assert to delete multiple test * Fix typo in cinderclient * Fix string representation of VolumeBackupsRestore * Fix inappropriate comment for set\_metadata * Update HACKING.rst with release note requirement * Add encryption-type-delete to cinderclient * Updated from global requirements * change assertEquals to assertEqual * Add index.rst section for patches merged to master * Update link in HACKING.rst and Make it DRYer 1.0.7 ----- * Update version and index.rst for push * Fix py33 due to readonly and metadata patches * Enable del of other tenants resources by name * Adding volume readonly-mode-update interface to Cinder client * Enable "cinder delete" can delete multiple volumes in one request * Adding Read-Only volume attaching support to Cinder client * Deprecates --volume-id arg for v2 backup-restore * Override endpoint URL check for API version * Fixes broken v1 and v2 api backup-restore * Addition of volume/snapshot\_metadata CLI * python3: iteration order of dict is unpredictable * python3: align the order of parameters for urlencode() * python3: sort dict for post\_volumes\_1234\_action test * python3: Refactor dict for python2/python3 compat * Updated from global requirements * Fix DeprecationWarning when printing exception * Fix the failure of fetching the version in cinder endpoint 1.0.6 ----- * Update docs/index.rst with release info for 1.0.6 * Add quota-usage command * Synch up with OSLO-Incubator * Implement qos support * Fix find volume for migrate command * Replace OpenStack LLC with OpenStack Foundation * Error if arguments are not supplied for rename commands * Use v2 endpoint with v2 shell for migration * Add volume name arguments * Implement ability to migrate volume * Fix help messages for name arguments * Added support for running the tests under PyPy with tox * Don't need to init testr explicitly * Add update\_snapshot\_metadata action * Add volume encryption metadata to cinderclient * Sync strutils from oslo * Fixing erroneous clearing of test callstack * Fixing malformed assert message formatting * Add commandline option --metadata for cinder list * Add print for "backup-create" command * Add missing babel dependency * Add support for multiple cinder endpoints * Updated from global requirements * python3: Fix tox requirements 1.0.5 ----- * Add a couple more things to index before release * Sync with global requirements * convert third-party exception to ConnectionError * Provide cinder CLI man page * Revert "Add evaluation of --force parameter when creating snapshots" * Add timeout parameter in requests * Remove locals() from cinder client code base * Add evaluation of --force parameter when creating snapshots * Updating HACKING file * Add availability-zone-list command * Revert "Use exceptions from oslo" * Changes for volume type quotas * Update to latest openstack/requirements * Add print to the upload-to-image command * Add os-services extension support * Update index.rst * Fix wrong method call for extend subcommand * Implement ability to extend volume for v1 * Sync install\_venv\_common from oslo * Enable ability to reset state on snapshots * Implement ability to extend volume * Use exceptions from oslo * Fix volume info display error on create with v2 * Implement reset-state (os-reset\_status) action * Connectivity between the endpoint version and OS\_VOLUME\_API\_VERSION * python3: Strutils is not needed * python3: Fix traceback while running tests * Implements support migration for volume transfer * python3: Fix traceback while running tests * python3: Fix import compatibility * python3: Fix unicode strings * python3: Update for metaclasses * python3: fix imports compatibility * python3: Basic python3 compatibility * python3: compatibility for iteritems and iterkeys * Remove explicit depend on distribute * python3: Drop mox dependency * Start Gating on Pyflakes and Hacking * Add \`snapshots\` key support for quota class update * python3: Introduce py33 to tox.ini * Update run\_tests and bring back colorizer * Set the correct location for the tests * Only add logging handlers if there currently aren't any * Move tests into cinderclient package * Rename requires files to standard names * Migrate to pbr * Implement scheduler hints for APIv2 * Make ManagerWithFind abstract and fix its descendants * Migrate to flake8 * Add .coveragerc file to show correct code coverage * Allow generator as input to utils.print\_list * Fixed do\_create() in v2 shell * Add license information 1.0.4 ----- * Update release info in index.rst * Update setup.py prior to next upload to pypi * Add support for volume backups * Fixed unit test name in v1 and v2 tests * Don't print the empty table on list operations * Sync with oslo-incubator copy of setup.py and version.py * Minor typo/message fixes * Remove unused "import sys" * Fix result -> resp typo in unset\_keys * Fix X-Auth\_Token -> X-Auth-Token header name * Pin prettytable versions 1.0.3 ----- * Decodes input and encodes output * Add OS\_TENANT\_ID as authentication option * Remove unused tools/rfc.sh * Add debug option processing to run\_tests * Add support for snapshot quotas * Catch KeyboardInterrupt * Docs in cinderlcient were never actually updated * Debug output the http body * Fix typo breaking --debug option to cinder client * Fix upload-to-image volume\_id help * Handle metadata args the same for all calls * adding v2 support to cinderclient * Allow requests 0.8 and greater * Re-add setuptools-git to setup.py * Avoid UnicodeEncodeError exception on exception * Correct parsing of volume metadata * Update to latest oslo version code * Fixed documentation of the cinder shell command * Change Nova -> Cinder in credentials error message * Add ability to call force\_delete from cinderclient * Move from nose to testr * Fixed Version Functionality * Add upload-to-image function to client * Add access to update volume metadata * Move from unittest2 to testtools 1.0.2 ----- * Add list-extensions capability to cinderclient * Use requests module for HTTP/HTTPS * Port some additional logging changes from novaclient * Bring back the output from client.http\_log() * Add clone volume support to cinderclient * Update to swapped versioninfo logic * Align cinderclient version code 1.0.1 ----- * Adding bootable column to volume list view * Pin pep8 to 1.3.3 * show help when calling without arguments * Add retries to cinderclient * Fixes setup compatibility issue on Windows * Revert "Add retries to cinderclient." * Add retries to cinderclient * Remove extra-specs from types-list command output * Remove attach/detach code from cinderclient * Add python\_cinderclient.egg-info to .gitignore * Fix support for Unicode volume names * Add OpenStack trove classifier for PyPI * Add volume\_type extra\_specs support to client * add rename and snapshot-rename commands 1.0.0 ----- * Show volume and snapshot data on create * Fix some pep8 issues * Remove unused methods in FakeHTTPClient and unused unittests * Patch for bug #1004382 * add tenant\_id and make projectid optional * Add begin\_detaching and roll\_detaching functions * Add filter options to list and snapshot-list * Fixes bug 1045777 * Implement volume quota support in the cinderclient * Fix PEP8 issues * Change '\_' to '-' in options * Correct param comments in docstring * Add the test environment for the virtualenv * Add nosehtmloutput as a test dependency * Add ability to provide metadata on volume creation * Add image id arg to create 0.2 --- * Add availability\_zone support for volume creation * Rename bash completion file from nova to cinder * Add all\_tenants flag to snapshots and volumes * Add missing parameters to volume create body * Bump pep8 to 1.2 * Add post-tag versioning * Add support for extended\_snapshot\_attributes * Add client work for new cinder extensions * Move docs to doc * Set pep8 version to 1.1 in test\_requires * Auto generate AUTHORS file for python-cinderclient component * Align setup.py and tox with standards 0.0 --- * Initial split from python-novaclient ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/HACKING.rst0000664000175000017500000000275400000000000017335 0ustar00zuulzuul00000000000000Cinder Client Style Commandments ================================ - Step 1: Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ - Step 2: Read on Cinder Client Specific Commandments ----------------------------------- General ------- - Use 'raise' instead of 'raise e' to preserve original traceback or exception being reraised:: except Exception as e: ... raise e # BAD except Exception: ... raise # OKAY Release Notes ------------- - Any patch that makes a change significant to the end consumer or deployer of an OpenStack environment should include a release note (new features, upgrade impacts, deprecated functionality, significant bug fixes, etc.) - Cinder Client uses Reno for release notes management. See the `Reno Documentation`_ for more details on its usage. .. _Reno Documentation: https://docs.openstack.org/reno/latest/ - As a quick example, when adding a new shell command for Awesome Storage Feature, one could perform the following steps to include a release note for the new feature:: $ tox -e venv -- reno new add-awesome-command $ vi releasenotes/notes/add-awesome-command-bb8bb8bb8bb8bb81.yaml Remove the extra template text from the release note and update the details so it looks something like:: --- features: - Added shell command `cinder be-awesome` for Awesome Storage Feature. - Include the generated release notes file when submitting your patch for review. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/LICENSE0000664000175000017500000002707500000000000016547 0ustar00zuulzuul00000000000000Copyright (c) 2009 Jacob Kaplan-Moss - initial codebase (< v2.1) Copyright (c) 2011 Rackspace - OpenStack extensions (>= v2.1) All rights reserved. Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. --- License for python-cinderclient versions prior to 2.1 --- All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 3. Neither the name of this project nor the names of its contributors may be used to endorse or promote products derived from this software without specific prior written permission. THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.3058963 python-cinderclient-8.3.0/PKG-INFO0000664000175000017500000005243500000000000016635 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: python-cinderclient Version: 8.3.0 Summary: OpenStack Block Storage API Client Library Home-page: https://docs.openstack.org/python-cinderclient/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-cinderclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on Python bindings to the OpenStack Cinder API =========================================== .. image:: https://img.shields.io/pypi/v/python-cinderclient.svg :target: https://pypi.org/project/python-cinderclient/ :alt: Latest Version This is a client for the OpenStack Cinder API. There's a Python API (the ``cinderclient`` module), and a command-line script (``cinder``). Each implements 100% of the OpenStack Cinder API. See the `OpenStack CLI Reference`_ for information on how to use the ``cinder`` command-line tool. You may also want to look at the `OpenStack API documentation`_. .. _OpenStack CLI Reference: https://docs.openstack.org/python-openstackclient/latest/cli/ .. _OpenStack API documentation: https://docs.openstack.org/api-quick-start/ The project is hosted on `Launchpad`_, where bugs can be filed. The code is hosted on `OpenStack`_. Patches must be submitted using `Gerrit`_. .. _OpenStack: https://opendev.org/openstack/python-cinderclient .. _Launchpad: https://launchpad.net/python-cinderclient .. _Gerrit: https://docs.openstack.org/infra/manual/developers.html#development-workflow * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.org/project/python-cinderclient .. _Online Documentation: https://docs.openstack.org/python-cinderclient/latest/ .. _Blueprints: https://blueprints.launchpad.net/python-cinderclient .. _Bugs: https://bugs.launchpad.net/python-cinderclient .. _Source: https://opendev.org/openstack/python-cinderclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/cinder-specs/ .. contents:: Contents: :local: Command-line API ---------------- Installing this package gets you a shell command, ``cinder``, that you can use to interact with any Rackspace compatible API (including OpenStack). You'll need to provide your OpenStack username and password. You can do this with the ``--os-username``, ``--os-password`` and ``--os-tenant-name`` params, but it's easier to just set them as environment variables:: export OS_USERNAME=openstack export OS_PASSWORD=yadayada export OS_TENANT_NAME=myproject You will also need to define the authentication url with ``--os-auth-url`` and the version of the API with ``--os-volume-api-version``. Or set them as environment variables as well. Since Block Storage API V2 is officially deprecated, you are encouraged to set ``OS_VOLUME_API_VERSION=3``. If you are using Keystone, you need to set the ``OS_AUTH_URL`` to the keystone endpoint:: export OS_AUTH_URL=http://controller:5000/v3 export OS_VOLUME_API_VERSION=3 Since Keystone can return multiple regions in the Service Catalog, you can specify the one you want with ``--os-region-name`` (or ``export OS_REGION_NAME``). It defaults to the first in the list returned. You'll find complete documentation on the shell by running ``cinder help``:: usage: cinder [--version] [-d] [--os-auth-system ] [--service-type ] [--service-name ] [--volume-service-name ] [--os-endpoint-type ] [--endpoint-type ] [--os-volume-api-version ] [--retries ] [--profile HMAC_KEY] [--os-auth-strategy ] [--os-username ] [--os-password ] [--os-tenant-name ] [--os-tenant-id ] [--os-auth-url ] [--os-user-id ] [--os-user-domain-id ] [--os-user-domain-name ] [--os-project-id ] [--os-project-name ] [--os-project-domain-id ] [--os-project-domain-name ] [--os-region-name ] [--os-token ] [--os-url ] [--insecure] [--os-cacert ] [--os-cert ] [--os-key ] [--timeout ] ... Command-line interface to the OpenStack Cinder API. Positional arguments: absolute-limits Lists absolute limits for a user. api-version Display the server API version information. (Supported by API versions 3.0 - 3.latest) availability-zone-list Lists all availability zones. backup-create Creates a volume backup. backup-delete Removes one or more backups. backup-export Export backup metadata record. backup-import Import backup metadata record. backup-list Lists all backups. backup-reset-state Explicitly updates the backup state. backup-restore Restores a backup. backup-show Shows backup details. cgsnapshot-create Creates a cgsnapshot. cgsnapshot-delete Removes one or more cgsnapshots. cgsnapshot-list Lists all cgsnapshots. cgsnapshot-show Shows cgsnapshot details. consisgroup-create Creates a consistency group. consisgroup-create-from-src Creates a consistency group from a cgsnapshot or a source CG. consisgroup-delete Removes one or more consistency groups. consisgroup-list Lists all consistency groups. consisgroup-show Shows details of a consistency group. consisgroup-update Updates a consistency group. create Creates a volume. credentials Shows user credentials returned from auth. delete Removes one or more volumes. encryption-type-create Creates encryption type for a volume type. Admin only. encryption-type-delete Deletes encryption type for a volume type. Admin only. encryption-type-list Shows encryption type details for volume types. Admin only. encryption-type-show Shows encryption type details for a volume type. Admin only. encryption-type-update Update encryption type information for a volume type (Admin Only). endpoints Discovers endpoints registered by authentication service. extend Attempts to extend size of an existing volume. extra-specs-list Lists current volume types and extra specs. failover-host Failover a replicating cinder-volume host. force-delete Attempts force-delete of volume, regardless of state. freeze-host Freeze and disable the specified cinder-volume host. get-capabilities Show backend volume stats and properties. Admin only. get-pools Show pool information for backends. Admin only. image-metadata Sets or deletes volume image metadata. image-metadata-show Shows volume image metadata. list Lists all volumes. manage Manage an existing volume. metadata Sets or deletes volume metadata. metadata-show Shows volume metadata. metadata-update-all Updates volume metadata. migrate Migrates volume to a new host. qos-associate Associates qos specs with specified volume type. qos-create Creates a qos specs. qos-delete Deletes a specified qos specs. qos-disassociate Disassociates qos specs from specified volume type. qos-disassociate-all Disassociates qos specs from all its associations. qos-get-association Lists all associations for specified qos specs. qos-key Sets or unsets specifications for a qos spec. qos-list Lists qos specs. qos-show Shows qos specs details. quota-class-show Lists quotas for a quota class. quota-class-update Updates quotas for a quota class. quota-defaults Lists default quotas for a tenant. quota-delete Delete the quotas for a tenant. quota-show Lists quotas for a tenant. quota-update Updates quotas for a tenant. quota-usage Lists quota usage for a tenant. rate-limits Lists rate limits for a user. readonly-mode-update Updates volume read-only access-mode flag. rename Renames a volume. reset-state Explicitly updates the volume state in the Cinder database. retype Changes the volume type for a volume. service-disable Disables the service. service-enable Enables the service. service-list Lists all services. Filter by host and service binary. (Supported by API versions 3.0 - 3.latest) set-bootable Update bootable status of a volume. show Shows volume details. snapshot-create Creates a snapshot. snapshot-delete Removes one or more snapshots. snapshot-list Lists all snapshots. snapshot-manage Manage an existing snapshot. snapshot-metadata Sets or deletes snapshot metadata. snapshot-metadata-show Shows snapshot metadata. snapshot-metadata-update-all Updates snapshot metadata. snapshot-rename Renames a snapshot. snapshot-reset-state Explicitly updates the snapshot state. snapshot-show Shows snapshot details. snapshot-unmanage Stop managing a snapshot. thaw-host Thaw and enable the specified cinder-volume host. transfer-accept Accepts a volume transfer. transfer-create Creates a volume transfer. transfer-delete Undoes a transfer. transfer-list Lists all transfers. transfer-show Shows transfer details. type-access-add Adds volume type access for the given project. type-access-list Print access information about the given volume type. type-access-remove Removes volume type access for the given project. type-create Creates a volume type. type-default List the default volume type. type-delete Deletes volume type or types. type-key Sets or unsets extra_spec for a volume type. type-list Lists available 'volume types'. type-show Show volume type details. type-update Updates volume type name, description, and/or is_public. unmanage Stop managing a volume. upload-to-image Uploads volume to Image Service as an image. version-list List all API versions. (Supported by API versions 3.0 - 3.latest) bash-completion Prints arguments for bash_completion. help Shows help about this program or one of its subcommands. list-extensions Optional arguments: --version show program's version number and exit -d, --debug Shows debugging output. --os-auth-system Defaults to env[OS_AUTH_SYSTEM]. --service-type Service type. For most actions, default is volume. --service-name Service name. Default=env[CINDER_SERVICE_NAME]. --volume-service-name Volume service name. Default=env[CINDER_VOLUME_SERVICE_NAME]. --os-endpoint Use this API endpoint instead of the Service Catalog. Default=env[CINDER_ENDPOINT] --os-endpoint-type Endpoint type, which is publicURL or internalURL. Default=env[OS_ENDPOINT_TYPE] or nova env[CINDER_ENDPOINT_TYPE] or publicURL. --endpoint-type DEPRECATED! Use --os-endpoint-type. --os-volume-api-version Block Storage API version. Accepts X, X.Y (where X is major and Y is minor part).Default=env[OS_VOLUME_API_VERSION]. --retries Number of retries. --profile HMAC_KEY HMAC key to use for encrypting context data for performance profiling of operation. This key needs to match the one configured on the cinder api server. Without key the profiling will not be triggered even if osprofiler is enabled on server side. Defaults to env[OS_PROFILE]. --os-auth-strategy Authentication strategy (Env: OS_AUTH_STRATEGY, default keystone). For now, any other value will disable the authentication. --os-username OpenStack user name. Default=env[OS_USERNAME]. --os-password Password for OpenStack user. Default=env[OS_PASSWORD]. --os-tenant-name Tenant name. Default=env[OS_TENANT_NAME]. --os-tenant-id ID for the tenant. Default=env[OS_TENANT_ID]. --os-auth-url URL for the authentication service. Default=env[OS_AUTH_URL]. --os-user-id Authentication user ID (Env: OS_USER_ID). --os-user-domain-id OpenStack user domain ID. Defaults to env[OS_USER_DOMAIN_ID]. --os-user-domain-name OpenStack user domain name. Defaults to env[OS_USER_DOMAIN_NAME]. --os-project-id Another way to specify tenant ID. This option is mutually exclusive with --os-tenant-id. Defaults to env[OS_PROJECT_ID]. --os-project-name Another way to specify tenant name. This option is mutually exclusive with --os-tenant-name. Defaults to env[OS_PROJECT_NAME]. --os-project-domain-id Defaults to env[OS_PROJECT_DOMAIN_ID]. --os-project-domain-name Defaults to env[OS_PROJECT_DOMAIN_NAME]. --os-region-name Region name. Default=env[OS_REGION_NAME]. --os-token Defaults to env[OS_TOKEN]. --os-url Defaults to env[OS_URL]. API Connection Options: Options controlling the HTTP API Connections --insecure Explicitly allow client to perform "insecure" TLS (https) requests. The server's certificate will not be verified against any certificate authorities. This option should be used with caution. --os-cacert Specify a CA bundle file to use in verifying a TLS (https) server certificate. Defaults to env[OS_CACERT]. --os-cert Defaults to env[OS_CERT]. --os-key Defaults to env[OS_KEY]. --timeout Set request timeout (in seconds). Run "cinder help SUBCOMMAND" for help on a subcommand. If you want to get a particular version API help message, you can add ``--os-volume-api-version `` in help command, like this:: cinder --os-volume-api-version 3.28 help Python API ---------- There's also a complete Python API, but it has not yet been documented. Quick-start using keystone:: # use v3 auth with http://controller:5000/v3 >>> from cinderclient.v3 import client >>> nt = client.Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) >>> nt.volumes.list() [...] See release notes and more at ``_. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Requires-Python: >=3.6 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/README.rst0000664000175000017500000004301100000000000017215 0ustar00zuulzuul00000000000000======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-cinderclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on Python bindings to the OpenStack Cinder API =========================================== .. image:: https://img.shields.io/pypi/v/python-cinderclient.svg :target: https://pypi.org/project/python-cinderclient/ :alt: Latest Version This is a client for the OpenStack Cinder API. There's a Python API (the ``cinderclient`` module), and a command-line script (``cinder``). Each implements 100% of the OpenStack Cinder API. See the `OpenStack CLI Reference`_ for information on how to use the ``cinder`` command-line tool. You may also want to look at the `OpenStack API documentation`_. .. _OpenStack CLI Reference: https://docs.openstack.org/python-openstackclient/latest/cli/ .. _OpenStack API documentation: https://docs.openstack.org/api-quick-start/ The project is hosted on `Launchpad`_, where bugs can be filed. The code is hosted on `OpenStack`_. Patches must be submitted using `Gerrit`_. .. _OpenStack: https://opendev.org/openstack/python-cinderclient .. _Launchpad: https://launchpad.net/python-cinderclient .. _Gerrit: https://docs.openstack.org/infra/manual/developers.html#development-workflow * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.org/project/python-cinderclient .. _Online Documentation: https://docs.openstack.org/python-cinderclient/latest/ .. _Blueprints: https://blueprints.launchpad.net/python-cinderclient .. _Bugs: https://bugs.launchpad.net/python-cinderclient .. _Source: https://opendev.org/openstack/python-cinderclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/cinder-specs/ .. contents:: Contents: :local: Command-line API ---------------- Installing this package gets you a shell command, ``cinder``, that you can use to interact with any Rackspace compatible API (including OpenStack). You'll need to provide your OpenStack username and password. You can do this with the ``--os-username``, ``--os-password`` and ``--os-tenant-name`` params, but it's easier to just set them as environment variables:: export OS_USERNAME=openstack export OS_PASSWORD=yadayada export OS_TENANT_NAME=myproject You will also need to define the authentication url with ``--os-auth-url`` and the version of the API with ``--os-volume-api-version``. Or set them as environment variables as well. Since Block Storage API V2 is officially deprecated, you are encouraged to set ``OS_VOLUME_API_VERSION=3``. If you are using Keystone, you need to set the ``OS_AUTH_URL`` to the keystone endpoint:: export OS_AUTH_URL=http://controller:5000/v3 export OS_VOLUME_API_VERSION=3 Since Keystone can return multiple regions in the Service Catalog, you can specify the one you want with ``--os-region-name`` (or ``export OS_REGION_NAME``). It defaults to the first in the list returned. You'll find complete documentation on the shell by running ``cinder help``:: usage: cinder [--version] [-d] [--os-auth-system ] [--service-type ] [--service-name ] [--volume-service-name ] [--os-endpoint-type ] [--endpoint-type ] [--os-volume-api-version ] [--retries ] [--profile HMAC_KEY] [--os-auth-strategy ] [--os-username ] [--os-password ] [--os-tenant-name ] [--os-tenant-id ] [--os-auth-url ] [--os-user-id ] [--os-user-domain-id ] [--os-user-domain-name ] [--os-project-id ] [--os-project-name ] [--os-project-domain-id ] [--os-project-domain-name ] [--os-region-name ] [--os-token ] [--os-url ] [--insecure] [--os-cacert ] [--os-cert ] [--os-key ] [--timeout ] ... Command-line interface to the OpenStack Cinder API. Positional arguments: absolute-limits Lists absolute limits for a user. api-version Display the server API version information. (Supported by API versions 3.0 - 3.latest) availability-zone-list Lists all availability zones. backup-create Creates a volume backup. backup-delete Removes one or more backups. backup-export Export backup metadata record. backup-import Import backup metadata record. backup-list Lists all backups. backup-reset-state Explicitly updates the backup state. backup-restore Restores a backup. backup-show Shows backup details. cgsnapshot-create Creates a cgsnapshot. cgsnapshot-delete Removes one or more cgsnapshots. cgsnapshot-list Lists all cgsnapshots. cgsnapshot-show Shows cgsnapshot details. consisgroup-create Creates a consistency group. consisgroup-create-from-src Creates a consistency group from a cgsnapshot or a source CG. consisgroup-delete Removes one or more consistency groups. consisgroup-list Lists all consistency groups. consisgroup-show Shows details of a consistency group. consisgroup-update Updates a consistency group. create Creates a volume. credentials Shows user credentials returned from auth. delete Removes one or more volumes. encryption-type-create Creates encryption type for a volume type. Admin only. encryption-type-delete Deletes encryption type for a volume type. Admin only. encryption-type-list Shows encryption type details for volume types. Admin only. encryption-type-show Shows encryption type details for a volume type. Admin only. encryption-type-update Update encryption type information for a volume type (Admin Only). endpoints Discovers endpoints registered by authentication service. extend Attempts to extend size of an existing volume. extra-specs-list Lists current volume types and extra specs. failover-host Failover a replicating cinder-volume host. force-delete Attempts force-delete of volume, regardless of state. freeze-host Freeze and disable the specified cinder-volume host. get-capabilities Show backend volume stats and properties. Admin only. get-pools Show pool information for backends. Admin only. image-metadata Sets or deletes volume image metadata. image-metadata-show Shows volume image metadata. list Lists all volumes. manage Manage an existing volume. metadata Sets or deletes volume metadata. metadata-show Shows volume metadata. metadata-update-all Updates volume metadata. migrate Migrates volume to a new host. qos-associate Associates qos specs with specified volume type. qos-create Creates a qos specs. qos-delete Deletes a specified qos specs. qos-disassociate Disassociates qos specs from specified volume type. qos-disassociate-all Disassociates qos specs from all its associations. qos-get-association Lists all associations for specified qos specs. qos-key Sets or unsets specifications for a qos spec. qos-list Lists qos specs. qos-show Shows qos specs details. quota-class-show Lists quotas for a quota class. quota-class-update Updates quotas for a quota class. quota-defaults Lists default quotas for a tenant. quota-delete Delete the quotas for a tenant. quota-show Lists quotas for a tenant. quota-update Updates quotas for a tenant. quota-usage Lists quota usage for a tenant. rate-limits Lists rate limits for a user. readonly-mode-update Updates volume read-only access-mode flag. rename Renames a volume. reset-state Explicitly updates the volume state in the Cinder database. retype Changes the volume type for a volume. service-disable Disables the service. service-enable Enables the service. service-list Lists all services. Filter by host and service binary. (Supported by API versions 3.0 - 3.latest) set-bootable Update bootable status of a volume. show Shows volume details. snapshot-create Creates a snapshot. snapshot-delete Removes one or more snapshots. snapshot-list Lists all snapshots. snapshot-manage Manage an existing snapshot. snapshot-metadata Sets or deletes snapshot metadata. snapshot-metadata-show Shows snapshot metadata. snapshot-metadata-update-all Updates snapshot metadata. snapshot-rename Renames a snapshot. snapshot-reset-state Explicitly updates the snapshot state. snapshot-show Shows snapshot details. snapshot-unmanage Stop managing a snapshot. thaw-host Thaw and enable the specified cinder-volume host. transfer-accept Accepts a volume transfer. transfer-create Creates a volume transfer. transfer-delete Undoes a transfer. transfer-list Lists all transfers. transfer-show Shows transfer details. type-access-add Adds volume type access for the given project. type-access-list Print access information about the given volume type. type-access-remove Removes volume type access for the given project. type-create Creates a volume type. type-default List the default volume type. type-delete Deletes volume type or types. type-key Sets or unsets extra_spec for a volume type. type-list Lists available 'volume types'. type-show Show volume type details. type-update Updates volume type name, description, and/or is_public. unmanage Stop managing a volume. upload-to-image Uploads volume to Image Service as an image. version-list List all API versions. (Supported by API versions 3.0 - 3.latest) bash-completion Prints arguments for bash_completion. help Shows help about this program or one of its subcommands. list-extensions Optional arguments: --version show program's version number and exit -d, --debug Shows debugging output. --os-auth-system Defaults to env[OS_AUTH_SYSTEM]. --service-type Service type. For most actions, default is volume. --service-name Service name. Default=env[CINDER_SERVICE_NAME]. --volume-service-name Volume service name. Default=env[CINDER_VOLUME_SERVICE_NAME]. --os-endpoint Use this API endpoint instead of the Service Catalog. Default=env[CINDER_ENDPOINT] --os-endpoint-type Endpoint type, which is publicURL or internalURL. Default=env[OS_ENDPOINT_TYPE] or nova env[CINDER_ENDPOINT_TYPE] or publicURL. --endpoint-type DEPRECATED! Use --os-endpoint-type. --os-volume-api-version Block Storage API version. Accepts X, X.Y (where X is major and Y is minor part).Default=env[OS_VOLUME_API_VERSION]. --retries Number of retries. --profile HMAC_KEY HMAC key to use for encrypting context data for performance profiling of operation. This key needs to match the one configured on the cinder api server. Without key the profiling will not be triggered even if osprofiler is enabled on server side. Defaults to env[OS_PROFILE]. --os-auth-strategy Authentication strategy (Env: OS_AUTH_STRATEGY, default keystone). For now, any other value will disable the authentication. --os-username OpenStack user name. Default=env[OS_USERNAME]. --os-password Password for OpenStack user. Default=env[OS_PASSWORD]. --os-tenant-name Tenant name. Default=env[OS_TENANT_NAME]. --os-tenant-id ID for the tenant. Default=env[OS_TENANT_ID]. --os-auth-url URL for the authentication service. Default=env[OS_AUTH_URL]. --os-user-id Authentication user ID (Env: OS_USER_ID). --os-user-domain-id OpenStack user domain ID. Defaults to env[OS_USER_DOMAIN_ID]. --os-user-domain-name OpenStack user domain name. Defaults to env[OS_USER_DOMAIN_NAME]. --os-project-id Another way to specify tenant ID. This option is mutually exclusive with --os-tenant-id. Defaults to env[OS_PROJECT_ID]. --os-project-name Another way to specify tenant name. This option is mutually exclusive with --os-tenant-name. Defaults to env[OS_PROJECT_NAME]. --os-project-domain-id Defaults to env[OS_PROJECT_DOMAIN_ID]. --os-project-domain-name Defaults to env[OS_PROJECT_DOMAIN_NAME]. --os-region-name Region name. Default=env[OS_REGION_NAME]. --os-token Defaults to env[OS_TOKEN]. --os-url Defaults to env[OS_URL]. API Connection Options: Options controlling the HTTP API Connections --insecure Explicitly allow client to perform "insecure" TLS (https) requests. The server's certificate will not be verified against any certificate authorities. This option should be used with caution. --os-cacert Specify a CA bundle file to use in verifying a TLS (https) server certificate. Defaults to env[OS_CACERT]. --os-cert Defaults to env[OS_CERT]. --os-key Defaults to env[OS_KEY]. --timeout Set request timeout (in seconds). Run "cinder help SUBCOMMAND" for help on a subcommand. If you want to get a particular version API help message, you can add ``--os-volume-api-version `` in help command, like this:: cinder --os-volume-api-version 3.28 help Python API ---------- There's also a complete Python API, but it has not yet been documented. Quick-start using keystone:: # use v3 auth with http://controller:5000/v3 >>> from cinderclient.v3 import client >>> nt = client.Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) >>> nt.volumes.list() [...] See release notes and more at ``_. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/bindep.txt0000664000175000017500000000072700000000000017537 0ustar00zuulzuul00000000000000# This is a cross-platform list tracking distribution packages needed by tests; # see https://docs.openstack.org/infra/bindep/ for additional information. gettext libffi-dev [platform:dpkg] libffi-devel [platform:rpm] libssl-dev [platform:ubuntu-xenial] locales [platform:debian] python-dev [platform:dpkg] python-devel [platform:rpm !platform:centos-8] python3-all-dev [platform:ubuntu !platform:ubuntu-precise] python3-dev [platform:dpkg] python3-devel [platform:rpm] ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.2658958 python-cinderclient-8.3.0/cinderclient/0000775000175000017500000000000000000000000020172 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/__init__.py0000664000175000017500000000165200000000000022307 0ustar00zuulzuul00000000000000# Copyright (c) 2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pbr.version __all__ = ['__version__'] version_info = pbr.version.VersionInfo('python-cinderclient') # We have a circular import problem when we first run python setup.py sdist # It's harmless, so deflect it. try: __version__ = version_info.version_string() except AttributeError: __version__ = None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/_i18n.py0000664000175000017500000000235600000000000021470 0ustar00zuulzuul00000000000000# Copyright 2016 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. """oslo.i18n integration module. See https://docs.openstack.org/oslo.i18n/latest/user/usage.html . """ import oslo_i18n DOMAIN = "cinderclient" _translators = oslo_i18n.TranslatorFactory(domain=DOMAIN) # The primary translation function using the well-known name "_" _ = _translators.primary # The contextual translation function using the name "_C" # requires oslo.i18n >=2.1.0 _C = _translators.contextual_form # The plural translation function using the name "_P" # requires oslo.i18n >=2.1.0 _P = _translators.plural_form def get_available_languages(): return oslo_i18n.get_available_languages(DOMAIN) def enable_lazy(): return oslo_i18n.enable_lazy() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/api_versions.py0000664000175000017500000003722000000000000023251 0ustar00zuulzuul00000000000000# # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import functools import logging import re from oslo_utils import strutils from cinderclient._i18n import _ from cinderclient import exceptions from cinderclient import utils LOG = logging.getLogger(__name__) # key is unsupported version, value is appropriate supported alternative REPLACEMENT_VERSIONS = {"1": "3", "2": "3"} MAX_VERSION = "3.68" MIN_VERSION = "3.0" _SUBSTITUTIONS = {} _type_error_msg = "'%(other)s' should be an instance of '%(cls)s'" class APIVersion(object): """This class represents an API Version with convenience methods for manipulation and comparison of version numbers that we need to do to implement microversions. """ def __init__(self, version_str=None): """Create an API version object.""" self.ver_major = 0 self.ver_minor = 0 if version_str is not None: match = re.match(r"^([1-9]\d*)\.([1-9]\d*|0|latest)$", version_str) if match: self.ver_major = int(match.group(1)) if match.group(2) == "latest": # NOTE(andreykurilin): Infinity allows to easily determine # latest version and doesn't require any additional checks # in comparison methods. self.ver_minor = float("inf") else: self.ver_minor = int(match.group(2)) else: msg = (_("Invalid format of client version '%s'. " "Expected format 'X.Y', where X is a major part and Y " "is a minor part of version.") % version_str) raise exceptions.UnsupportedVersion(msg) def __str__(self): """Debug/Logging representation of object.""" if self.is_latest(): return "Latest API Version Major: %s" % self.ver_major return ("API Version Major: %s, Minor: %s" % (self.ver_major, self.ver_minor)) def __repr__(self): if self: return "" % self.get_string() return "" def __bool__(self): return self.ver_major != 0 or self.ver_minor != 0 __nonzero__ = __bool__ def is_latest(self): return self.ver_minor == float("inf") def __lt__(self, other): if not isinstance(other, APIVersion): raise TypeError(_type_error_msg % {"other": other, "cls": self.__class__}) return ((self.ver_major, self.ver_minor) < (other.ver_major, other.ver_minor)) def __eq__(self, other): if not isinstance(other, APIVersion): raise TypeError(_type_error_msg % {"other": other, "cls": self.__class__}) return ((self.ver_major, self.ver_minor) == (other.ver_major, other.ver_minor)) def __gt__(self, other): if not isinstance(other, APIVersion): raise TypeError(_type_error_msg % {"other": other, "cls": self.__class__}) return ((self.ver_major, self.ver_minor) > (other.ver_major, other.ver_minor)) def __le__(self, other): return self < other or self == other def __ne__(self, other): return not self.__eq__(other) def __ge__(self, other): return self > other or self == other def matches(self, min_version, max_version=None): """Returns whether the version object represents a version greater than or equal to the minimum version and less than or equal to the maximum version. :param min_version: Minimum acceptable version. :param max_version: Maximum acceptable version. :returns: boolean If min_version is null then there is no minimum limit. If max_version is null then there is no maximum limit. If self is null then raise ValueError """ if not self: raise ValueError("Null APIVersion doesn't support 'matches'.") if isinstance(min_version, str): min_version = APIVersion(version_str=min_version) if isinstance(max_version, str): max_version = APIVersion(version_str=max_version) # This will work when they are None and when they are version 0.0 if not min_version and not max_version: return True if not max_version: return min_version <= self if not min_version: return self <= max_version return min_version <= self <= max_version def get_string(self): """Converts object to string representation which if used to create an APIVersion object results in the same version. """ if not self: raise ValueError("Null APIVersion cannot be converted to string.") elif self.is_latest(): return "%s.%s" % (self.ver_major, "latest") return "%s.%s" % (self.ver_major, self.ver_minor) def get_major_version(self): return "%s" % self.ver_major class VersionedMethod(object): def __init__(self, name, start_version, end_version, func): """Versioning information for a single method :param name: Name of the method :param start_version: Minimum acceptable version :param end_version: Maximum acceptable_version :param func: Method to call Minimum and maximums are inclusive """ self.name = name self.start_version = start_version self.end_version = end_version self.func = func def __str__(self): return ("Version Method %s: min: %s, max: %s" % (self.name, self.start_version, self.end_version)) def __repr__(self): return "" % self.name def get_available_major_versions(): # NOTE: the discovery code previously here assumed that if a v2 # module exists, it must contain a client. This will be False # during the transition period when the v2 client is removed but # we are still using other classes in that module. Right now there's # only one client version available, so we simply hard-code it. return ['3'] def check_major_version(api_version): """Checks major part of ``APIVersion`` obj is supported. :raises cinderclient.exceptions.UnsupportedVersion: if major part is not supported """ available_versions = get_available_major_versions() if (api_version and str(api_version.ver_major) not in available_versions): if len(available_versions) == 1: msg = ("Invalid client version '%(version)s'. " "Major part should be '%(major)s'") % { "version": api_version.get_string(), "major": available_versions[0]} else: msg = ("Invalid client version '%(version)s'. " "Major part must be one of: '%(major)s'") % { "version": api_version.get_string(), "major": ", ".join(available_versions)} raise exceptions.UnsupportedVersion(msg) def get_api_version(version_string): """Returns checked APIVersion object""" version_string = str(version_string) if version_string in REPLACEMENT_VERSIONS: LOG.warning("Version %(old)s is not supported, use " "supported version %(now)s instead.", {"old": version_string, "now": REPLACEMENT_VERSIONS[version_string]}) if strutils.is_int_like(version_string): version_string = "%s.0" % version_string api_version = APIVersion(version_string) check_major_version(api_version) return api_version def _get_server_version_range(client): try: versions = client.services.server_api_version() except AttributeError: # Wrong client was used, translate to something helpful. raise exceptions.UnsupportedVersion( _('Invalid client version %s to get server version range. Only ' 'the v3 client is supported for this operation.') % client.version) if not versions: msg = _("Server does not support microversions. You cannot use this " "version of the cinderclient with the requested server. " "Try using a cinderclient version less than 8.0.0.") raise exceptions.UnsupportedVersion(msg) for version in versions: if '3.' in version.version: return APIVersion(version.min_version), APIVersion(version.version) # if we're still here, there's nothing we understand in the versions msg = _("You cannot use this version of the cinderclient with the " "requested server.") raise exceptions.UnsupportedVersion(msg) def get_highest_version(client): """Queries the server version info and returns highest supported microversion :param client: client object :returns: APIVersion """ server_start_version, server_end_version = _get_server_version_range( client) return server_end_version def discover_version(client, requested_version): """Checks ``requested_version`` and returns the most recent version supported by both the API and the client. :param client: client object :param requested_version: requested version represented by APIVersion obj :returns: APIVersion """ server_start_version, server_end_version = _get_server_version_range( client) _validate_server_version(server_start_version, server_end_version) # get the highest version the server can handle relative to the # requested version valid_version = _validate_requested_version( requested_version, server_start_version, server_end_version) # see if we need to downgrade for the client client_max = APIVersion(MAX_VERSION) if client_max < valid_version: msg = _("Requested version %(requested_version)s is " "not supported. Downgrading requested version " "to %(actual_version)s.") LOG.debug(msg, { "requested_version": requested_version, "actual_version": client_max}) valid_version = client_max return valid_version def _validate_requested_version(requested_version, server_start_version, server_end_version): """Validates the requested version. Checks 'requested_version' is within the min/max range supported by the server. If 'requested_version' is not within range then attempts to downgrade to 'server_end_version'. Otherwise an UnsupportedVersion exception is thrown. :param requested_version: requestedversion represented by APIVersion obj :param server_start_version: APIVersion object representing server min :param server_end_version: APIVersion object representing server max """ valid_version = requested_version if not requested_version.matches(server_start_version, server_end_version): if server_end_version <= requested_version: if (APIVersion(MIN_VERSION) <= server_end_version and server_end_version <= APIVersion(MAX_VERSION)): msg = _("Requested version %(requested_version)s is " "not supported. Downgrading requested version " "to %(server_end_version)s.") LOG.debug(msg, { "requested_version": requested_version, "server_end_version": server_end_version}) valid_version = server_end_version else: raise exceptions.UnsupportedVersion( _("The specified version isn't supported by server. The valid " "version range is '%(min)s' to '%(max)s'") % { "min": server_start_version.get_string(), "max": server_end_version.get_string()}) return valid_version def _validate_server_version(server_start_version, server_end_version): """Validates the server version. Checks that the 'server_end_version' is greater than the minimum version supported by the client. Then checks that the 'server_start_version' is less than the maximum version supported by the client. :param server_start_version: :param server_end_version: :return: """ if APIVersion(MIN_VERSION) > server_end_version: raise exceptions.UnsupportedVersion( _("Server's version is too old. The client's valid version range " "is '%(client_min)s' to '%(client_max)s'. The server valid " "version range is '%(server_min)s' to '%(server_max)s'.") % { 'client_min': MIN_VERSION, 'client_max': MAX_VERSION, 'server_min': server_start_version.get_string(), 'server_max': server_end_version.get_string()}) elif APIVersion(MAX_VERSION) < server_start_version: raise exceptions.UnsupportedVersion( _("Server's version is too new. The client's valid version range " "is '%(client_min)s' to '%(client_max)s'. The server valid " "version range is '%(server_min)s' to '%(server_max)s'.") % { 'client_min': MIN_VERSION, 'client_max': MAX_VERSION, 'server_min': server_start_version.get_string(), 'server_max': server_end_version.get_string()}) def update_headers(headers, api_version): """Set 'OpenStack-API-Version' header if api_version is not null """ if api_version and api_version.ver_minor != 0: headers["OpenStack-API-Version"] = "volume " + api_version.get_string() def add_substitution(versioned_method): _SUBSTITUTIONS.setdefault(versioned_method.name, []) _SUBSTITUTIONS[versioned_method.name].append(versioned_method) def get_substitutions(func_name, api_version=None): substitutions = _SUBSTITUTIONS.get(func_name, []) if api_version: return [m for m in substitutions if api_version.matches(m.start_version, m.end_version)] return substitutions def wraps(start_version, end_version=None): start_version = APIVersion(start_version) if end_version: end_version = APIVersion(end_version) else: end_version = APIVersion("%s.latest" % start_version.ver_major) def decor(func): func.versioned = True name = utils.get_function_name(func) versioned_method = VersionedMethod(name, start_version, end_version, func) add_substitution(versioned_method) @functools.wraps(func) def substitution(obj, *args, **kwargs): methods = get_substitutions(name, obj.api_version) if not methods: raise exceptions.VersionNotFoundForAPIMethod( obj.api_version.get_string(), name) method = max(methods, key=lambda f: f.start_version) return method.func(obj, *args, **kwargs) if hasattr(func, 'arguments'): for cli_args, cli_kwargs in func.arguments: utils.add_arg(substitution, *cli_args, **cli_kwargs) return substitution return decor ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.2658958 python-cinderclient-8.3.0/cinderclient/apiclient/0000775000175000017500000000000000000000000022142 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/apiclient/__init__.py0000664000175000017500000000000000000000000024241 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/apiclient/base.py0000664000175000017500000004357300000000000023442 0ustar00zuulzuul00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 OpenStack Foundation # Copyright 2012 Grid Dynamics # Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Base utilities to build API operation managers and objects on top of.""" # E1102: %s is not callable # pylint: disable=E1102 import abc import copy from oslo_utils import encodeutils from oslo_utils import strutils from requests import Response from cinderclient.apiclient import exceptions from cinderclient import utils def getid(obj): """Return id if argument is a Resource. Abstracts the common pattern of allowing both an object or an object's ID (UUID) as a parameter when dealing with relationships. """ if getattr(obj, 'uuid', None): return obj.uuid else: return getattr(obj, 'id', obj) # TODO(aababilov): call run_hooks() in HookableMixin's child classes class HookableMixin(object): """Mixin so classes can register and run hooks.""" _hooks_map = {} @classmethod def add_hook(cls, hook_type, hook_func): """Add a new hook of specified type. :param cls: class that registers hooks :param hook_type: hook type, e.g., '__pre_parse_args__' :param hook_func: hook function """ if hook_type not in cls._hooks_map: cls._hooks_map[hook_type] = [] cls._hooks_map[hook_type].append(hook_func) @classmethod def run_hooks(cls, hook_type, *args, **kwargs): """Run all hooks of specified type. :param cls: class that registers hooks :param hook_type: hook type, e.g., '__pre_parse_args__' :param **args: args to be passed to every hook function :param **kwargs: kwargs to be passed to every hook function """ hook_funcs = cls._hooks_map.get(hook_type) or [] for hook_func in hook_funcs: hook_func(*args, **kwargs) class BaseManager(HookableMixin): """Basic manager type providing common operations. Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. """ resource_class = None def __init__(self, client): """Initializes BaseManager with `client`. :param client: instance of BaseClient descendant for HTTP requests """ super(BaseManager, self).__init__() self.client = client def _list(self, url, response_key, obj_class=None, json=None): """List the collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, e.g., 'servers' :param obj_class: class for constructing the returned objects (self.resource_class will be used by default) :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) """ if json: body = self.client.post(url, json=json).json() else: body = self.client.get(url).json() if obj_class is None: obj_class = self.resource_class data = body[response_key] # NOTE(ja): keystone returns values as list as {'values': [ ... ]} # unlike other services which just return the list... try: data = data['values'] except (KeyError, TypeError): pass return [obj_class(self, res, loaded=True) for res in data if res] def _get(self, url, response_key): """Get an object from collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, e.g., 'server' """ body = self.client.get(url).json() return self.resource_class(self, body[response_key], loaded=True) def _head(self, url): """Retrieve request headers for an object. :param url: a partial URL, e.g., '/servers' """ resp = self.client.head(url) return resp.status_code == 204 def _post(self, url, json, response_key, return_raw=False): """Create an object. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'servers' :param return_raw: flag to force returning raw JSON instead of Python object of self.resource_class """ body = self.client.post(url, json=json).json() if return_raw: return body[response_key] return self.resource_class(self, body[response_key]) def _put(self, url, json=None, response_key=None): """Update an object with PUT method. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'servers' """ resp = self.client.put(url, json=json) # PUT requests may not return a body if resp.content: body = resp.json() if response_key is not None: return self.resource_class(self, body[response_key]) else: return self.resource_class(self, body) def _patch(self, url, json=None, response_key=None): """Update an object with PATCH method. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'servers' """ body = self.client.patch(url, json=json).json() if response_key is not None: return self.resource_class(self, body[response_key]) else: return self.resource_class(self, body) def _delete(self, url): """Delete an object. :param url: a partial URL, e.g., '/servers/my-server' """ return self.client.delete(url) class ManagerWithFind(BaseManager, metaclass=abc.ABCMeta): """Manager with additional `find()`/`findall()` methods.""" @abc.abstractmethod def list(self): pass def find(self, **kwargs): """Find a single item with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) raise exceptions.NotFound(msg) elif num_matches > 1: raise exceptions.NoUniqueMatch() else: return matches[0] def findall(self, **kwargs): """Find all items with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ found = [] searches = kwargs.items() for obj in self.list(): try: if all(getattr(obj, attr) == value for (attr, value) in searches): found.append(obj) except AttributeError: continue return found class CrudManager(BaseManager): """Base manager class for manipulating entities. Children of this class are expected to define a `collection_key` and `key`. - `collection_key`: Usually a plural noun by convention (e.g. `entities`); used to refer collections in both URL's (e.g. `/v3/entities`) and JSON objects containing a list of member resources (e.g. `{'entities': [{}, {}, {}]}`). - `key`: Usually a singular noun by convention (e.g. `entity`); used to refer to an individual member of the collection. """ collection_key = None key = None def build_url(self, base_url=None, **kwargs): """Builds a resource URL for the given kwargs. Given an example collection where `collection_key = 'entities'` and `key = 'entity'`, the following URL's could be generated. By default, the URL will represent a collection of entities, e.g.:: /entities If kwargs contains an `entity_id`, then the URL will represent a specific member, e.g.:: /entities/{entity_id} :param base_url: if provided, the generated URL will be appended to it """ url = base_url if base_url is not None else '' url += '/%s' % self.collection_key # do we have a specific entity? entity_id = kwargs.get('%s_id' % self.key) if entity_id is not None: url += '/%s' % entity_id return url def _filter_kwargs(self, kwargs): """Drop null values and handle ids.""" for key, ref in kwargs.copy().items(): if ref is None: kwargs.pop(key) else: if isinstance(ref, Resource): kwargs.pop(key) kwargs['%s_id' % key] = getid(ref) return kwargs def create(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._post( self.build_url(**kwargs), {self.key: kwargs}, self.key) def get(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._get( self.build_url(**kwargs), self.key) def head(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._head(self.build_url(**kwargs)) def list(self, base_url=None, **kwargs): """List the collection. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) return self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), 'query': utils.build_query_param(kwargs), }, self.collection_key) def put(self, base_url=None, **kwargs): """Update an element. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) return self._put(self.build_url(base_url=base_url, **kwargs)) def update(self, **kwargs): kwargs = self._filter_kwargs(kwargs) params = kwargs.copy() params.pop('%s_id' % self.key) return self._patch( self.build_url(**kwargs), {self.key: params}, self.key) def delete(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._delete( self.build_url(**kwargs)) def find(self, base_url=None, **kwargs): """Find a single item with attributes matching ``**kwargs``. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) rl = self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), 'query': '?%s' % utils.build_query_param(kwargs), }, self.collection_key) num = len(rl) if num == 0: msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) raise exceptions.NotFound(404, msg) elif num > 1: raise exceptions.NoUniqueMatch else: return rl[0] class Extension(HookableMixin): """Extension descriptor.""" SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') manager_class = None def __init__(self, name, module): super(Extension, self).__init__() self.name = name self.module = module self._parse_extension_module() def _parse_extension_module(self): self.manager_class = None for attr_name, attr_value in self.module.__dict__.items(): if attr_name in self.SUPPORTED_HOOKS: self.add_hook(attr_name, attr_value) else: try: if issubclass(attr_value, BaseManager): self.manager_class = attr_value except TypeError: pass def __repr__(self): return "" % self.name class RequestIdMixin(object): """Wrapper class to expose x-openstack-request-id to the caller.""" def setup(self): self.x_openstack_request_ids = [] @property def request_ids(self): return self.x_openstack_request_ids def append_request_ids(self, resp): """Add request_ids as an attribute to the object :param resp: list, Response object or string """ if resp is None: return if isinstance(resp, list): # Add list of request_ids if response is of type list. for resp_obj in resp: self._append_request_id(resp_obj) else: # Add request_ids if response contains single object. self._append_request_id(resp) def _append_request_id(self, resp): if isinstance(resp, Response): # Extract 'x-openstack-request-id' from headers if # response is a Response object. request_id = resp.headers.get('x-openstack-request-id') self.x_openstack_request_ids.append(request_id) else: # If resp is of type string (in case of encryption type list) self.x_openstack_request_ids.append(resp) class Resource(RequestIdMixin): """Base class for OpenStack resources (tenant, user, etc.). This is pretty much just a bag for attributes. """ HUMAN_ID = False NAME_ATTR = 'name' def __init__(self, manager, info, loaded=False, resp=None): """Populate and bind to a manager. :param manager: BaseManager object :param info: dictionary representing resource attributes :param loaded: prevent lazy-loading if set to True :param resp: Response or list of Response objects """ self.manager = manager self._info = info self._add_details(info) self._loaded = loaded if resp and hasattr(resp, "headers"): self._checksum = resp.headers.get("Etag") self.setup() self.append_request_ids(resp) def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k not in ['manager', 'x_openstack_request_ids']) info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) @property def human_id(self): """Human-readable ID which can be used for bash completion. """ if self.NAME_ATTR in self.__dict__ and self.HUMAN_ID: return strutils.to_slug(getattr(self, self.NAME_ATTR)) return None def _add_details(self, info): for (k, v) in info.items(): try: setattr(self, k, v) except AttributeError: # In this case we already defined the attribute on the class continue except UnicodeEncodeError: setattr(self, encodeutils.safe_encode(k), v) self._info[k] = v def __getattr__(self, k): if k not in self.__dict__ or k not in self._info: # NOTE(bcwaldon): disallow lazy-loading if already loaded once if not self.is_loaded(): self.get() return self.__getattr__(k) raise AttributeError(k) else: if k in self.__dict__: return self.__dict__[k] return self._info[k] @property def api_version(self): return self.manager.api_version def get(self): # set_loaded() first ... so if we have to bail, we know we tried. self.set_loaded(True) if not hasattr(self.manager, 'get'): return new = self.manager.get(self.id) if new: self._add_details(new._info) def __eq__(self, other): if not isinstance(other, Resource): return NotImplemented # two resources of different types are not equal if not isinstance(other, self.__class__): return False return self._info == other._info def __ne__(self, other): return not self.__eq__(other) def is_loaded(self): return self._loaded def set_loaded(self, val): self._loaded = val def to_dict(self): return copy.deepcopy(self._info) class ListWithMeta(list, RequestIdMixin): def __init__(self, values, resp): super(ListWithMeta, self).__init__(values) self.setup() self.append_request_ids(resp) class DictWithMeta(dict, RequestIdMixin): def __init__(self, values, resp): super(DictWithMeta, self).__init__(values) self.setup() self.append_request_ids(resp) class TupleWithMeta(tuple, RequestIdMixin): def __new__(cls, values, resp): return super(TupleWithMeta, cls).__new__(cls, values) def __init__(self, values, resp): self.setup() self.append_request_ids(resp) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/apiclient/exceptions.py0000664000175000017500000002717400000000000024710 0ustar00zuulzuul00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Nebula, Inc. # Copyright 2013 Alessio Ababilov # Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Exception definitions. """ import inspect import sys class ClientException(Exception): """The base exception class for all exceptions this library raises. """ pass class MissingArgs(ClientException): """Supplied arguments are not sufficient for calling a function.""" def __init__(self, missing): self.missing = missing msg = "Missing argument(s): %s" % ", ".join(missing) super(MissingArgs, self).__init__(msg) class ValidationError(ClientException): """Error in validation on API client side.""" pass class UnsupportedVersion(ClientException): """User is trying to use an unsupported version of the API.""" pass class CommandError(ClientException): """Error in CLI tool.""" pass class AuthorizationFailure(ClientException): """Cannot authorize API client.""" pass class ConnectionRefused(ClientException): """Cannot connect to API service.""" pass class AuthPluginOptionsMissing(AuthorizationFailure): """Auth plugin misses some options.""" def __init__(self, opt_names): super(AuthPluginOptionsMissing, self).__init__( "Authentication failed. Missing options: %s" % ", ".join(opt_names)) self.opt_names = opt_names class AuthSystemNotFound(AuthorizationFailure): """User has specified a AuthSystem that is not installed.""" def __init__(self, auth_system): super(AuthSystemNotFound, self).__init__( "AuthSystemNotFound: %s" % repr(auth_system)) self.auth_system = auth_system class NoUniqueMatch(ClientException): """Multiple entities found instead of one.""" pass class EndpointException(ClientException): """Something is rotten in Service Catalog.""" pass class EndpointNotFound(EndpointException): """Could not find requested endpoint in Service Catalog.""" pass class AmbiguousEndpoints(EndpointException): """Found more than one matching endpoint in Service Catalog.""" def __init__(self, endpoints=None): super(AmbiguousEndpoints, self).__init__( "AmbiguousEndpoints: %s" % repr(endpoints)) self.endpoints = endpoints class HttpError(ClientException): """The base exception class for all HTTP exceptions. """ http_status = 0 message = "HTTP Error" def __init__(self, message=None, details=None, response=None, request_id=None, url=None, method=None, http_status=None): self.http_status = http_status or self.http_status self.message = message or self.message self.details = details self.request_id = request_id self.response = response self.url = url self.method = method formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) if request_id: formatted_string += " (Request-ID: %s)" % request_id super(HttpError, self).__init__(formatted_string) class HTTPClientError(HttpError): """Client-side HTTP error. Exception for cases in which the client seems to have erred. """ message = "HTTP Client Error" class HttpServerError(HttpError): """Server-side HTTP error. Exception for cases in which the server is aware that it has erred or is incapable of performing the request. """ message = "HTTP Server Error" class BadRequest(HTTPClientError): """HTTP 400 - Bad Request. The request cannot be fulfilled due to bad syntax. """ http_status = 400 message = "Bad Request" class Unauthorized(HTTPClientError): """HTTP 401 - Unauthorized. Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. """ http_status = 401 message = "Unauthorized" class PaymentRequired(HTTPClientError): """HTTP 402 - Payment Required. Reserved for future use. """ http_status = 402 message = "Payment Required" class Forbidden(HTTPClientError): """HTTP 403 - Forbidden. The request was a valid request, but the server is refusing to respond to it. """ http_status = 403 message = "Forbidden" class NotFound(HTTPClientError): """HTTP 404 - Not Found. The requested resource could not be found but may be available again in the future. """ http_status = 404 message = "Not Found" class MethodNotAllowed(HTTPClientError): """HTTP 405 - Method Not Allowed. A request was made of a resource using a request method not supported by that resource. """ http_status = 405 message = "Method Not Allowed" class NotAcceptable(HTTPClientError): """HTTP 406 - Not Acceptable. The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request. """ http_status = 406 message = "Not Acceptable" class ProxyAuthenticationRequired(HTTPClientError): """HTTP 407 - Proxy Authentication Required. The client must first authenticate itself with the proxy. """ http_status = 407 message = "Proxy Authentication Required" class RequestTimeout(HTTPClientError): """HTTP 408 - Request Timeout. The server timed out waiting for the request. """ http_status = 408 message = "Request Timeout" class Conflict(HTTPClientError): """HTTP 409 - Conflict. Indicates that the request could not be processed because of conflict in the request, such as an edit conflict. """ http_status = 409 message = "Conflict" class Gone(HTTPClientError): """HTTP 410 - Gone. Indicates that the resource requested is no longer available and will not be available again. """ http_status = 410 message = "Gone" class LengthRequired(HTTPClientError): """HTTP 411 - Length Required. The request did not specify the length of its content, which is required by the requested resource. """ http_status = 411 message = "Length Required" class PreconditionFailed(HTTPClientError): """HTTP 412 - Precondition Failed. The server does not meet one of the preconditions that the requester put on the request. """ http_status = 412 message = "Precondition Failed" class RequestEntityTooLarge(HTTPClientError): """HTTP 413 - Request Entity Too Large. The request is larger than the server is willing or able to process. """ http_status = 413 message = "Request Entity Too Large" def __init__(self, *args, **kwargs): try: self.retry_after = int(kwargs.pop('retry_after')) except (KeyError, ValueError): self.retry_after = 0 super(RequestEntityTooLarge, self).__init__(*args, **kwargs) class RequestUriTooLong(HTTPClientError): """HTTP 414 - Request-URI Too Long. The URI provided was too long for the server to process. """ http_status = 414 message = "Request-URI Too Long" class UnsupportedMediaType(HTTPClientError): """HTTP 415 - Unsupported Media Type. The request entity has a media type which the server or resource does not support. """ http_status = 415 message = "Unsupported Media Type" class RequestedRangeNotSatisfiable(HTTPClientError): """HTTP 416 - Requested Range Not Satisfiable. The client has asked for a portion of the file, but the server cannot supply that portion. """ http_status = 416 message = "Requested Range Not Satisfiable" class ExpectationFailed(HTTPClientError): """HTTP 417 - Expectation Failed. The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 message = "Expectation Failed" class UnprocessableEntity(HTTPClientError): """HTTP 422 - Unprocessable Entity. The request was well-formed but was unable to be followed due to semantic errors. """ http_status = 422 message = "Unprocessable Entity" class InternalServerError(HttpServerError): """HTTP 500 - Internal Server Error. A generic error message, given when no more specific message is suitable. """ http_status = 500 message = "Internal Server Error" # NotImplemented is a python keyword. class HttpNotImplemented(HttpServerError): """HTTP 501 - Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request. """ http_status = 501 message = "Not Implemented" class BadGateway(HttpServerError): """HTTP 502 - Bad Gateway. The server was acting as a gateway or proxy and received an invalid response from the upstream server. """ http_status = 502 message = "Bad Gateway" class ServiceUnavailable(HttpServerError): """HTTP 503 - Service Unavailable. The server is currently unavailable. """ http_status = 503 message = "Service Unavailable" class GatewayTimeout(HttpServerError): """HTTP 504 - Gateway Timeout. The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. """ http_status = 504 message = "Gateway Timeout" class HttpVersionNotSupported(HttpServerError): """HTTP 505 - HttpVersion Not Supported. The server does not support the HTTP protocol version used in the request. """ http_status = 505 message = "HTTP Version Not Supported" # _code_map contains all the classes that have http_status attribute. _code_map = dict( (getattr(obj, 'http_status', None), obj) for name, obj in vars(sys.modules[__name__]).items() if inspect.isclass(obj) and getattr(obj, 'http_status', False) ) def from_response(response, method, url): """Returns an instance of :class:`HttpError` or subclass based on response. :param response: instance of `requests.Response` class :param method: HTTP method used for request :param url: URL used for request """ kwargs = { "http_status": response.status_code, "response": response, "method": method, "url": url, "request_id": response.headers.get("x-compute-request-id"), } if "retry-after" in response.headers: kwargs["retry_after"] = response.headers["retry-after"] content_type = response.headers.get("Content-Type", "") if content_type.startswith("application/json"): try: body = response.json() except ValueError: pass else: if hasattr(body, "keys"): error = body[list(body.keys())[0]] kwargs["message"] = error.get("message", None) kwargs["details"] = error.get("details", None) elif content_type.startswith("text/"): kwargs["details"] = response.text try: cls = _code_map[response.status_code] except KeyError: if 500 <= response.status_code < 600: cls = HttpServerError elif 400 <= response.status_code < 500: cls = HTTPClientError else: cls = HttpError return cls(**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/base.py0000664000175000017500000003735400000000000021472 0ustar00zuulzuul00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Base utilities to build API operation managers and objects on top of. """ import abc import contextlib import hashlib import os from cinderclient.apiclient import base as common_base from cinderclient import exceptions from cinderclient import utils # Valid sort directions and client sort keys SORT_DIR_VALUES = ('asc', 'desc') SORT_KEY_VALUES = ('id', 'status', 'size', 'availability_zone', 'name', 'bootable', 'created_at', 'reference') SORT_MANAGEABLE_KEY_VALUES = ('size', 'reference') # Mapping of client keys to actual sort keys SORT_KEY_MAPPINGS = {'name': 'display_name'} # Additional sort keys for resources SORT_KEY_ADD_VALUES = { 'backups': ('data_timestamp', ), 'messages': ('resource_type', 'event_id', 'resource_uuid', 'message_level', 'guaranteed_until', 'request_id'), } Resource = common_base.Resource def getid(obj): """ Abstracts the common pattern of allowing both an object or an object's ID as a parameter when dealing with relationships. """ return getattr(obj, 'id', obj) class Manager(common_base.HookableMixin): """ Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. """ resource_class = None def __init__(self, api): self.api = api @property def api_version(self): return self.api.api_version def _list(self, url, response_key, obj_class=None, body=None, limit=None, items=None): resp = None if items is None: items = [] if body: resp, body = self.api.client.post(url, body=body) else: resp, body = self.api.client.get(url) if obj_class is None: obj_class = self.resource_class data = body[response_key] # NOTE(ja): keystone returns values as list as {'values': [ ... ]} # unlike other services which just return the list... if isinstance(data, dict): try: data = data['values'] except KeyError: pass items_new = [obj_class(self, res, loaded=True) for res in data if res] if limit: limit = int(limit) margin = limit - len(items) if margin <= len(items_new): # If the limit is reached, return the items. items = items + items_new[:margin] if "count" in body: return common_base.ListWithMeta(items, resp), body['count'] else: return common_base.ListWithMeta(items, resp) else: items = items + items_new else: items = items + items_new # It is possible that the length of the list we request is longer # than osapi_max_limit, so we have to retrieve multiple times to # get the complete list. next = None link_name = response_key + '_links' if link_name in body: links = body[link_name] if links: for link in links: if 'rel' in link and 'next' == link['rel']: next = link['href'] break if next: # As long as the 'next' link is not empty, keep requesting it # till there is no more items. items = self._list(next, response_key, obj_class, None, limit, items) # If we use '--with-count' to get the resource count, # the _list function will return the tuple result with # (resources, count). # So here, we must check the items' type then to do return. if isinstance(items, tuple): items = items[0] if "count" in body: return common_base.ListWithMeta(items, resp), body['count'] else: return common_base.ListWithMeta(items, resp) def _build_list_url(self, resource_type, detailed=True, search_opts=None, marker=None, limit=None, sort=None, offset=None): if search_opts is None: search_opts = {} query_params = {} for key, val in search_opts.items(): if val: query_params[key] = val if marker: query_params['marker'] = marker if limit: query_params['limit'] = limit if sort: query_params['sort'] = self._format_sort_param(sort, resource_type) if offset: query_params['offset'] = offset query_params = query_params # Transform the dict to a sequence of two-element tuples in fixed # order, then the encoded string will be consistent in Python 2&3. query_string = utils.build_query_param(query_params, sort=True) detail = "" if detailed: detail = "/detail" return ("/%(resource_type)s%(detail)s%(query_string)s" % {"resource_type": resource_type, "detail": detail, "query_string": query_string}) def _format_sort_param(self, sort, resource_type=None): """Formats the sort information into the sort query string parameter. The input sort information can be any of the following: - Comma-separated string in the form of - List of strings in the form of - List of either string keys, or tuples of (key, dir) For example, the following import sort values are valid: - 'key1:dir1,key2,key3:dir3' - ['key1:dir1', 'key2', 'key3:dir3'] - [('key1', 'dir1'), 'key2', ('key3', dir3')] :param sort: Input sort information :returns: Formatted query string parameter or None :raise ValueError: If an invalid sort direction or invalid sort key is given """ if not sort: return None if isinstance(sort, str): # Convert the string into a list for consistent validation sort = [s for s in sort.split(',') if s] sort_array = [] for sort_item in sort: sort_key, _sep, sort_dir = sort_item.partition(':') sort_key = sort_key.strip() sort_key = self._format_sort_key_param(sort_key, resource_type) if sort_dir: sort_dir = sort_dir.strip() if sort_dir not in SORT_DIR_VALUES: msg = ('sort_dir must be one of the following: %s.' % ', '.join(SORT_DIR_VALUES)) raise ValueError(msg) sort_array.append('%s:%s' % (sort_key, sort_dir)) else: sort_array.append(sort_key) return ','.join(sort_array) def _format_sort_key_param(self, sort_key, resource_type=None): valid_sort_keys = SORT_KEY_VALUES if resource_type: add_sort_keys = SORT_KEY_ADD_VALUES.get(resource_type, None) if add_sort_keys: valid_sort_keys += add_sort_keys if sort_key in valid_sort_keys: return SORT_KEY_MAPPINGS.get(sort_key, sort_key) msg = ('sort_key must be one of the following: %s.' % ', '.join(valid_sort_keys)) raise ValueError(msg) @contextlib.contextmanager def completion_cache(self, cache_type, obj_class, mode): """ The completion cache store items that can be used for bash autocompletion, like UUIDs or human-friendly IDs. A resource listing will clear and repopulate the cache. A resource create will append to the cache. Delete is not handled because listings are assumed to be performed often enough to keep the cache reasonably up-to-date. """ base_dir = utils.env('CINDERCLIENT_UUID_CACHE_DIR', default="~/.cache/cinderclient") # NOTE(sirp): Keep separate UUID caches for each username + endpoint # pair username = utils.env('OS_USERNAME', 'CINDER_USERNAME') url = utils.env('OS_URL', 'CINDER_URL') uniqifier = hashlib.sha1(username.encode('utf-8') + # nosec url.encode('utf-8')).hexdigest() cache_dir = os.path.expanduser(os.path.join(base_dir, uniqifier)) try: os.makedirs(cache_dir, 0o750) except OSError: # NOTE(kiall): This is typically either permission denied while # attempting to create the directory, or the directory # already exists. Either way, don't fail. pass resource = obj_class.__name__.lower() filename = "%s-%s-cache" % (resource, cache_type.replace('_', '-')) path = os.path.join(cache_dir, filename) cache_attr = "_%s_cache" % cache_type try: setattr(self, cache_attr, open(path, mode)) except IOError: # NOTE(kiall): This is typically a permission denied while # attempting to write the cache file. pass try: yield finally: cache = getattr(self, cache_attr, None) if cache: cache.close() try: delattr(self, cache_attr) except AttributeError: # NOTE(kiall): If this attr is deleted by another # operation, don't fail any way. pass def write_to_completion_cache(self, cache_type, val): cache = getattr(self, "_%s_cache" % cache_type, None) if cache: try: cache.write("%s\n" % val) except UnicodeEncodeError: pass def _get(self, url, response_key=None): resp, body = self.api.client.get(url) if response_key: return self.resource_class(self, body[response_key], loaded=True, resp=resp) else: return self.resource_class(self, body, loaded=True, resp=resp) def _create(self, url, body, response_key, return_raw=False, **kwargs): self.run_hooks('modify_body_for_create', body, **kwargs) resp, body = self.api.client.post(url, body=body) if return_raw: return common_base.DictWithMeta(body[response_key], resp) return self.resource_class(self, body[response_key], resp=resp) def _delete(self, url): resp, body = self.api.client.delete(url) return common_base.TupleWithMeta((resp, body), resp) def _update(self, url, body, response_key=None, **kwargs): self.run_hooks('modify_body_for_update', body, **kwargs) resp, body = self.api.client.put(url, body=body, **kwargs) if response_key: return self.resource_class(self, body[response_key], loaded=True, resp=resp) # (NOTE)ankit: In case of qos_specs.unset_keys method, None is # returned back to the caller and in all other cases dict is # returned but in order to return request_ids to the caller, it's # not possible to return None so returning DictWithMeta for all cases. body = body or {} return common_base.DictWithMeta(body, resp) def _get_with_base_url(self, url, response_key=None): resp, body = self.api.client.get_with_base_url(url) if response_key: return [self.resource_class(self, res, loaded=True) for res in body[response_key] if res] else: return self.resource_class(self, body, loaded=True) def _get_all_with_base_url(self, url, response_key=None): resp, body = self.api.client.get_with_base_url(url) if response_key: if isinstance(body[response_key], list): return [self.resource_class(self, res, loaded=True) for res in body[response_key] if res] return self.resource_class(self, body[response_key], loaded=True) return self.resource_class(self, body, loaded=True) def _create_update_with_base_url(self, url, body, response_key=None): resp, body = self.api.client.create_update_with_base_url( url, body=body) if response_key: return self.resource_class(self, body[response_key], loaded=True) return self.resource_class(self, body, loaded=True) def _delete_with_base_url(self, url, response_key=None): self.api.client.delete_with_base_url(url) class ManagerWithFind(Manager, metaclass=abc.ABCMeta): """ Like a `Manager`, but with additional `find()`/`findall()` methods. """ @abc.abstractmethod def list(self): pass def find(self, **kwargs): """ Find a single item with attributes matching ``**kwargs``. This isn't very efficient for search options which require the Python side filtering(e.g. 'human_id') """ matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) raise exceptions.NotFound(404, msg) elif num_matches > 1: raise exceptions.NoUniqueMatch else: matches[0].append_request_ids(matches.request_ids) return matches[0] def findall(self, **kwargs): """ Find all items with attributes matching ``**kwargs``. This isn't very efficient for search options which require the Python side filtering(e.g. 'human_id') """ # Want to search for all tenants here so that when attempting to delete # that a user like admin doesn't get a failure when trying to delete # another tenant's volume by name. search_opts = {'all_tenants': 1} # Pass 'name' or 'display_name' search_opts to server filtering to # increase search performance. if 'name' in kwargs: search_opts['name'] = kwargs['name'] elif 'display_name' in kwargs: search_opts['display_name'] = kwargs['display_name'] found = common_base.ListWithMeta([], None) # list_volume is used for group query, it's not resource's property. list_volume = kwargs.pop('list_volume', False) searches = kwargs.items() if list_volume: listing = self.list(search_opts=search_opts, list_volume=list_volume) else: listing = self.list(search_opts=search_opts) found.append_request_ids(listing.request_ids) # Not all resources attributes support filters on server side # (e.g. 'human_id' doesn't), so when doing findall some client # side filtering is still needed. for obj in listing: try: if all(getattr(obj, attr) == value for (attr, value) in searches): found.append(obj) except AttributeError: continue return found ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/client.py0000664000175000017500000010006300000000000022022 0ustar00zuulzuul00000000000000# Copyright (c) 2011 OpenStack Foundation # Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 Piston Cloud Computing, 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. """OpenStack Client interface. Handles the REST calls and responses.""" import glob import hashlib import importlib.util import itertools import logging import os import pkgutil import re import urllib from urllib import parse as urlparse from keystoneauth1 import access from keystoneauth1 import adapter from keystoneauth1 import discover from keystoneauth1.identity import base from oslo_utils import encodeutils from oslo_utils import importutils from oslo_utils import strutils import requests from cinderclient._i18n import _ from cinderclient import api_versions from cinderclient import exceptions import cinderclient.extension try: from eventlet import sleep except ImportError: from time import sleep try: import json except ImportError: import simplejson as json try: osprofiler_web = importutils.try_import("osprofiler.web") except Exception: pass _VALID_VERSIONS = ['v3'] V3_SERVICE_TYPE = 'volumev3' SERVICE_TYPES = {'3': V3_SERVICE_TYPE} REQ_ID_HEADER = 'X-OpenStack-Request-ID' # tell keystoneclient that we can ignore the /v1|v2/{project_id} component of # the service catalog when doing discovery lookups for svc in ('volume', 'volumev3'): discover.add_catalog_discover_hack(svc, re.compile(r'/v[12]/\w+/?$'), '/') def get_server_version(url, insecure=False, cacert=None, cert=None): """Queries the server via the naked endpoint and gets version info. :param url: url of the cinder endpoint :param insecure: Explicitly allow client to perform "insecure" TLS (https) requests :param cacert: Specify a CA bundle file to use in verifying a TLS (https) server certificate :param cert: A client certificate to pass to requests. These are of the same form as requests expects. Either a single filename containing both the certificate and key or a tuple containing the path to the certificate then a path to the key. (optional) :returns: APIVersion object for min and max version supported by the server """ # NOTE: we (the client) don't support v2 anymore, but this function # is checking the server version min_version = "2.0" current_version = "2.0" logger = logging.getLogger(__name__) try: u = urllib.parse.urlparse(url) version_url = None # NOTE(andreykurilin): endpoint URL has at least 2 formats: # 1. The classic (legacy) endpoint: # http://{host}:{optional_port}/v{2 or 3}/{project-id} # http://{host}:{optional_port}/v{2 or 3} # 3. Under wsgi: # http://{host}:{optional_port}/volume/v{2 or 3} for ver in ['v2', 'v3']: if u.path.endswith(ver) or "/{0}/".format(ver) in u.path: path = u.path[:u.path.rfind(ver)] version_url = '%s://%s%s' % (u.scheme, u.netloc, path) break if not version_url: # NOTE(andreykurilin): probably, it is one of the next cases: # * https://volume.example.com/ # * https://example.com/volume # leave as is without cropping. version_url = url if insecure: verify_cert = False else: if cacert: verify_cert = cacert else: verify_cert = True response = requests.get(version_url, verify=verify_cert, cert=cert) data = json.loads(response.text) versions = data['versions'] for version in versions: if '3.' in version['version']: min_version = version['min_version'] current_version = version['version'] break else: # keep looking in case this cloud is running v2 and # we haven't seen v3 yet continue except exceptions.ClientException as e: # NOTE: logging the warning but returning the lowest server API version # supported in this OpenStack release is the legacy behavior, so that's # what we do here min_version = '3.0' current_version = '3.0' logger.warning("Error in server version query:%s\n" "Returning APIVersion 3.0", str(e.message)) return (api_versions.APIVersion(min_version), api_versions.APIVersion(current_version)) def get_highest_client_server_version(url, insecure=False, cacert=None, cert=None): """Returns highest supported version by client and server as a string. :raises: UnsupportedVersion if the maximum supported by the server is less than the minimum supported by the client """ min_server, max_server = get_server_version(url, insecure, cacert, cert) max_client = api_versions.APIVersion(api_versions.MAX_VERSION) min_client = api_versions.APIVersion(api_versions.MIN_VERSION) if max_server < min_client: msg = _("The maximum version supported by the server (%(srv)s) does " "not meet the minimum version supported by this client " "(%(cli)s)") % {"srv": str(max_server), "cli": api_versions.MIN_VERSION} raise exceptions.UnsupportedVersion(msg) return min(max_server, max_client).get_string() def get_volume_api_from_url(url): scheme, netloc, path, query, frag = urlparse.urlsplit(url) components = path.split("/") for version in _VALID_VERSIONS: if version in components: return version[1:] msg = (_("Invalid url: '%(url)s'. It must include one of: %(version)s.") % {'url': url, 'version': ', '.join(_VALID_VERSIONS)}) raise exceptions.UnsupportedVersion(msg) class SessionClient(adapter.LegacyJsonAdapter): def __init__(self, *args, **kwargs): apiver = kwargs.pop('api_version', None) or api_versions.APIVersion() if not isinstance(apiver, api_versions.APIVersion): apiver = api_versions.APIVersion(str(apiver)) if apiver.ver_minor != 0: kwargs['default_microversion'] = apiver.get_string() self.retries = kwargs.pop('retries', 0) self._logger = logging.getLogger(__name__) super(SessionClient, self).__init__(*args, **kwargs) def request(self, *args, **kwargs): kwargs.setdefault('authenticated', False) # Note(tpatil): The standard call raises errors from # keystoneauth, here we need to raise the cinderclient errors. raise_exc = kwargs.pop('raise_exc', True) resp, body = super(SessionClient, self).request(*args, raise_exc=False, **kwargs) if raise_exc and resp.status_code >= 400: raise exceptions.from_response(resp, body) if not self.global_request_id: self.global_request_id = resp.headers.get('x-openstack-request-id') return resp, body def _cs_request(self, url, method, **kwargs): # this function is mostly redundant but makes compatibility easier kwargs.setdefault('authenticated', True) attempts = 0 while True: attempts += 1 try: return self.request(url, method, **kwargs) except exceptions.OverLimit as overlim: if attempts > self.retries or overlim.retry_after < 1: raise msg = "Retrying after %s seconds." % overlim.retry_after self._logger.debug(msg) sleep(overlim.retry_after) def get(self, url, **kwargs): return self._cs_request(url, 'GET', **kwargs) def post(self, url, **kwargs): return self._cs_request(url, 'POST', **kwargs) def put(self, url, **kwargs): return self._cs_request(url, 'PUT', **kwargs) def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) def _get_base_url(self): endpoint = self.get_endpoint() m = re.search('(.+)/v[1-3].*', endpoint) if m: # Get everything up until the version identifier base_url = '%s/' % m.group(1) else: # Fall back to the root of the URL base_url = '/'.join(endpoint.split('/')[:3]) + '/' return base_url def get_volume_api_version_from_endpoint(self): try: version = get_volume_api_from_url(self.get_endpoint()) except exceptions.UnsupportedVersion as e: msg = (_("Service catalog returned invalid url.\n" "%s") % str(e)) raise exceptions.UnsupportedVersion(msg) return version def authenticate(self, auth=None): self.invalidate(auth) return self.get_token(auth) @property def service_catalog(self): # NOTE(jamielennox): This is ugly and should be deprecated. auth = self.auth or self.session.auth if isinstance(auth, base.BaseIdentityPlugin): return auth.get_access(self.session).service_catalog raise AttributeError('There is no service catalog for this type of ' 'auth plugin.') def _cs_request_base_url(self, url, method, **kwargs): base_url = self._get_base_url() return self._cs_request( base_url + url, method, **kwargs) def get_with_base_url(self, url, **kwargs): return self._cs_request_base_url(url, 'GET', **kwargs) def create_update_with_base_url(self, url, **kwargs): return self._cs_request_base_url(url, 'PUT', **kwargs) def delete_with_base_url(self, url, **kwargs): return self._cs_request_base_url(url, 'DELETE', **kwargs) class HTTPClient(object): SENSITIVE_HEADERS = ('X-Auth-Token', 'X-Subject-Token',) USER_AGENT = 'python-cinderclient' def __init__(self, user, password, projectid, auth_url=None, insecure=False, timeout=None, tenant_id=None, proxy_tenant_id=None, proxy_token=None, region_name=None, endpoint_type='publicURL', service_type=None, service_name=None, volume_service_name=None, os_endpoint=None, retries=None, http_log_debug=False, cacert=None, cert=None, auth_system='keystone', auth_plugin=None, api_version=None, logger=None, user_domain_name='Default', project_domain_name='Default', global_request_id=None): self.user = user self.password = password self.projectid = projectid self.tenant_id = tenant_id self.api_version = api_version or api_versions.APIVersion() self.global_request_id = global_request_id if auth_system and auth_system != 'keystone' and not auth_plugin: raise exceptions.AuthSystemNotFound(auth_system) if not auth_url and auth_system and auth_system != 'keystone': auth_url = auth_plugin.get_auth_url() if not auth_url: raise exceptions.EndpointNotFound() self.auth_url = auth_url.rstrip('/') if auth_url else None self.ks_version = 'v1' self.region_name = region_name self.endpoint_type = endpoint_type self.service_type = service_type self.service_name = service_name self.volume_service_name = volume_service_name self.os_endpoint = os_endpoint.rstrip('/') \ if os_endpoint else os_endpoint self.retries = int(retries or 0) self.http_log_debug = http_log_debug self.management_url = self.os_endpoint or None self.auth_token = None self.proxy_token = proxy_token self.proxy_tenant_id = proxy_tenant_id self.timeout = timeout self.user_domain_name = user_domain_name self.project_domain_name = project_domain_name self.cert = cert if insecure: self.verify_cert = False else: if cacert: self.verify_cert = cacert else: self.verify_cert = True self.auth_system = auth_system self.auth_plugin = auth_plugin self._logger = logger or logging.getLogger(__name__) def _safe_header(self, name, value): if name in HTTPClient.SENSITIVE_HEADERS: encoded = value.encode('utf-8') hashed = hashlib.sha1(encoded) digested = hashed.hexdigest() return encodeutils.safe_decode(name), "{SHA1}%s" % digested else: return (encodeutils.safe_decode(name), encodeutils.safe_decode(value)) def http_log_req(self, args, kwargs): if not self.http_log_debug: return string_parts = ['curl -i'] for element in args: if element in ('GET', 'POST', 'DELETE', 'PUT'): string_parts.append(' -X %s' % element) else: string_parts.append(' %s' % element) for element in kwargs['headers']: header = ("-H '%s: %s'" % self._safe_header(element, kwargs['headers'][element])) string_parts.append(header) if 'data' in kwargs: data = strutils.mask_password(kwargs['data']) string_parts.append(" -d '%s'" % (data)) self._logger.debug("\nREQ: %s\n" % "".join(string_parts)) def http_log_resp(self, resp): if not self.http_log_debug: return self._logger.debug( "RESP: [%s] %s\nRESP BODY: %s\n", resp.status_code, resp.headers, strutils.mask_password(resp.text)) def request(self, url, method, **kwargs): kwargs.setdefault('headers', kwargs.get('headers', {})) kwargs['headers']['User-Agent'] = self.USER_AGENT kwargs['headers']['Accept'] = 'application/json' if osprofiler_web: kwargs['headers'].update(osprofiler_web.get_trace_id_headers()) if 'body' in kwargs: kwargs['headers']['Content-Type'] = 'application/json' kwargs['data'] = json.dumps(kwargs.pop('body')) api_versions.update_headers(kwargs["headers"], self.api_version) if self.global_request_id: kwargs['headers'].setdefault(REQ_ID_HEADER, self.global_request_id) if self.timeout: kwargs.setdefault('timeout', self.timeout) self.http_log_req((url, method,), kwargs) resp = requests.request( method, url, verify=self.verify_cert, cert=self.cert, **kwargs) self.http_log_resp(resp) body = None if resp.text: try: body = json.loads(resp.text) except ValueError as e: self._logger.debug("Load http response text error: %s", e) if resp.status_code >= 400: raise exceptions.from_response(resp, body) return resp, body def _cs_request(self, url, method, **kwargs): auth_attempts = 0 attempts = 0 backoff = 1 while True: attempts += 1 if not self.management_url or not self.auth_token: self.authenticate() kwargs.setdefault('headers', {})['X-Auth-Token'] = self.auth_token if self.projectid: kwargs['headers']['X-Auth-Project-Id'] = self.projectid try: if not url.startswith(self.management_url): url = self.management_url + url resp, body = self.request(url, method, **kwargs) return resp, body except exceptions.BadRequest: if attempts > self.retries: raise except exceptions.Unauthorized: if auth_attempts > 0: raise self._logger.debug("Unauthorized, reauthenticating.") self.management_url = self.auth_token = None # First reauth. Discount this attempt. attempts -= 1 auth_attempts += 1 continue except exceptions.OverLimit as overlim: if attempts > self.retries or overlim.retry_after < 1: raise msg = "Retrying after %s seconds." % overlim.retry_after self._logger.debug(msg) sleep(overlim.retry_after) continue except exceptions.ClientException as e: if attempts > self.retries: raise if 500 <= e.code <= 599: pass else: raise except requests.exceptions.ConnectionError as e: self._logger.debug("Connection error: %s" % e) if attempts > self.retries: msg = 'Unable to establish connection: %s' % e raise exceptions.ConnectionError(msg) except requests.exceptions.Timeout as e: self._logger.debug("Timeout error: %s" % e) if attempts > self.retries: raise self._logger.debug( "Failed attempt(%s of %s), retrying in %s seconds" % (attempts, self.retries, backoff)) sleep(backoff) backoff *= 2 def get(self, url, **kwargs): return self._cs_request(url, 'GET', **kwargs) def post(self, url, **kwargs): return self._cs_request(url, 'POST', **kwargs) def put(self, url, **kwargs): return self._cs_request(url, 'PUT', **kwargs) def delete(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) def get_volume_api_version_from_endpoint(self): try: version = get_volume_api_from_url(self.management_url) except exceptions.UnsupportedVersion as e: if self.management_url == self.os_endpoint: msg = (_("Invalid url was specified in --os-endpoint %s") % str(e)) else: msg = (_("Service catalog returned invalid url.\n" "%s") % str(e)) raise exceptions.UnsupportedVersion(msg) return version def _extract_service_catalog(self, url, resp, body, extract_token=True): """See what the auth service told us and process the response. We may get redirected to another site, fail or actually get back a service catalog with a token and our endpoints. """ # content must always present if resp.status_code == 200 or resp.status_code == 201: try: self.auth_url = url self.auth_ref = access.create(resp=resp, body=body) self.service_catalog = self.auth_ref.service_catalog if extract_token: self.auth_token = self.auth_ref.auth_token management_url = self.service_catalog.url_for( region_name=self.region_name, interface=self.endpoint_type, service_type=self.service_type, service_name=self.service_name) self.management_url = management_url.rstrip('/') return None except exceptions.AmbiguousEndpoints: print("Found more than one valid endpoint. Use a more " "restrictive filter") raise except ValueError: # ValueError is raised when you pass an invalid response to # access.create. This should never happen in reality if the # status code is 200. raise exceptions.AuthorizationFailure() except exceptions.EndpointNotFound: print("Could not find any suitable endpoint. Correct region?") raise elif resp.status_code == 305: return resp.headers['location'] else: raise exceptions.from_response(resp, body) def _fetch_endpoints_from_auth(self, url): """We have a token, but don't know the final endpoint for the region. We have to go back to the auth service and ask again. This request requires an admin-level token to work. The proxy token supplied could be from a low-level enduser. We can't get this from the keystone service endpoint, we have to use the admin endpoint. This will overwrite our admin token with the user token. """ # GET ...:5001/v2.0/tokens/#####/endpoints url = '/'.join([url, 'tokens', '%s?belongsTo=%s' % (self.proxy_token, self.proxy_tenant_id)]) self._logger.debug("Using Endpoint URL: %s" % url) resp, body = self.request(url, "GET", headers={'X-Auth-Token': self.auth_token}) return self._extract_service_catalog(url, resp, body, extract_token=False) def set_management_url(self, url): self.management_url = url def authenticate(self): magic_tuple = urlparse.urlsplit(self.auth_url) scheme, netloc, path, query, frag = magic_tuple port = magic_tuple.port if port is None: port = 80 path_parts = path.split('/') for part in path_parts: if len(part) > 0 and part[0] == 'v': self.ks_version = part break # TODO(sandy): Assume admin endpoint is 35357 for now. # Ideally this is going to have to be provided by the service catalog. new_netloc = netloc.replace(':%d' % port, ':%d' % (35357,)) admin_url = urlparse.urlunsplit((scheme, new_netloc, path, query, frag)) auth_url = self.auth_url if 'v2' in self.ks_version or 'v3' in self.ks_version: while auth_url: if not self.auth_system or self.auth_system == 'keystone': auth_url = self._v2_or_v3_auth(auth_url) # Are we acting on behalf of another user via an # existing token? If so, our actual endpoints may # be different than that of the admin token. if self.proxy_token: if self.os_endpoint: self.set_management_url(self.os_endpoint) else: self._fetch_endpoints_from_auth(admin_url) # Since keystone no longer returns the user token # with the endpoints any more, we need to replace # our service account token with the user token. self.auth_token = self.proxy_token else: try: while auth_url: auth_url = self._v1_auth(auth_url) # In some configurations cinder makes redirection to # v2.0 keystone endpoint. Also, new location does not contain # real endpoint, only hostname and port. except exceptions.AuthorizationFailure: if auth_url.find('v2.0') < 0: auth_url = auth_url + '/v2.0' self._v2_or_v3_auth(auth_url) if self.os_endpoint: self.set_management_url(self.os_endpoint) elif not self.management_url: raise exceptions.Unauthorized('Cinder Client') def _v1_auth(self, url): if self.proxy_token: raise exceptions.NoTokenLookupException() headers = {'X-Auth-User': self.user, 'X-Auth-Key': self.password} if self.projectid: headers['X-Auth-Project-Id'] = self.projectid resp, body = self.request(url, 'GET', headers=headers) if resp.status_code in (200, 204): # in some cases we get No Content try: mgmt_header = 'x-server-management-url' self.management_url = resp.headers[mgmt_header].rstrip('/') self.auth_token = resp.headers['x-auth-token'] self.auth_url = url except (KeyError, TypeError): raise exceptions.AuthorizationFailure() elif resp.status_code == 305: return resp.headers['location'] else: raise exceptions.from_response(resp, body) def _v2_or_v3_auth(self, url): """Authenticate against a v2.0 auth service.""" if self.ks_version == "v3": body = { "auth": { "identity": { "methods": ["password"], "password": {"user": { "domain": {"name": self.user_domain_name}, "name": self.user, "password": self.password}}}, } } scope = {"project": {"domain": {"name": self.project_domain_name}}} if self.projectid: scope['project']['name'] = self.projectid elif self.tenant_id: scope['project']['id'] = self.tenant_id body["auth"]["scope"] = scope else: body = {"auth": { "passwordCredentials": {"username": self.user, "password": self.password}}} if self.projectid: body['auth']['tenantName'] = self.projectid elif self.tenant_id: body['auth']['tenantId'] = self.tenant_id return self._authenticate(url, body) def _authenticate(self, url, body): """Authenticate and extract the service catalog.""" if self.ks_version == 'v3': token_url = url + "/auth/tokens" else: token_url = url + "/tokens" # Make sure we follow redirects when trying to reach Keystone resp, body = self.request( token_url, "POST", body=body, allow_redirects=True) return self._extract_service_catalog(url, resp, body) def _construct_http_client(username=None, password=None, project_id=None, auth_url=None, insecure=False, timeout=None, proxy_tenant_id=None, proxy_token=None, region_name=None, endpoint_type='publicURL', service_type='volume', service_name=None, volume_service_name=None, os_endpoint=None, retries=None, http_log_debug=False, auth_system='keystone', auth_plugin=None, cacert=None, cert=None, tenant_id=None, session=None, auth=None, api_version=None, **kwargs): if session: kwargs.setdefault('user_agent', 'python-cinderclient') kwargs.setdefault('interface', endpoint_type) kwargs.setdefault('endpoint_override', os_endpoint) return SessionClient(session=session, auth=auth, service_type=service_type, service_name=service_name, region_name=region_name, retries=retries, api_version=api_version, **kwargs) else: # FIXME(jamielennox): username and password are now optional. Need # to test that they were provided in this mode. logger = kwargs.get('logger') return HTTPClient(username, password, projectid=project_id, auth_url=auth_url, insecure=insecure, timeout=timeout, tenant_id=tenant_id, proxy_token=proxy_token, proxy_tenant_id=proxy_tenant_id, region_name=region_name, endpoint_type=endpoint_type, service_type=service_type, service_name=service_name, volume_service_name=volume_service_name, os_endpoint=os_endpoint, retries=retries, http_log_debug=http_log_debug, cacert=cacert, cert=cert, auth_system=auth_system, auth_plugin=auth_plugin, logger=logger, api_version=api_version ) def _get_client_class_and_version(version): if not isinstance(version, api_versions.APIVersion): version = api_versions.get_api_version(version) else: api_versions.check_major_version(version) if version.is_latest(): raise exceptions.UnsupportedVersion( _("The version should be explicit, not latest.")) return version, importutils.import_class( "cinderclient.v%s.client.Client" % version.ver_major) def get_client_class(version): version_map = { '3': 'cinderclient.v3.client.Client', } try: client_path = version_map[str(version)] except (KeyError, ValueError): msg = "Invalid client version '%s'. must be one of: %s" % ( (version, ', '.join(version_map))) raise exceptions.UnsupportedVersion(msg) return importutils.import_class(client_path) def discover_extensions(version): extensions = [] for name, module in itertools.chain( _discover_via_python_path(), _discover_via_contrib_path(version)): extension = cinderclient.extension.Extension(name, module) extensions.append(extension) return extensions def _discover_via_python_path(): for (module_loader, name, ispkg) in pkgutil.iter_modules(): if name.endswith('cinderclient_ext'): module = module_loader.load_module(name) yield name, module def load_module(name, path): module_spec = importlib.util.spec_from_file_location( name, path ) module = importlib.util.module_from_spec(module_spec) module_spec.loader.exec_module(module) return module def _discover_via_contrib_path(version): module_path = os.path.dirname(os.path.abspath(__file__)) version_str = "v%s" % version.replace('.', '_') ext_path = os.path.join(module_path, version_str, 'contrib') ext_glob = os.path.join(ext_path, "*.py") for ext_path in glob.iglob(ext_glob): name = os.path.basename(ext_path)[:-3] if name == "__init__": continue module = load_module(name, ext_path) yield name, module def Client(version, *args, **kwargs): """Initialize client object based on given version. HOW-TO: The simplest way to create a client instance is initialization with your credentials:: .. code-block:: python >>> from cinderclient import client >>> cinder = client.Client(VERSION, USERNAME, PASSWORD, ... PROJECT_NAME, AUTH_URL) Here ``VERSION`` can be a string or ``cinderclient.api_versions.APIVersion`` obj. If you prefer string value, you can use ``3`` or ``3.X`` (where X is a microversion). Alternatively, you can create a client instance using the keystoneclient session API. See "The cinderclient Python API" page at python-cinderclient's doc. """ api_version, client_class = _get_client_class_and_version(version) return client_class(api_version=api_version, *args, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.2658958 python-cinderclient-8.3.0/cinderclient/contrib/0000775000175000017500000000000000000000000021632 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/contrib/__init__.py0000664000175000017500000000000000000000000023731 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/contrib/noauth.py0000664000175000017500000000507300000000000023507 0ustar00zuulzuul00000000000000# 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 os from keystoneauth1 import loading from keystoneauth1 import plugin class CinderNoAuthPlugin(plugin.BaseAuthPlugin): def __init__(self, user_id, project_id=None, roles=None, endpoint=None): self._user_id = user_id self._project_id = project_id if project_id else user_id self._endpoint = endpoint self._roles = roles self.auth_token = '%s:%s' % (self._user_id, self._project_id) def get_headers(self, session, **kwargs): return {'x-user-id': self._user_id, 'x-project-id': self._project_id, 'X-Auth-Token': self.auth_token} def get_user_id(self, session, **kwargs): return self._user_id def get_project_id(self, session, **kwargs): return self._project_id def get_endpoint(self, session, **kwargs): return '%s/%s' % (self._endpoint, self._project_id) def invalidate(self): pass class CinderOpt(loading.Opt): @property def argparse_args(self): return ['--%s' % o.name for o in self._all_opts] @property def argparse_default(self): # select the first ENV that is not false-y or return None for o in self._all_opts: v = os.environ.get('Cinder_%s' % o.name.replace('-', '_').upper()) if v: return v return self.default class CinderNoAuthLoader(loading.BaseLoader): plugin_class = CinderNoAuthPlugin def get_options(self): options = super(CinderNoAuthLoader, self).get_options() options.extend([ CinderOpt('user-id', help='User ID', required=True, metavar=""), CinderOpt('project-id', help='Project ID', metavar=""), CinderOpt('endpoint', help='Cinder endpoint', dest="endpoint", required=True, metavar=""), ]) return options ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/exceptions.py0000664000175000017500000002117500000000000022733 0ustar00zuulzuul00000000000000# Copyright 2010 Jacob Kaplan-Moss # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """ Exception definitions. """ from datetime import datetime from oslo_utils import timeutils class ResourceInErrorState(Exception): """When resource is in Error state""" def __init__(self, obj, fault_msg): msg = "'%s' resource is in the error state" % obj.__class__.__name__ if fault_msg: msg += " due to '%s'" % fault_msg self.message = "%s." % msg def __str__(self): return self.message class TimeoutException(Exception): """When an action exceeds the timeout period to complete the action""" def __init__(self, obj, action): self.message = ("The '%(action)s' of the '%(object_name)s' exceeded " "the timeout period." % {"action": action, "object_name": obj.__class__.__name__}) def __str__(self): return self.message class UnsupportedVersion(Exception): """Indicates that the user is trying to use an unsupported version of the API. """ pass class UnsupportedAttribute(AttributeError): """Indicates that the user is trying to transmit the argument to a method, which is not supported by selected version. """ def __init__(self, argument_name, start_version, end_version): if start_version and end_version: self.message = ( "'%(name)s' argument is only allowed for microversions " "%(start)s - %(end)s." % {"name": argument_name, "start": start_version.get_string(), "end": end_version.get_string()}) elif start_version: self.message = ( "'%(name)s' argument is only allowed since microversion " "%(start)s." % {"name": argument_name, "start": start_version.get_string()}) elif end_version: self.message = ( "'%(name)s' argument is not allowed after microversion " "%(end)s." % {"name": argument_name, "end": end_version.get_string()}) def __str__(self): return self.message class InvalidAPIVersion(Exception): pass class CommandError(Exception): pass class AuthorizationFailure(Exception): pass class NoUniqueMatch(Exception): pass class AuthSystemNotFound(Exception): """When the user specifies an AuthSystem but not installed.""" def __init__(self, auth_system): self.auth_system = auth_system def __str__(self): return "AuthSystemNotFound: %s" % repr(self.auth_system) class NoTokenLookupException(Exception): """This form of authentication does not support looking up endpoints from an existing token. """ pass class EndpointNotFound(Exception): """Could not find Service or Region in Service Catalog.""" pass class ConnectionError(Exception): """Could not open a connection to the API service.""" pass class AmbiguousEndpoints(Exception): """Found more than one matching endpoint in Service Catalog.""" def __init__(self, endpoints=None): self.endpoints = endpoints def __str__(self): return "AmbiguousEndpoints: %s" % repr(self.endpoints) class ClientException(Exception): """ The base exception class for all exceptions this library raises. """ def __init__(self, code, message=None, details=None, request_id=None, response=None): self.code = code # NOTE(mriedem): Use getattr on self.__class__.message since # BaseException.message was dropped in python 3, see PEP 0352. self.message = message or getattr(self.__class__, 'message', None) self.details = details self.request_id = request_id def __str__(self): formatted_string = "%s" % self.message if self.code >= 100: # HTTP codes start at 100. formatted_string += " (HTTP %s)" % self.code if self.request_id: formatted_string += " (Request-ID: %s)" % self.request_id return formatted_string class BadRequest(ClientException): """ HTTP 400 - Bad request: you sent some malformed data. """ http_status = 400 message = "Bad request" class Unauthorized(ClientException): """ HTTP 401 - Unauthorized: bad credentials. """ http_status = 401 message = "Unauthorized" class Forbidden(ClientException): """ HTTP 403 - Forbidden: your credentials don't give you access to this resource. """ http_status = 403 message = "Forbidden" class NotFound(ClientException): """ HTTP 404 - Not found """ http_status = 404 message = "Not found" class NotAcceptable(ClientException): """ HTTP 406 - Not Acceptable """ http_status = 406 message = "Not Acceptable" class OverLimit(ClientException): """ HTTP 413 - Over limit: you're over the API limits for this time period. """ http_status = 413 message = "Over limit" def __init__(self, code, message=None, details=None, request_id=None, response=None): super(OverLimit, self).__init__(code, message=message, details=details, request_id=request_id, response=response) self.retry_after = 0 self._get_rate_limit(response) def _get_rate_limit(self, resp): if (resp is not None) and resp.headers: utc_now = timeutils.utcnow() value = resp.headers.get('Retry-After', '0') try: value = datetime.strptime(value, '%a, %d %b %Y %H:%M:%S %Z') if value > utc_now: self.retry_after = ((value - utc_now).seconds) else: self.retry_after = 0 except ValueError: self.retry_after = int(value) # NotImplemented is a python keyword. class HTTPNotImplemented(ClientException): """ HTTP 501 - Not Implemented: the server does not support this operation. """ http_status = 501 message = "Not Implemented" # In Python 2.4 Exception is old-style and thus doesn't have a __subclasses__() # so we can do this: # _code_map = dict((c.http_status, c) # for c in ClientException.__subclasses__()) # # Instead, we have to hardcode it: _code_map = dict((c.http_status, c) for c in [BadRequest, Unauthorized, Forbidden, NotFound, NotAcceptable, OverLimit, HTTPNotImplemented]) def from_response(response, body): """ Return an instance of a ClientException or subclass based on a requests response. Usage:: resp, body = requests.request(...) if resp.status_code != 200: raise exceptions.from_response(resp, resp.text) """ cls = _code_map.get(response.status_code, ClientException) if response.headers: request_id = response.headers.get('x-compute-request-id') else: request_id = None if body: message = "n/a" details = "n/a" if hasattr(body, 'keys'): # Only in webob>=1.6.0 if 'message' in body: message = body.get('message') details = body.get('details') else: error = body[list(body)[0]] message = error.get('message', message) details = error.get('details', details) return cls(code=response.status_code, message=message, details=details, request_id=request_id, response=response) else: return cls(code=response.status_code, request_id=request_id, message=response.reason, response=response) class VersionNotFoundForAPIMethod(Exception): msg_fmt = "API version '%(vers)s' is not supported on '%(method)s' method." def __init__(self, version, method): self.version = version self.method = method def __str__(self): return self.msg_fmt % {"vers": self.version, "method": self.method} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/extension.py0000664000175000017500000000267600000000000022573 0ustar00zuulzuul00000000000000# Copyright (c) 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient.apiclient import base as common_base from cinderclient import base from cinderclient import utils class Extension(common_base.HookableMixin): """Extension descriptor.""" SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') def __init__(self, name, module): self.name = name self.module = module self._parse_extension_module() def _parse_extension_module(self): self.manager_class = None for attr_name, attr_value in list(self.module.__dict__.items()): if attr_name in self.SUPPORTED_HOOKS: self.add_hook(attr_name, attr_value) elif utils.safe_issubclass(attr_value, base.Manager): self.manager_class = attr_value def __repr__(self): return "" % self.name ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/shell.py0000664000175000017500000012273300000000000021663 0ustar00zuulzuul00000000000000# Copyright 2011-2014 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Command-line interface to the OpenStack Cinder API. """ import argparse import collections import getpass import logging import sys from urllib import parse as urlparse from keystoneauth1 import discover from keystoneauth1 import exceptions from keystoneauth1.identity import v2 as v2_auth from keystoneauth1.identity import v3 as v3_auth from keystoneauth1 import loading from keystoneauth1 import session from oslo_utils import importutils import requests import cinderclient from cinderclient._i18n import _ from cinderclient import api_versions from cinderclient import client from cinderclient import exceptions as exc from cinderclient import utils try: osprofiler_profiler = importutils.try_import("osprofiler.profiler") except Exception: pass DEFAULT_MAJOR_OS_VOLUME_API_VERSION = "3" DEFAULT_CINDER_ENDPOINT_TYPE = 'publicURL' V3_SHELL = 'cinderclient.v3.shell' HINT_HELP_MSG = (" [hint: use '--os-volume-api-version' flag to show help " "message for proper version]") FILTER_CHECK = ["type-list", "backup-list", "get-pools", "list", "group-list", "group-snapshot-list", "message-list", "snapshot-list", "attachment-list"] RESOURCE_FILTERS = { "list": ["name", "status", "metadata", "bootable", "migration_status", "availability_zone", "group_id", "size"], "backup-list": ["name", "status", "volume_id"], "snapshot-list": ["name", "status", "volume_id", "metadata", "availability_zone"], "group-list": ["name"], "group-snapshot-list": ["name", "status", "group_id"], "attachment-list": ["volume_id", "status", "instance_id", "attach_status"], "message-list": ["resource_uuid", "resource_type", "event_id", "request_id", "message_level"], "get-pools": ["name", "volume_type"], "type-list": ["is_public"] } logging.basicConfig() logger = logging.getLogger(__name__) class CinderClientArgumentParser(argparse.ArgumentParser): def __init__(self, *args, **kwargs): super(CinderClientArgumentParser, self).__init__(*args, **kwargs) def error(self, message): """error(message: string) Prints a usage message incorporating the message to stderr and exits. """ self.print_usage(sys.stderr) # FIXME(lzyeval): if changes occur in argparse.ArgParser._check_value choose_from = ' (choose from' progparts = self.prog.partition(' ') self.exit(2, "error: %(errmsg)s\nTry '%(mainp)s help %(subp)s'" " for more information.\n" % {'errmsg': message.split(choose_from)[0], 'mainp': progparts[0], 'subp': progparts[2]}) def _get_option_tuples(self, option_string): """Avoid ambiguity in argument abbreviation. The idea of this method is to override the default behaviour to avoid ambiguity in the abbreviation feature of argparse. In the case that the ambiguity is generated by 2 or more parameters and only one is visible in the help and the others are with help=argparse.SUPPRESS, the ambiguity is solved by taking the visible one. The use case is for parameters that are left hidden for backward compatibility. """ result = super(CinderClientArgumentParser, self)._get_option_tuples( option_string) if len(result) > 1: aux = [x for x in result if x[0].help != argparse.SUPPRESS] if len(aux) == 1: result = aux return result class OpenStackCinderShell(object): def __init__(self): self.ks_logger = None self.client_logger = None self.extensions = [] def get_base_parser(self): parser = CinderClientArgumentParser( prog='cinder', description=__doc__.strip(), epilog=_('Run "cinder help SUBCOMMAND" for help on a subcommand.'), add_help=False, formatter_class=OpenStackHelpFormatter, ) # Global arguments parser.add_argument('-h', '--help', action='store_true', help=argparse.SUPPRESS) parser.add_argument('--version', action='version', version=cinderclient.__version__) parser.add_argument('-d', '--debug', action='store_true', default=utils.env('CINDERCLIENT_DEBUG', default=False), help=_('Shows debugging output.')) parser.add_argument('--service-type', metavar='', help=_('Service type. ' 'For most actions, default is volume.')) parser.add_argument('--service_type', help=argparse.SUPPRESS) parser.add_argument('--service-name', metavar='', default=utils.env('CINDER_SERVICE_NAME'), help=_('Service name. ' 'Default=env[CINDER_SERVICE_NAME].')) parser.add_argument('--service_name', help=argparse.SUPPRESS) parser.add_argument('--volume-service-name', metavar='', default=utils.env('CINDER_VOLUME_SERVICE_NAME'), help=_('Volume service name. ' 'Default=env[CINDER_VOLUME_SERVICE_NAME].')) parser.add_argument('--volume_service_name', help=argparse.SUPPRESS) parser.add_argument('--os-endpoint-type', metavar='', default=utils.env('CINDER_ENDPOINT_TYPE', default=utils.env('OS_ENDPOINT_TYPE', default=DEFAULT_CINDER_ENDPOINT_TYPE)), help=_('Endpoint type, which is publicURL or ' 'internalURL. ' 'Default=env[OS_ENDPOINT_TYPE] or ' 'nova env[CINDER_ENDPOINT_TYPE] or %s.') % DEFAULT_CINDER_ENDPOINT_TYPE) parser.add_argument('--os_endpoint_type', help=argparse.SUPPRESS) parser.add_argument('--os-volume-api-version', metavar='', default=utils.env('OS_VOLUME_API_VERSION', default=None), help=_('Block Storage API version. ' 'Accepts X, X.Y (where X is major and Y is minor ' 'part). NOTE: this client accepts only \'3\' for ' 'the major version. ' 'Default=env[OS_VOLUME_API_VERSION].')) parser.add_argument('--os_volume_api_version', help=argparse.SUPPRESS) parser.add_argument('--os-endpoint', metavar='', dest='os_endpoint', default=utils.env('CINDER_ENDPOINT'), help=_("Use this API endpoint instead of the " "Service Catalog. Defaults to " "env[CINDER_ENDPOINT].")) parser.add_argument('--os_endpoint', help=argparse.SUPPRESS) parser.add_argument('--retries', metavar='', type=int, default=0, help=_('Number of retries.')) parser.set_defaults(func=self.do_help) parser.set_defaults(command='') if osprofiler_profiler: parser.add_argument('--profile', metavar='HMAC_KEY', default=utils.env('OS_PROFILE'), help=_('HMAC key to use for encrypting ' 'context data for performance profiling ' 'of operation. This key needs to match the ' 'one configured on the cinder api server. ' 'Without key the profiling will not be ' 'triggered even if osprofiler is enabled ' 'on server side. Defaults to ' 'env[OS_PROFILE].')) self._append_global_identity_args(parser) return parser def _append_global_identity_args(self, parser): loading.register_session_argparse_arguments(parser) # Use "password" auth plugin as default and keep the explicit # "--os-token" arguments below for backward compatibility. default_auth_plugin = 'password' # Passing [] to loading.register_auth_argparse_arguments to avoid # the auth_type being overridden by the command line. loading.register_auth_argparse_arguments( parser, [], default=default_auth_plugin) parser.add_argument( '--os-auth-strategy', metavar='', default=utils.env('OS_AUTH_STRATEGY', default='keystone'), help=_('Authentication strategy (Env: OS_AUTH_STRATEGY' ', default keystone). For now, any other value will' ' disable the authentication.')) parser.add_argument( '--os_auth_strategy', help=argparse.SUPPRESS) # Change os_auth_type default value defined by # register_auth_argparse_arguments to be backward compatible # with OS_AUTH_SYSTEM. env_plugin = utils.env('OS_AUTH_TYPE', 'OS_AUTH_PLUGIN', 'OS_AUTH_SYSTEM') parser.set_defaults(os_auth_type=env_plugin) parser.add_argument('--os_auth_type', help=argparse.SUPPRESS) parser.set_defaults(os_username=utils.env('OS_USERNAME', 'CINDER_USERNAME')) parser.add_argument('--os_username', help=argparse.SUPPRESS) parser.set_defaults(os_password=utils.env('OS_PASSWORD', 'CINDER_PASSWORD')) parser.add_argument('--os_password', help=argparse.SUPPRESS) parser.set_defaults(os_project_name=utils.env('OS_PROJECT_NAME', 'CINDER_PROJECT_ID')) parser.add_argument( '--os_project_name', help=argparse.SUPPRESS) parser.set_defaults(os_project_id=utils.env('OS_PROJECT_ID', 'CINDER_PROJECT_ID')) parser.add_argument( '--os_project_id', help=argparse.SUPPRESS) parser.set_defaults(os_auth_url=utils.env('OS_AUTH_URL', 'CINDER_URL')) parser.add_argument('--os_auth_url', help=argparse.SUPPRESS) parser.set_defaults(os_user_id=utils.env('OS_USER_ID')) parser.add_argument( '--os_user_id', help=argparse.SUPPRESS) parser.set_defaults( os_user_domain_id=utils.env('OS_USER_DOMAIN_ID')) parser.add_argument( '--os_user_domain_id', help=argparse.SUPPRESS) parser.set_defaults( os_user_domain_name=utils.env('OS_USER_DOMAIN_NAME')) parser.add_argument( '--os_user_domain_name', help=argparse.SUPPRESS) parser.set_defaults( os_project_domain_id=utils.env('OS_PROJECT_DOMAIN_ID')) parser.set_defaults( os_project_domain_name=utils.env('OS_PROJECT_DOMAIN_NAME')) parser.set_defaults( os_region_name=utils.env('OS_REGION_NAME', 'CINDER_REGION_NAME')) parser.add_argument('--os_region_name', help=argparse.SUPPRESS) parser.add_argument( '--os-token', metavar='', default=utils.env('OS_TOKEN'), help=_('Defaults to env[OS_TOKEN].')) parser.add_argument( '--os_token', help=argparse.SUPPRESS) parser.add_argument( '--os-url', metavar='', default=utils.env('OS_URL'), help=_('Defaults to env[OS_URL].')) parser.add_argument( '--os_url', help=argparse.SUPPRESS) parser.set_defaults(insecure=utils.env('CINDERCLIENT_INSECURE', default=False)) def get_subcommand_parser(self, version, do_help=False, input_args=None): parser = self.get_base_parser() self.subcommands = {} subparsers = parser.add_subparsers(metavar='') actions_module = importutils.import_module(V3_SHELL) self._find_actions(subparsers, actions_module, version, do_help, input_args) self._find_actions(subparsers, self, version, do_help, input_args) for extension in self.extensions: self._find_actions(subparsers, extension.module, version, do_help, input_args) self._add_bash_completion_subparser(subparsers) return parser def _add_bash_completion_subparser(self, subparsers): subparser = subparsers.add_parser( 'bash_completion', add_help=False, formatter_class=OpenStackHelpFormatter) self.subcommands['bash_completion'] = subparser subparser.set_defaults(func=self.do_bash_completion) def _build_versioned_help_message(self, start_version, end_version): if start_version and end_version: msg = (_(" (Supported by API versions %(start)s - %(end)s)") % {"start": start_version.get_string(), "end": end_version.get_string()}) elif start_version: msg = (_(" (Supported by API version %(start)s and later)") % {"start": start_version.get_string()}) else: msg = (_(" (Supported until API version %(end)s)") % {"end": end_version.get_string()}) return str(msg) def _find_actions(self, subparsers, actions_module, version, do_help, input_args): for attr in (a for a in dir(actions_module) if a.startswith('do_')): # I prefer to be hyphen-separated instead of underscores. command = attr[3:].replace('_', '-') callback = getattr(actions_module, attr) desc = callback.__doc__ or '' action_help = desc.strip().split('\n')[0] if hasattr(callback, "versioned"): additional_msg = "" subs = api_versions.get_substitutions( utils.get_function_name(callback)) if do_help: additional_msg = self._build_versioned_help_message( subs[0].start_version, subs[-1].end_version) if version.is_latest(): additional_msg += HINT_HELP_MSG subs = [versioned_method for versioned_method in subs if version.matches(versioned_method.start_version, versioned_method.end_version)] if not subs: # There is no proper versioned method. continue # Use the "latest" substitution. callback = subs[-1].func desc = callback.__doc__ or desc action_help = desc.strip().split('\n')[0] action_help += additional_msg exclusive_args = getattr(callback, 'exclusive_args', {}) arguments = getattr(callback, 'arguments', []) subparser = subparsers.add_parser( command, help=action_help, description=desc, add_help=False, formatter_class=OpenStackHelpFormatter) subparser.add_argument('-h', '--help', action='help', help=argparse.SUPPRESS,) self.subcommands[command] = subparser self._add_subparser_args(subparser, arguments, version, do_help, input_args, command) self._add_subparser_exclusive_args(subparser, exclusive_args, version, do_help, input_args, command) subparser.set_defaults(func=callback) def _add_subparser_args(self, subparser, arguments, version, do_help, input_args, command): # NOTE(ntpttr): We get a counter for each argument in this # command here because during the microversion check we only # want to raise an exception if no version of the argument # matches the current microversion. The exception will only # be raised after the last instance of a particular argument # fails the check. arg_counter = collections.defaultdict(int) for (args, kwargs) in arguments: arg_counter[args[0]] += 1 for (args, kwargs) in arguments: start_version = kwargs.get("start_version", None) start_version = api_versions.APIVersion(start_version) end_version = kwargs.get('end_version', None) end_version = api_versions.APIVersion(end_version) if do_help and (start_version or end_version): kwargs["help"] = kwargs.get("help", "") + ( self._build_versioned_help_message(start_version, end_version)) if not version.matches(start_version, end_version): if args[0] in input_args and command == input_args[0]: if arg_counter[args[0]] == 1: # This is the last version of this argument, # raise the exception. raise exc.UnsupportedAttribute(args[0], start_version, end_version) arg_counter[args[0]] -= 1 continue kw = kwargs.copy() kw.pop("start_version", None) kw.pop("end_version", None) subparser.add_argument(*args, **kw) def _add_subparser_exclusive_args(self, subparser, exclusive_args, version, do_help, input_args, command): for group_name, arguments in exclusive_args.items(): if group_name == '__required__': continue required = exclusive_args['__required__'][group_name] exclusive_group = subparser.add_mutually_exclusive_group( required=required) self._add_subparser_args(exclusive_group, arguments, version, do_help, input_args, command) def setup_debugging(self, debug): if not debug: return streamhandler = logging.StreamHandler() streamformat = "%(levelname)s (%(module)s:%(lineno)d) %(message)s" streamhandler.setFormatter(logging.Formatter(streamformat)) logger.setLevel(logging.DEBUG if debug else logging.WARNING) logger.addHandler(streamhandler) self.client_logger = logging.getLogger(client.__name__) ch = logging.StreamHandler() self.client_logger.setLevel(logging.DEBUG) self.client_logger.addHandler(ch) if hasattr(requests, 'logging'): requests.logging.getLogger(requests.__name__).addHandler(ch) self.ks_logger = logging.getLogger("keystoneauth") self.ks_logger.setLevel(logging.DEBUG) def _delimit_metadata_args(self, argv): """This function adds -- separator at the appropriate spot """ word = '--metadata' tmp = [] # flag is true in between metadata option and next option metadata_options = False if word in argv: for arg in argv: if arg == word: metadata_options = True elif metadata_options: if arg.startswith('--'): metadata_options = False elif '=' not in arg: tmp.append(u'--') metadata_options = False tmp.append(arg) return tmp else: return argv @staticmethod def _validate_input_api_version(options): if not options.os_volume_api_version: api_version = api_versions.APIVersion(api_versions.MAX_VERSION) else: api_version = api_versions.get_api_version( options.os_volume_api_version) return api_version @staticmethod def downgrade_warning(requested, discovered): logger.warning("API version %s requested, " % requested.get_string()) logger.warning("downgrading to %s based on server support." % discovered.get_string()) def check_duplicate_filters(self, argv, filter): resource = RESOURCE_FILTERS[filter] filters = [] for opt in range(len(argv)): if argv[opt].startswith('--'): if argv[opt] == '--filters': key, __ = argv[opt + 1].split('=') if key in resource: filters.append(key) elif argv[opt][2:] in resource: filters.append(argv[opt][2:]) if len(set(filters)) != len(filters): raise exc.CommandError( "Filters are only allowed to be passed once.") def main(self, argv): # Parse args once to find version and debug settings for filter in FILTER_CHECK: if filter in argv: self.check_duplicate_filters(argv, filter) break parser = self.get_base_parser() (options, args) = parser.parse_known_args(argv) self.setup_debugging(options.debug) api_version_input = True self.options = options do_help = ('help' in argv) or ( '--help' in argv) or ('-h' in argv) or not argv api_version = self._validate_input_api_version(options) # build available subcommands based on version major_version_string = "%s" % api_version.ver_major self.extensions = client.discover_extensions(major_version_string) self._run_extension_hooks('__pre_parse_args__') subcommand_parser = self.get_subcommand_parser(api_version, do_help, args) self.parser = subcommand_parser if argv and len(argv) > 1 and '--help' in argv: argv = [x for x in argv if x != '--help'] if argv[0] in self.subcommands: self.subcommands[argv[0]].print_help() return 0 if options.help or not argv: subcommand_parser.print_help() return 0 argv = self._delimit_metadata_args(argv) args = subcommand_parser.parse_args(argv) self._run_extension_hooks('__post_parse_args__', args) # Short-circuit and deal with help right away. if args.func == self.do_help: self.do_help(args) return 0 elif args.func == self.do_bash_completion: self.do_bash_completion(args) return 0 (os_username, os_password, os_project_name, os_auth_url, os_region_name, os_project_id, endpoint_type, service_type, service_name, volume_service_name, os_endpoint, cacert, os_auth_type) = ( args.os_username, args.os_password, args.os_project_name, args.os_auth_url, args.os_region_name, args.os_project_id, args.os_endpoint_type, args.service_type, args.service_name, args.volume_service_name, args.os_endpoint, args.os_cacert, args.os_auth_type) auth_session = None if os_auth_type and os_auth_type != "keystone": auth_plugin = loading.load_auth_from_argparse_arguments( self.options) auth_session = loading.load_session_from_argparse_arguments( self.options, auth=auth_plugin) else: auth_plugin = None if not service_type: service_type = client.SERVICE_TYPES[major_version_string] # FIXME(usrleon): Here should be restrict for project id same as # for os_username or os_password but for compatibility it is not. # V3 stuff project_info_provided = ((self.options.os_project_name and (self.options.os_project_domain_name or self.options.os_project_domain_id)) or self.options.os_project_id or self.options.os_project_name) # NOTE(e0ne): if auth_session exists it means auth plugin created # session and we don't need to check for password and other # authentification-related things. if not utils.isunauthenticated(args.func) and not auth_session: if not os_password: # No password, If we've got a tty, try prompting for it if hasattr(sys.stdin, 'isatty') and sys.stdin.isatty(): # Check for Ctl-D try: os_password = getpass.getpass('OS Password: ') # Initialize options.os_password with password # input from tty. It is used in _get_keystone_session. options.os_password = os_password except EOFError: pass # No password because we didn't have a tty or the # user Ctl-D when prompted. if not os_password: raise exc.CommandError("You must provide a password " "through --os-password, " "env[OS_PASSWORD] " "or, prompted response.") if not project_info_provided: raise exc.CommandError(_( "You must provide a project_id or project_name (with " "project_domain_name or project_domain_id) via " " --os-project-id (env[OS_PROJECT_ID])" " --os-project-name (env[OS_PROJECT_NAME])," " --os-project-domain-id " "(env[OS_PROJECT_DOMAIN_ID])" " --os-project-domain-name " "(env[OS_PROJECT_DOMAIN_NAME])" )) if not os_auth_url: raise exc.CommandError( "You must provide an authentication URL " "through --os-auth-url or env[OS_AUTH_URL].") if not project_info_provided: raise exc.CommandError(_( "You must provide a project_id or project_name (with " "project_domain_name or project_domain_id) via " " --os-project-id (env[OS_PROJECT_ID])" " --os-project-name (env[OS_PROJECT_NAME])," " --os-project-domain-id " "(env[OS_PROJECT_DOMAIN_ID])" " --os-project-domain-name " "(env[OS_PROJECT_DOMAIN_NAME])" )) if not os_auth_url and not auth_plugin: raise exc.CommandError( "You must provide an authentication URL " "through --os-auth-url or env[OS_AUTH_URL].") if not auth_session: auth_session = self._get_keystone_session() insecure = self.options.insecure client_args = dict( region_name=os_region_name, tenant_id=os_project_id, endpoint_type=endpoint_type, extensions=self.extensions, service_type=service_type, service_name=service_name, volume_service_name=volume_service_name, os_endpoint=os_endpoint, retries=options.retries, http_log_debug=args.debug, insecure=insecure, cacert=cacert, auth_system=os_auth_type, auth_plugin=auth_plugin, session=auth_session, logger=self.ks_logger if auth_session else self.client_logger) self.cs = client.Client( api_version, os_username, os_password, os_project_name, os_auth_url, **client_args) try: if not utils.isunauthenticated(args.func): self.cs.authenticate() except exc.Unauthorized: raise exc.CommandError("OpenStack credentials are not valid.") except exc.AuthorizationFailure: raise exc.CommandError("Unable to authorize user.") # FIXME: this section figuring out the api version could use # analysis and refactoring. See # https://review.opendev.org/c/openstack/python-cinderclient/+/766882/ # for some ideas. endpoint_api_version = None # Try to get the API version from the endpoint URL. If that fails fall # back to trying to use what the user specified via # --os-volume-api-version or with the OS_VOLUME_API_VERSION environment # variable. Fail safe is to use the default API setting. try: endpoint_api_version = \ self.cs.get_volume_api_version_from_endpoint() except exc.UnsupportedVersion: endpoint_api_version = options.os_volume_api_version # FIXME: api_version_input is initialized as True at the beginning # of this function and never modified if api_version_input and endpoint_api_version: logger.warning("Cannot determine the API version from " "the endpoint URL. Falling back to the " "user-specified version: %s", endpoint_api_version) elif endpoint_api_version: logger.warning("Cannot determine the API version from the " "endpoint URL or user input. Falling back " "to the default API version: %s", endpoint_api_version) else: msg = _("Cannot determine API version. Please specify by " "using --os-volume-api-version option.") raise exc.UnsupportedVersion(msg) API_MAX_VERSION = api_versions.APIVersion(api_versions.MAX_VERSION) # FIXME: the endpoint_api_version[0] can ONLY be '3' now, so the # above line should probably be ripped out and this condition removed if endpoint_api_version[0] == '3': disc_client = client.Client(API_MAX_VERSION, os_username, os_password, os_project_name, os_auth_url, **client_args) self.cs, discovered_version = self._discover_client( disc_client, api_version, args.os_endpoint_type, args.service_type, os_username, os_password, os_project_name, os_auth_url, client_args) if discovered_version < api_version: self.downgrade_warning(api_version, discovered_version) profile = osprofiler_profiler and options.profile if profile: osprofiler_profiler.init(options.profile) try: args.func(self.cs, args) finally: if profile: trace_id = osprofiler_profiler.get().get_base_id() print("Trace ID: %s" % trace_id) print("To display trace use next command:\n" "osprofiler trace show --html %s " % trace_id) def _discover_client(self, current_client, os_api_version, os_endpoint_type, os_service_type, os_username, os_password, os_project_name, os_auth_url, client_args): discovered_version = api_versions.discover_version( current_client, os_api_version) if not os_endpoint_type: os_endpoint_type = DEFAULT_CINDER_ENDPOINT_TYPE if not os_service_type: os_service_type = self._discover_service_type(discovered_version) API_MAX_VERSION = api_versions.APIVersion(api_versions.MAX_VERSION) if (discovered_version != API_MAX_VERSION or os_service_type != 'volume' or os_endpoint_type != DEFAULT_CINDER_ENDPOINT_TYPE): client_args['service_type'] = os_service_type client_args['endpoint_type'] = os_endpoint_type return (client.Client(discovered_version, os_username, os_password, os_project_name, os_auth_url, **client_args), discovered_version) else: return current_client, discovered_version def _discover_service_type(self, discovered_version): # FIXME: this function is either no longer needed or could use a # refactoring. The official service type is 'block-storage', # which isn't even present here. (Devstack creates 2 service # types which it maps to v3: 'block-storage' and 'volumev3'. # The default 'catalog_type' in tempest is 'volumev3'.) SERVICE_TYPES = {'1': 'volume', '2': 'volumev2', '3': 'volumev3'} major_version = discovered_version.get_major_version() service_type = SERVICE_TYPES[major_version] return service_type def _run_extension_hooks(self, hook_type, *args, **kwargs): """Runs hooks for all registered extensions.""" for extension in self.extensions: extension.run_hooks(hook_type, *args, **kwargs) def do_bash_completion(self, args): """Prints arguments for bash_completion. Prints all commands and options to stdout so that the cinder.bash_completion script does not have to hard code them. """ commands = set() options = set() for sc_str, sc in list(self.subcommands.items()): commands.add(sc_str) for option in sc._optionals._option_string_actions: options.add(option) commands.remove('bash-completion') commands.remove('bash_completion') print(' '.join(commands | options)) @utils.arg('command', metavar='', nargs='?', help='Shows help for .') def do_help(self, args): """ Shows help about this program or one of its subcommands. """ if args.command: if args.command in self.subcommands: self.subcommands[args.command].print_help() else: raise exc.CommandError("'%s' is not a valid subcommand" % args.command) else: self.parser.print_help() def get_v2_auth(self, v2_auth_url): username = self.options.os_username password = self.options.os_password tenant_id = self.options.os_project_id tenant_name = self.options.os_project_name return v2_auth.Password( v2_auth_url, username=username, password=password, tenant_id=tenant_id, tenant_name=tenant_name) def get_v3_auth(self, v3_auth_url): username = self.options.os_username user_id = self.options.os_user_id user_domain_name = self.options.os_user_domain_name user_domain_id = self.options.os_user_domain_id password = self.options.os_password project_id = self.options.os_project_id project_name = self.options.os_project_name project_domain_name = self.options.os_project_domain_name project_domain_id = self.options.os_project_domain_id return v3_auth.Password( v3_auth_url, username=username, password=password, user_id=user_id, user_domain_name=user_domain_name, user_domain_id=user_domain_id, project_id=project_id, project_name=project_name, project_domain_name=project_domain_name, project_domain_id=project_domain_id, ) def _discover_auth_versions(self, session, auth_url): # discover the API versions the server is supporting based on the # given URL v2_auth_url = None v3_auth_url = None try: ks_discover = discover.Discover(session=session, url=auth_url) v2_auth_url = ks_discover.url_for('2.0') v3_auth_url = ks_discover.url_for('3.0') except exceptions.DiscoveryFailure: # Discovery response mismatch. Raise the error raise except Exception: # Some public clouds throw some other exception or doesn't support # discovery. In that case try to determine version from auth_url # API version from the original URL url_parts = urlparse.urlparse(auth_url) (scheme, netloc, path, params, query, fragment) = url_parts path = path.lower() if path.startswith('/v3'): v3_auth_url = auth_url elif path.startswith('/v2'): v2_auth_url = auth_url else: raise exc.CommandError('Unable to determine the Keystone' ' version to authenticate with ' 'using the given auth_url.') return (v2_auth_url, v3_auth_url) def _get_keystone_session(self, **kwargs): # first create a Keystone session cacert = self.options.os_cacert or None cert = self.options.os_cert or None if cert and self.options.os_key: cert = cert, self.options.os_key insecure = self.options.insecure or False if insecure: verify = False else: verify = cacert or True ks_session = session.Session(verify=verify, cert=cert) # discover the supported keystone versions using the given url (v2_auth_url, v3_auth_url) = self._discover_auth_versions( session=ks_session, auth_url=self.options.os_auth_url) username = self.options.os_username or None user_domain_name = self.options.os_user_domain_name or None user_domain_id = self.options.os_user_domain_id or None auth = None if v3_auth_url and v2_auth_url: # support both v2 and v3 auth. Use v3 if possible. if username: if user_domain_name or user_domain_id: # use v3 auth auth = self.get_v3_auth(v3_auth_url) else: # use v2 auth auth = self.get_v2_auth(v2_auth_url) elif v3_auth_url: # support only v3 auth = self.get_v3_auth(v3_auth_url) elif v2_auth_url: # support only v2 auth = self.get_v2_auth(v2_auth_url) else: raise exc.CommandError('Unable to determine the Keystone version ' 'to authenticate with using the given ' 'auth_url.') ks_session.auth = auth return ks_session # I'm picky about my shell help. class OpenStackHelpFormatter(argparse.HelpFormatter): def start_section(self, heading): # Title-case the headings heading = heading.title() super(OpenStackHelpFormatter, self).start_section(heading) def main(): try: OpenStackCinderShell().main(sys.argv[1:]) except KeyboardInterrupt: print("... terminating cinder client", file=sys.stderr) sys.exit(130) except Exception as e: logger.debug(e, exc_info=1) print("ERROR: %s" % str(e), file=sys.stderr) sys.exit(1) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/shell_utils.py0000664000175000017500000002363300000000000023102 0ustar00zuulzuul00000000000000# 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 sys import time from cinderclient import exceptions from cinderclient import utils _quota_resources = ['volumes', 'snapshots', 'gigabytes', 'backups', 'backup_gigabytes', 'per_volume_gigabytes', 'groups', ] _quota_infos = ['Type', 'In_use', 'Reserved', 'Limit', 'Allocated'] def print_volume_image(image_resp_tuple): # image_resp_tuple = tuple (response, body) image = image_resp_tuple[1] vt = image['os-volume_upload_image'].get('volume_type') if vt is not None: image['os-volume_upload_image']['volume_type'] = vt.get('name') utils.print_dict(image['os-volume_upload_image']) def poll_for_status(poll_fn, obj_id, action, final_ok_states, poll_period=5, show_progress=True): """Blocks while an action occurs. Periodically shows progress.""" def print_progress(progress): if show_progress: msg = ('\rInstance %(action)s... %(progress)s%% complete' % dict(action=action, progress=progress)) else: msg = '\rInstance %(action)s...' % dict(action=action) sys.stdout.write(msg) sys.stdout.flush() print() while True: obj = poll_fn(obj_id) status = obj.status.lower() progress = getattr(obj, 'progress', None) or 0 if status in final_ok_states: print_progress(100) print("\nFinished") break elif status == "error": print("\nError %(action)s instance" % {'action': action}) break else: print_progress(progress) time.sleep(poll_period) def find_volume_snapshot(cs, snapshot): """Gets a volume snapshot by name or ID.""" return utils.find_resource(cs.volume_snapshots, snapshot) def find_vtype(cs, vtype): """Gets a volume type by name or ID.""" return utils.find_resource(cs.volume_types, vtype) def find_gtype(cs, gtype): """Gets a group type by name or ID.""" return utils.find_resource(cs.group_types, gtype) def find_backup(cs, backup): """Gets a backup by name or ID.""" return utils.find_resource(cs.backups, backup) def find_consistencygroup(cs, consistencygroup): """Gets a consistency group by name or ID.""" return utils.find_resource(cs.consistencygroups, consistencygroup) def find_group(cs, group, **kwargs): """Gets a group by name or ID.""" kwargs['is_group'] = True return utils.find_resource(cs.groups, group, **kwargs) def find_cgsnapshot(cs, cgsnapshot): """Gets a cgsnapshot by name or ID.""" return utils.find_resource(cs.cgsnapshots, cgsnapshot) def find_group_snapshot(cs, group_snapshot): """Gets a group_snapshot by name or ID.""" return utils.find_resource(cs.group_snapshots, group_snapshot) def find_transfer(cs, transfer): """Gets a transfer by name or ID.""" return utils.find_resource(cs.transfers, transfer) def find_qos_specs(cs, qos_specs): """Gets a qos specs by ID.""" return utils.find_resource(cs.qos_specs, qos_specs) def find_message(cs, message): """Gets a message by ID.""" return utils.find_resource(cs.messages, message) def print_volume_snapshot(snapshot): utils.print_dict(snapshot._info) def translate_keys(collection, convert): for item in collection: keys = item.__dict__ for from_key, to_key in convert: if from_key in keys and to_key not in keys: setattr(item, to_key, item._info[from_key]) def translate_volume_keys(collection): convert = [('volumeType', 'volume_type'), ('os-vol-tenant-attr:tenant_id', 'tenant_id')] translate_keys(collection, convert) def translate_volume_snapshot_keys(collection): convert = [('volumeId', 'volume_id')] translate_keys(collection, convert) def translate_availability_zone_keys(collection): convert = [('zoneName', 'name'), ('zoneState', 'status')] translate_keys(collection, convert) def extract_filters(args): filters = {} for f in args: if '=' in f: (key, value) = f.split('=', 1) if value.startswith('{') and value.endswith('}'): value = _build_internal_dict(value[1:-1]) filters[key] = value else: print("WARNING: Ignoring the filter %s while showing result." % f) return filters def _build_internal_dict(content): result = {} for pair in content.split(','): k, v = pair.split(':', 1) result.update({k.strip(): v.strip()}) return result def extract_metadata(args, type='user_metadata'): metadata = {} if type == 'image_metadata': args_metadata = args.image_metadata else: args_metadata = args.metadata for metadatum in args_metadata: # unset doesn't require a val, so we have the if/else if '=' in metadatum: (key, value) = metadatum.split('=', 1) else: key = metadatum value = None metadata[key] = value return metadata def print_volume_type_list(vtypes): utils.print_list(vtypes, ['ID', 'Name', 'Description', 'Is_Public']) def print_group_type_list(gtypes): utils.print_list(gtypes, ['ID', 'Name', 'Description']) def print_resource_filter_list(filters): formatter = {'Filters': lambda resource: ', '.join(resource.filters)} utils.print_list(filters, ['Resource', 'Filters'], formatters=formatter) def quota_show(quotas): quotas_info_dict = quotas._info quota_dict = {} for resource in quotas_info_dict.keys(): good_name = False for name in _quota_resources: if resource.startswith(name): good_name = True if not good_name: continue quota_dict[resource] = getattr(quotas, resource, None) utils.print_dict(quota_dict) def quota_usage_show(quotas): quota_list = [] quotas_info_dict = quotas._info for resource in quotas_info_dict.keys(): good_name = False for name in _quota_resources: if resource.startswith(name): good_name = True if not good_name: continue quota_info = getattr(quotas, resource, None) quota_info['Type'] = resource quota_info = dict((k.capitalize(), v) for k, v in quota_info.items()) quota_list.append(quota_info) utils.print_list(quota_list, _quota_infos) def quota_update(manager, identifier, args): updates = {} for resource in _quota_resources: val = getattr(args, resource, None) if val is not None: if args.volume_type: resource = resource + '_%s' % args.volume_type updates[resource] = val if updates: skip_validation = getattr(args, 'skip_validation', True) if not skip_validation: updates['skip_validation'] = skip_validation quota_show(manager.update(identifier, **updates)) else: msg = 'Must supply at least one quota field to update.' raise exceptions.ClientException(code=1, message=msg) def find_volume_type(cs, vtype): """Gets a volume type by name or ID.""" return utils.find_resource(cs.volume_types, vtype) def find_group_type(cs, gtype): """Gets a group type by name or ID.""" return utils.find_resource(cs.group_types, gtype) def print_volume_encryption_type_list(encryption_types): """ Lists volume encryption types. :param encryption_types: a list of :class: VolumeEncryptionType instances """ utils.print_list(encryption_types, ['Volume Type ID', 'Provider', 'Cipher', 'Key Size', 'Control Location']) def print_qos_specs(qos_specs): # formatters defines field to be converted from unicode to string utils.print_dict(qos_specs._info, formatters=['specs']) def print_qos_specs_list(q_specs): utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) def print_qos_specs_and_associations_list(q_specs): utils.print_list(q_specs, ['ID', 'Name', 'Consumer', 'specs']) def print_associations_list(associations): utils.print_list(associations, ['Association_Type', 'Name', 'ID']) def _poll_for_status(poll_fn, obj_id, info, action, final_ok_states, timeout_period, global_request_id=None, messages=None, poll_period=2, status_field="status"): """Block while an action is being performed.""" time_elapsed = 0 while True: time.sleep(poll_period) time_elapsed += poll_period obj = poll_fn(obj_id) status = getattr(obj, status_field) info[status_field] = status if status: status = status.lower() if status in final_ok_states: break elif status == "error": utils.print_dict(info) if global_request_id: search_opts = { 'request_id': global_request_id } message_list = messages.list(search_opts=search_opts) try: fault_msg = message_list[0].user_message except IndexError: fault_msg = "Unknown error. Operation failed." raise exceptions.ResourceInErrorState(obj, fault_msg) elif time_elapsed == timeout_period: utils.print_dict(info) raise exceptions.TimeoutException(obj, action) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.2658958 python-cinderclient-8.3.0/cinderclient/tests/0000775000175000017500000000000000000000000021334 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/__init__.py0000664000175000017500000000000000000000000023433 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.2658958 python-cinderclient-8.3.0/cinderclient/tests/functional/0000775000175000017500000000000000000000000023476 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/functional/__init__.py0000664000175000017500000000000000000000000025575 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/functional/base.py0000664000175000017500000001401100000000000024757 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import configparser import os import time from tempest.lib.cli import base from tempest.lib.cli import output_parser from tempest.lib import exceptions _CREDS_FILE = 'functional_creds.conf' def credentials(): """Retrieves credentials to run functional tests Credentials are either read from the environment or from a config file ('functional_creds.conf'). Environment variables override those from the config file. The 'functional_creds.conf' file is the clean and new way to use (by default tox 2.0 does not pass environment variables). """ username = os.environ.get('OS_USERNAME') password = os.environ.get('OS_PASSWORD') tenant_name = (os.environ.get('OS_TENANT_NAME') or os.environ.get('OS_PROJECT_NAME')) auth_url = os.environ.get('OS_AUTH_URL') config = configparser.RawConfigParser() if config.read(_CREDS_FILE): username = username or config.get('admin', 'user') password = password or config.get('admin', 'pass') tenant_name = tenant_name or config.get('admin', 'tenant') auth_url = auth_url or config.get('auth', 'uri') return { 'username': username, 'password': password, 'tenant_name': tenant_name, 'uri': auth_url } class ClientTestBase(base.ClientTestBase): """Cinder base class, issues calls to cinderclient. """ def setUp(self): super(ClientTestBase, self).setUp() self.clients = self._get_clients() self.parser = output_parser def _get_clients(self): cli_dir = os.environ.get( 'OS_CINDERCLIENT_EXEC_DIR', os.path.join(os.path.abspath('.'), '.tox/functional/bin')) return base.CLIClient(cli_dir=cli_dir, **credentials()) def cinder(self, *args, **kwargs): return self.clients.cinder(*args, **kwargs) def assertTableHeaders(self, output_lines, field_names): """Verify that output table has headers item listed in field_names. :param output_lines: output table from cmd :param field_names: field names from the output table of the cmd """ table = self.parser.table(output_lines) headers = table['headers'] for field in field_names: self.assertIn(field, headers) def assert_object_details(self, expected, items): """Check presence of common object properties. :param expected: expected object properties :param items: object properties """ for value in expected: self.assertIn(value, items) def _get_property_from_output(self, output): """Create a dictionary from an output :param output: the output of the cmd """ obj = {} items = self.parser.listing(output) for item in items: obj[item['Property']] = str(item['Value']) return obj def object_cmd(self, object_name, cmd): return (object_name + '-' + cmd if object_name != 'volume' else cmd) def wait_for_object_status(self, object_name, object_id, status, timeout=120, interval=3): """Wait until object reaches given status. :param object_name: object name :param object_id: uuid4 id of an object :param status: expected status of an object :param timeout: timeout in seconds """ cmd = self.object_cmd(object_name, 'show') start_time = time.time() while time.time() - start_time < timeout: if status in self.cinder(cmd, params=object_id): break time.sleep(interval) else: self.fail("%s %s did not reach status %s after %d seconds." % (object_name, object_id, status, timeout)) def check_object_deleted(self, object_name, object_id, timeout=60): """Check that object deleted successfully. :param object_name: object name :param object_id: uuid4 id of an object :param timeout: timeout in seconds """ cmd = self.object_cmd(object_name, 'show') try: start_time = time.time() while time.time() - start_time < timeout: if object_id not in self.cinder(cmd, params=object_id): break except exceptions.CommandFailed: pass else: self.fail("%s %s not deleted after %d seconds." % (object_name, object_id, timeout)) def object_create(self, object_name, params): """Create an object. :param object_name: object name :param params: parameters to cinder command :return: object dictionary """ cmd = self.object_cmd(object_name, 'create') output = self.cinder(cmd, params=params) object = self._get_property_from_output(output) self.addCleanup(self.object_delete, object_name, object['id']) if object_name in ('volume', 'snapshot', 'backup'): self.wait_for_object_status( object_name, object['id'], 'available') return object def object_delete(self, object_name, object_id): """Delete specified object by ID. :param object_name: object name :param object_id: uuid4 id of an object """ cmd = self.object_cmd(object_name, 'list') cmd_delete = self.object_cmd(object_name, 'delete') if object_id in self.cinder(cmd): self.cinder(cmd_delete, params=object_id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/functional/test_cli.py0000664000175000017500000001375100000000000025665 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient.tests.functional import base class CinderVolumeTests(base.ClientTestBase): """Check of base cinder volume commands.""" CREATE_VOLUME_PROPERTY = ( 'attachments', 'os-vol-tenant-attr:tenant_id', 'availability_zone', 'bootable', 'created_at', 'description', 'encrypted', 'id', 'metadata', 'name', 'size', 'status', 'user_id', 'volume_type') SHOW_VOLUME_PROPERTY = ('attachment_ids', 'attached_servers', 'availability_zone', 'bootable', 'created_at', 'description', 'encrypted', 'id', 'metadata', 'name', 'size', 'status', 'user_id', 'volume_type') def test_volume_create_delete_id(self): """Create and delete a volume by ID.""" volume = self.object_create('volume', params='1') self.assert_object_details(self.CREATE_VOLUME_PROPERTY, volume.keys()) self.object_delete('volume', volume['id']) self.check_object_deleted('volume', volume['id']) def test_volume_create_delete_name(self): """Create and delete a volume by name.""" volume = self.object_create('volume', params='1 --name TestVolumeNamedCreate') self.cinder('delete', params='TestVolumeNamedCreate') self.check_object_deleted('volume', volume['id']) def test_volume_show(self): """Show volume details.""" volume = self.object_create('volume', params='1 --name TestVolumeShow') output = self.cinder('show', params='TestVolumeShow') volume = self._get_property_from_output(output) self.assertEqual('TestVolumeShow', volume['name']) self.assert_object_details(self.SHOW_VOLUME_PROPERTY, volume.keys()) self.object_delete('volume', volume['id']) self.check_object_deleted('volume', volume['id']) def test_volume_extend(self): """Extend a volume size.""" volume = self.object_create('volume', params='1 --name TestVolumeExtend') self.cinder('extend', params="%s %s" % (volume['id'], 2)) self.wait_for_object_status('volume', volume['id'], 'available') output = self.cinder('show', params=volume['id']) volume = self._get_property_from_output(output) self.assertEqual('2', volume['size']) self.object_delete('volume', volume['id']) self.check_object_deleted('volume', volume['id']) class CinderSnapshotTests(base.ClientTestBase): """Check of base cinder snapshot commands.""" SNAPSHOT_PROPERTY = ('created_at', 'description', 'metadata', 'id', 'name', 'size', 'status', 'volume_id') def test_snapshot_create_and_delete(self): """Create a volume snapshot and then delete.""" volume = self.object_create('volume', params='1') snapshot = self.object_create('snapshot', params=volume['id']) self.assert_object_details(self.SNAPSHOT_PROPERTY, snapshot.keys()) self.object_delete('snapshot', snapshot['id']) self.check_object_deleted('snapshot', snapshot['id']) self.object_delete('volume', volume['id']) self.check_object_deleted('volume', volume['id']) class CinderBackupTests(base.ClientTestBase): """Check of base cinder backup commands.""" BACKUP_PROPERTY = ('id', 'name', 'volume_id') def test_backup_create_and_delete(self): """Create a volume backup and then delete.""" volume = self.object_create('volume', params='1') backup = self.object_create('backup', params=volume['id']) self.assert_object_details(self.BACKUP_PROPERTY, backup.keys()) self.object_delete('volume', volume['id']) self.check_object_deleted('volume', volume['id']) self.object_delete('backup', backup['id']) self.check_object_deleted('backup', backup['id']) class VolumeTransferTests(base.ClientTestBase): """Check of base cinder volume transfers command""" TRANSFER_PROPERTY = ('created_at', 'volume_id', 'id', 'auth_key', 'name') TRANSFER_SHOW_PROPERTY = ('created_at', 'volume_id', 'id', 'name') def test_transfer_create_delete(self): """Create and delete a volume transfer""" volume = self.object_create('volume', params='1') transfer = self.object_create('transfer', params=volume['id']) self.assert_object_details(self.TRANSFER_PROPERTY, transfer.keys()) self.object_delete('transfer', transfer['id']) self.check_object_deleted('transfer', transfer['id']) self.object_delete('volume', volume['id']) self.check_object_deleted('volume', volume['id']) def test_transfer_show_delete_by_name(self): """Show volume transfer by name""" volume = self.object_create('volume', params='1') self.object_create( 'transfer', params=('%s --name TEST_TRANSFER_SHOW' % volume['id'])) output = self.cinder('transfer-show', params='TEST_TRANSFER_SHOW') transfer = self._get_property_from_output(output) self.assertEqual('TEST_TRANSFER_SHOW', transfer['name']) self.assert_object_details(self.TRANSFER_SHOW_PROPERTY, transfer.keys()) self.object_delete('transfer', 'TEST_TRANSFER_SHOW') self.check_object_deleted('transfer', 'TEST_TRANSFER_SHOW') self.object_delete('volume', volume['id']) self.check_object_deleted('volume', volume['id']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/functional/test_readonly_cli.py0000664000175000017500000000742300000000000027561 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient.tests.functional import base class CinderClientReadOnlyTests(base.ClientTestBase): """Basic read-only test for cinderclient. Simple check of base list commands, verify they respond and include the expected headers in the resultant table. Not intended for testing things that require actual resource creation/manipulation, thus the name 'read-only'. """ # Commands in order listed in 'cinder help' def test_absolute_limits(self): limits = self.cinder('absolute-limits') self.assertTableHeaders(limits, ['Name', 'Value']) def test_availability_zones(self): zone_list = self.cinder('availability-zone-list') self.assertTableHeaders(zone_list, ['Name', 'Status']) def test_backup_list(self): backup_list = self.cinder('backup-list') self.assertTableHeaders(backup_list, ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Object Count', 'Container']) def test_encryption_type_list(self): encrypt_list = self.cinder('encryption-type-list') self.assertTableHeaders(encrypt_list, ['Volume Type ID', 'Provider', 'Cipher', 'Key Size', 'Control Location']) def test_extra_specs_list(self): extra_specs_list = self.cinder('extra-specs-list') self.assertTableHeaders(extra_specs_list, ['ID', 'Name', 'extra_specs']) def test_list(self): list = self.cinder('list') self.assertTableHeaders(list, ['ID', 'Status', 'Name', 'Size', 'Volume Type', 'Bootable', 'Attached to']) def test_qos_list(self): qos_list = self.cinder('qos-list') self.assertTableHeaders(qos_list, ['ID', 'Name', 'Consumer', 'specs']) def test_rate_limits(self): rate_limits = self.cinder('rate-limits') self.assertTableHeaders(rate_limits, ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available']) def test_service_list(self): service_list = self.cinder('service-list') self.assertTableHeaders(service_list, ['Binary', 'Host', 'Zone', 'Status', 'State', 'Updated_at']) def test_snapshot_list(self): snapshot_list = self.cinder('snapshot-list') self.assertTableHeaders(snapshot_list, ['ID', 'Volume ID', 'Status', 'Name', 'Size']) def test_transfer_list(self): transfer_list = self.cinder('transfer-list') self.assertTableHeaders(transfer_list, ['ID', 'Volume ID', 'Name']) def test_type_list(self): type_list = self.cinder('type-list') self.assertTableHeaders(type_list, ['ID', 'Name']) def test_list_extensions(self): list_extensions = self.cinder('list-extensions') self.assertTableHeaders(list_extensions, ['Name', 'Summary', 'Alias', 'Updated']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/functional/test_snapshot_create_cli.py0000664000175000017500000000405300000000000031122 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient.tests.functional import base class CinderSnapshotTests(base.ClientTestBase): """Check of cinder snapshot commands.""" def setUp(self): super(CinderSnapshotTests, self).setUp() self.volume = self.object_create('volume', params='1') def test_snapshot_create_description(self): """Test steps: 1) create volume in Setup() 2) create snapshot with description 3) check that snapshot has right description """ description = 'test_description' snapshot = self.object_create('snapshot', params='--description {0} {1}'. format(description, self.volume['id'])) self.assertEqual(description, snapshot['description']) self.object_delete('snapshot', snapshot['id']) self.check_object_deleted('snapshot', snapshot['id']) def test_snapshot_create_metadata(self): """Test steps: 1) create volume in Setup() 2) create snapshot with metadata 3) check that metadata complies entered """ snapshot = self.object_create( 'snapshot', params='--metadata test_metadata=test_date {0}'.format( self.volume['id'])) self.assertEqual(str({'test_metadata': 'test_date'}), snapshot['metadata']) self.object_delete('snapshot', snapshot['id']) self.check_object_deleted('snapshot', snapshot['id']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/functional/test_volume_create_cli.py0000664000175000017500000000751100000000000030574 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ddt from tempest.lib import exceptions from cinderclient.tests.functional import base @ddt.ddt class CinderVolumeNegativeTests(base.ClientTestBase): """Check of cinder volume create commands.""" @ddt.data( ('', (r'Size is a required parameter')), ('-1', (r'Invalid input for field/attribute size')), ('0', (r"Invalid input for field/attribute size")), ('size', (r'invalid int value')), ('0.2', (r'invalid int value')), ('2 GB', (r'unrecognized arguments')), ('999999999', (r'VolumeSizeExceedsAvailableQuota')), ) @ddt.unpack def test_volume_create_with_incorrect_size(self, value, ex_text): self.assertRaisesRegex(exceptions.CommandFailed, ex_text, self.object_create, 'volume', params=value) class CinderVolumeTests(base.ClientTestBase): """Check of cinder volume create commands.""" def setUp(self): super(CinderVolumeTests, self).setUp() self.volume = self.object_create('volume', params='1') def test_volume_create_from_snapshot(self): """Test steps: 1) create volume in Setup() 2) create snapshot 3) create volume from snapshot 4) check that volume from snapshot has been successfully created """ snapshot = self.object_create('snapshot', params=self.volume['id']) volume_from_snapshot = self.object_create('volume', params='--snapshot-id {0} 1'. format(snapshot['id'])) self.object_delete('snapshot', snapshot['id']) self.check_object_deleted('snapshot', snapshot['id']) cinder_list = self.cinder('list') self.assertIn(volume_from_snapshot['id'], cinder_list) def test_volume_create_from_volume(self): """Test steps: 1) create volume in Setup() 2) create volume from volume 3) check that volume from volume has been successfully created """ volume_from_volume = self.object_create('volume', params='--source-volid {0} 1'. format(self.volume['id'])) cinder_list = self.cinder('list') self.assertIn(volume_from_volume['id'], cinder_list) class CinderVolumeTestsWithParameters(base.ClientTestBase): """Check of cinder volume create commands with parameters.""" def test_volume_create_description(self): """Test steps: 1) create volume with description 2) check that volume has right description """ volume_description = 'test_description' volume = self.object_create('volume', params='--description {0} 1'. format(volume_description)) self.assertEqual(volume_description, volume['description']) def test_volume_create_metadata(self): """Test steps: 1) create volume with metadata 2) check that metadata complies entered """ volume = self.object_create( 'volume', params='--metadata test_metadata=test_date 1') self.assertEqual(str({'test_metadata': 'test_date'}), volume['metadata']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/functional/test_volume_extend_cli.py0000664000175000017500000000427400000000000030623 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ddt from tempest.lib import exceptions from cinderclient.tests.functional import base @ddt.ddt class CinderVolumeExtendNegativeTests(base.ClientTestBase): """Check of cinder volume extend command.""" def setUp(self): super(CinderVolumeExtendNegativeTests, self).setUp() self.volume = self.object_create('volume', params='1') @ddt.data( ('', (r'too few arguments|the following arguments are required')), ('-1', (r'Invalid input for field/attribute new_size. Value: -1. ' r'-1 is less than the minimum of 1')), ('0', (r'Invalid input for field/attribute new_size. Value: 0. ' r'0 is less than the minimum of 1')), ('size', (r'invalid int value')), ('0.2', (r'invalid int value')), ('2 GB', (r'unrecognized arguments')), ('999999999', (r'VolumeSizeExceedsAvailableQuota')), ) @ddt.unpack def test_volume_extend_with_incorrect_size(self, value, ex_text): self.assertRaisesRegex( exceptions.CommandFailed, ex_text, self.cinder, 'extend', params='{0} {1}'.format(self.volume['id'], value)) @ddt.data( ('', (r'too few arguments|the following arguments are required')), ('1234-1234-1234', (r'No volume with a name or ID of')), ('my_volume', (r'No volume with a name or ID of')), ('1234 1234', (r'unrecognized arguments')) ) @ddt.unpack def test_volume_extend_with_incorrect_volume_id(self, value, ex_text): self.assertRaisesRegex( exceptions.CommandFailed, ex_text, self.cinder, 'extend', params='{0} 2'.format(value)) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.2698958 python-cinderclient-8.3.0/cinderclient/tests/unit/0000775000175000017500000000000000000000000022313 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/__init__.py0000664000175000017500000000000000000000000024412 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/fake_actions_module.py0000664000175000017500000000302000000000000026653 0ustar00zuulzuul00000000000000# 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. from cinderclient import api_versions from cinderclient import utils @api_versions.wraps("3.0", "3.1") def do_fake_action(): """help message This will not show up in help message """ return "fake_action 3.0 to 3.1" @api_versions.wraps("3.2", "3.3") def do_fake_action(): # noqa: F811 return "fake_action 3.2 to 3.3" @api_versions.wraps("3.6") @utils.arg( '--foo', start_version='3.7') def do_another_fake_action(): return "another_fake_action" @utils.arg( '--foo', start_version='3.1', end_version='3.2') @utils.arg( '--bar', help='bar help', start_version='3.3', end_version='3.4') def do_fake_action2(): return "fake_action2" @utils.arg( '--foo', help='first foo', start_version='3.6', end_version='3.7') @utils.arg( '--foo', help='second foo', start_version='3.8') def do_fake_action3(): return "fake_action3" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/fakes.py0000664000175000017500000001044000000000000023755 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """ A fake server that "responds" to API methods with pre-canned responses. All of these responses come from the spec, so if for some reason the spec's wrong the tests might raise AssertionError. I've indicated in comments the places where actual behavior differs from the spec. """ def assert_has_keys(dict, required=None, optional=None): required = required or [] optional = optional or [] for k in required: try: assert k in dict except AssertionError: extra_keys = set(dict).difference(set(required + optional)) raise AssertionError("found unexpected keys: %s" % list(extra_keys)) class FakeClient(object): def _dict_match(self, partial, real): result = True try: for key, value in partial.items(): if isinstance(value, dict): result = self._dict_match(value, real[key]) else: assert real[key] == value result = True except (AssertionError, KeyError): result = False return result def assert_in_call(self, url_part): """Assert a call contained a part in its URL.""" assert self.client.callstack, "Expected call but no calls were made" called = self.client.callstack[-1][1] assert url_part in called, 'Expected %s in call but found %s' % ( url_part, called) def assert_called(self, method, url, body=None, partial_body=None, pos=-1, **kwargs): """Assert than an API method was just called.""" expected = (method, url) assert self.client.callstack, ("Expected %s %s but no calls " "were made." % expected) called = self.client.callstack[pos][0:2] assert expected == called, 'Expected %s %s; got %s %s' % ( expected + called) if body is not None: actual_body = self.client.callstack[pos][2] assert actual_body == body, ("body mismatch. expected:\n" + str(body) + "\n" + "actual:\n" + str(actual_body)) if partial_body is not None: try: assert self._dict_match(partial_body, self.client.callstack[pos][2]) except AssertionError: print(self.client.callstack[pos][2]) print("does not contain") print(partial_body) raise def assert_called_anytime(self, method, url, body=None, partial_body=None): """ Assert than an API method was called anytime in the test. """ expected = (method, url) assert self.client.callstack, ("Expected %s %s but no calls " "were made." % expected) found = False for entry in self.client.callstack: if expected == entry[0:2]: found = True break assert found, 'Expected %s %s; got %s' % ( expected + (self.client.callstack, )) if body is not None: try: assert entry[2] == body except AssertionError: print(entry[2]) print("!=") print(body) raise if partial_body is not None: try: assert self._dict_match(partial_body, entry[2]) except AssertionError: print(entry[2]) print("does not contain") print(partial_body) raise def clear_callstack(self): self.client.callstack = [] def authenticate(self): pass ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.2698958 python-cinderclient-8.3.0/cinderclient/tests/unit/fixture_data/0000775000175000017500000000000000000000000024772 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/fixture_data/__init__.py0000664000175000017500000000000000000000000027071 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/fixture_data/availability_zones.py0000664000175000017500000000566600000000000031251 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from datetime import datetime from cinderclient.tests.unit.fixture_data import base # FIXME(jamielennox): use timeutils from oslo FORMAT = '%Y-%m-%d %H:%M:%S' REQUEST_ID = 'req-test-request-id' class Fixture(base.Fixture): base_url = 'os-availability-zone' def setUp(self): super(Fixture, self).setUp() get_availability = { "availabilityZoneInfo": [ { "zoneName": "zone-1", "zoneState": {"available": True}, "hosts": None, }, { "zoneName": "zone-2", "zoneState": {"available": False}, "hosts": None, }, ] } self.requests.register_uri( 'GET', self.url(), json=get_availability, headers={'x-openstack-request-id': REQUEST_ID} ) updated_1 = datetime(2012, 12, 26, 14, 45, 25, 0).strftime(FORMAT) updated_2 = datetime(2012, 12, 26, 14, 45, 24, 0).strftime(FORMAT) get_detail = { "availabilityZoneInfo": [ { "zoneName": "zone-1", "zoneState": {"available": True}, "hosts": { "fake_host-1": { "cinder-volume": { "active": True, "available": True, "updated_at": updated_1, } } } }, { "zoneName": "internal", "zoneState": {"available": True}, "hosts": { "fake_host-1": { "cinder-sched": { "active": True, "available": True, "updated_at": updated_2, } } } }, { "zoneName": "zone-2", "zoneState": {"available": False}, "hosts": None, }, ] } self.requests.register_uri( 'GET', self.url('detail'), json=get_detail, headers={'x-openstack-request-id': REQUEST_ID} ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/fixture_data/base.py0000664000175000017500000000232600000000000026261 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import fixtures IDENTITY_URL = 'http://identityserver:5000/v2.0' VOLUME_URL = 'http://volume.host' class Fixture(fixtures.Fixture): base_url = None json_headers = {'Content-Type': 'application/json'} def __init__(self, requests, volume_url=VOLUME_URL, identity_url=IDENTITY_URL): super(Fixture, self).__init__() self.requests = requests self.volume_url = volume_url self.identity_url = identity_url def url(self, *args): url_args = [self.volume_url] if self.base_url: url_args.append(self.base_url) return '/'.join(str(a).strip('/') for a in tuple(url_args) + args) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/fixture_data/client.py0000664000175000017500000000305100000000000026621 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from keystoneauth1 import fixture from cinderclient.tests.unit.fixture_data import base from cinderclient.v3 import client as v3client class Base(base.Fixture): def __init__(self, *args, **kwargs): super(Base, self).__init__(*args, **kwargs) self.token = fixture.V2Token() self.token.set_scope() def setUp(self): super(Base, self).setUp() auth_url = '%s/tokens' % self.identity_url self.requests.register_uri('POST', auth_url, json=self.token, headers=self.json_headers) class V3(Base): def __init__(self, *args, **kwargs): super(V3, self).__init__(*args, **kwargs) svc = self.token.add_service('volumev3') svc.add_endpoint(self.volume_url) def new_client(self): return v3client.Client(username='xx', api_key='xx', project_id='xx', auth_url=self.identity_url) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/fixture_data/keystone_client.py0000664000175000017500000002411300000000000030544 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import json from oslo_utils import uuidutils # these are copied from python-keystoneclient tests BASE_HOST = 'http://keystone.example.com' BASE_URL = "%s:5000/" % BASE_HOST UPDATED = '2013-03-06T00:00:00Z' V2_URL = "%sv2.0" % BASE_URL V2_DESCRIBED_BY_HTML = {'href': 'http://docs.openstack.org/api/' 'openstack-identity-service/2.0/content/', 'rel': 'describedby', 'type': 'text/html'} V2_DESCRIBED_BY_PDF = {'href': 'http://docs.openstack.org/api/openstack-ident' 'ity-service/2.0/identity-dev-guide-2.0.pdf', 'rel': 'describedby', 'type': 'application/pdf'} V2_VERSION = {'id': 'v2.0', 'links': [{'href': V2_URL, 'rel': 'self'}, V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF], 'status': 'stable', 'updated': UPDATED} V3_URL = "%sv3" % BASE_URL V3_MEDIA_TYPES = [{'base': 'application/json', 'type': 'application/vnd.openstack.identity-v3+json'}, {'base': 'application/xml', 'type': 'application/vnd.openstack.identity-v3+xml'}] V3_VERSION = {'id': 'v3.0', 'links': [{'href': V3_URL, 'rel': 'self'}], 'media-types': V3_MEDIA_TYPES, 'status': 'stable', 'updated': UPDATED} WRONG_VERSION_RESPONSE = {'id': 'v2.0', 'links': [V2_DESCRIBED_BY_HTML, V2_DESCRIBED_BY_PDF], 'status': 'stable', 'updated': UPDATED} def _create_version_list(versions): return json.dumps({'versions': {'values': versions}}) def _create_single_version(version): return json.dumps({'version': version}) V3_VERSION_LIST = _create_version_list([V3_VERSION, V2_VERSION]) V2_VERSION_LIST = _create_version_list([V2_VERSION]) V3_VERSION_ENTRY = _create_single_version(V3_VERSION) V2_VERSION_ENTRY = _create_single_version(V2_VERSION) CINDER_ENDPOINT = 'http://www.cinder.com/v1' def _get_normalized_token_data(**kwargs): ref = copy.deepcopy(kwargs) # normalized token data ref['user_id'] = ref.get('user_id', uuidutils.generate_uuid(dashed=False)) ref['username'] = ref.get('username', uuidutils.generate_uuid(dashed=False)) ref['project_id'] = ref.get('project_id', ref.get('tenant_id', uuidutils.generate_uuid( dashed=False))) ref['project_name'] = ref.get('project_name', ref.get('tenant_name', uuidutils.generate_uuid( dashed=False))) ref['user_domain_id'] = ref.get('user_domain_id', uuidutils.generate_uuid(dashed=False)) ref['user_domain_name'] = ref.get('user_domain_name', uuidutils.generate_uuid(dashed=False)) ref['project_domain_id'] = ref.get('project_domain_id', uuidutils.generate_uuid(dashed=False)) ref['project_domain_name'] = ref.get('project_domain_name', uuidutils.generate_uuid(dashed=False)) ref['roles'] = ref.get('roles', [{'name': uuidutils.generate_uuid(dashed=False), 'id': uuidutils.generate_uuid(dashed=False)}]) ref['roles_link'] = ref.get('roles_link', []) ref['cinder_url'] = ref.get('cinder_url', CINDER_ENDPOINT) return ref def generate_v2_project_scoped_token(**kwargs): """Generate a Keystone V2 token based on auth request.""" ref = _get_normalized_token_data(**kwargs) token = uuidutils.generate_uuid(dashed=False) o = {'access': {'token': {'id': token, 'expires': '2099-05-22T00:02:43.941430Z', 'issued_at': '2013-05-21T00:02:43.941473Z', 'tenant': {'enabled': True, 'id': ref.get('project_id'), 'name': ref.get('project_id') } }, 'user': {'id': ref.get('user_id'), 'name': uuidutils.generate_uuid(dashed=False), 'username': ref.get('username'), 'roles': ref.get('roles'), 'roles_links': ref.get('roles_links') } }} # Add endpoint Keystone o['access']['serviceCatalog'] = [ { 'endpoints': [ { 'publicURL': ref.get('auth_url'), 'adminURL': ref.get('auth_url'), 'internalURL': ref.get('auth_url'), 'id': uuidutils.generate_uuid(dashed=False), 'region': 'RegionOne' }], 'endpoint_links': [], 'name': 'keystone', 'type': 'identity' } ] cinder_endpoint = { 'endpoints': [ { 'publicURL': 'public_' + ref.get('cinder_url'), 'internalURL': 'internal_' + ref.get('cinder_url'), 'adminURL': 'admin_' + (ref.get('auth_url') or ""), 'id': uuidutils.generate_uuid(dashed=False), 'region': 'RegionOne' } ], 'endpoints_links': [], 'name': None, 'type': 'volumev3' } # Add multiple Cinder endpoints for count in range(1, 4): # Copy the endpoint and create a service name endpoint_copy = copy.deepcopy(cinder_endpoint) name = "cinder%i" % count # Assign the service name and a unique endpoint endpoint_copy['endpoints'][0]['publicURL'] = \ 'http://%s.api.com/v3' % name endpoint_copy['name'] = name o['access']['serviceCatalog'].append(endpoint_copy) return token, o def generate_v3_project_scoped_token(**kwargs): """Generate a Keystone V3 token based on auth request.""" ref = _get_normalized_token_data(**kwargs) o = {'token': {'expires_at': '2099-05-22T00:02:43.941430Z', 'issued_at': '2013-05-21T00:02:43.941473Z', 'methods': ['password'], 'project': {'id': ref.get('project_id'), 'name': ref.get('project_name'), 'domain': {'id': ref.get('project_domain_id'), 'name': ref.get( 'project_domain_name') } }, 'user': {'id': ref.get('user_id'), 'name': ref.get('username'), 'domain': {'id': ref.get('user_domain_id'), 'name': ref.get('user_domain_name') } }, 'roles': ref.get('roles') }} # we only care about Neutron and Keystone endpoints o['token']['catalog'] = [ {'endpoints': [ { 'id': uuidutils.generate_uuid(dashed=False), 'interface': 'public', 'region': 'RegionOne', 'url': 'public_' + ref.get('cinder_url') }, { 'id': uuidutils.generate_uuid(dashed=False), 'interface': 'internal', 'region': 'RegionOne', 'url': 'internal_' + ref.get('cinder_url') }, { 'id': uuidutils.generate_uuid(dashed=False), 'interface': 'admin', 'region': 'RegionOne', 'url': 'admin_' + ref.get('cinder_url') }], 'id': uuidutils.generate_uuid(dashed=False), 'type': 'network'}, {'endpoints': [ { 'id': uuidutils.generate_uuid(dashed=False), 'interface': 'public', 'region': 'RegionOne', 'url': ref.get('auth_url') }, { 'id': uuidutils.generate_uuid(dashed=False), 'interface': 'admin', 'region': 'RegionOne', 'url': ref.get('auth_url') }], 'id': uuidutils.generate_uuid(dashed=False), 'type': 'identity'}] # token ID is conveyed via the X-Subject-Token header so we are generating # one to stash there token_id = uuidutils.generate_uuid(dashed=False) return token_id, o def keystone_request_callback(request, context): context.headers['Content-Type'] = 'application/json' if request.url == BASE_URL: return V3_VERSION_LIST elif request.url == BASE_URL + "/v2.0": token_id, token_data = generate_v2_project_scoped_token() return token_data elif request.url.startswith("http://multiple.service.names"): token_id, token_data = generate_v2_project_scoped_token() return json.dumps(token_data) elif request.url == BASE_URL + "/v3": token_id, token_data = generate_v3_project_scoped_token() context.headers["X-Subject-Token"] = token_id context.status_code = 201 return token_data elif "wrongdiscoveryresponse.discovery.com" in request.url: return str(WRONG_VERSION_RESPONSE) else: context.status_code = 500 return str(WRONG_VERSION_RESPONSE) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/fixture_data/snapshots.py0000664000175000017500000000403000000000000027363 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient.tests.unit.fixture_data import base REQUEST_ID = 'req-test-request-id' def _stub_snapshot(**kwargs): snapshot = { "created_at": "2012-08-28T16:30:31.000000", "display_description": None, "display_name": None, "id": '11111111-1111-1111-1111-111111111111', "size": 1, "status": "available", "volume_id": '00000000-0000-0000-0000-000000000000', } snapshot.update(kwargs) return snapshot class Fixture(base.Fixture): base_url = 'snapshots' def setUp(self): super(Fixture, self).setUp() snapshot_1234 = _stub_snapshot(id='1234') self.requests.register_uri( 'GET', self.url('1234'), json={'snapshot': snapshot_1234}, headers={'x-openstack-request-id': REQUEST_ID} ) def action_1234(request, context): return '' self.requests.register_uri( 'POST', self.url('1234', 'action'), text=action_1234, status_code=202, headers={'x-openstack-request-id': REQUEST_ID} ) self.requests.register_uri( 'GET', self.url('detail?limit=2&marker=1234'), status_code=200, json={'snapshots': []}, headers={'x-openstack-request-id': REQUEST_ID} ) self.requests.register_uri( 'GET', self.url('detail?sort=id'), status_code=200, json={'snapshots': []}, headers={'x-openstack-request-id': REQUEST_ID} ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/test_api_versions.py0000664000175000017500000002625700000000000026441 0ustar00zuulzuul00000000000000# Copyright 2016 Mirantis # 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 ddt from cinderclient import api_versions from cinderclient import exceptions from cinderclient.tests.unit import test_utils from cinderclient.tests.unit import utils from cinderclient.v3 import client @ddt.ddt class APIVersionTestCase(utils.TestCase): def test_valid_version_strings(self): def _test_string(version, exp_major, exp_minor): v = api_versions.APIVersion(version) self.assertEqual(v.ver_major, exp_major) self.assertEqual(v.ver_minor, exp_minor) _test_string("1.1", 1, 1) _test_string("2.10", 2, 10) _test_string("5.234", 5, 234) _test_string("12.5", 12, 5) _test_string("2.0", 2, 0) _test_string("2.200", 2, 200) def test_null_version(self): v = api_versions.APIVersion() self.assertFalse(v) def test_not_null_version(self): v = api_versions.APIVersion('1.1') self.assertTrue(v) @ddt.data("2", "200", "2.1.4", "200.23.66.3", "5 .3", "5. 3", "5.03", "02.1", "2.001", "", " 2.1", "2.1 ") def test_invalid_version_strings(self, version_string): self.assertRaises(exceptions.UnsupportedVersion, api_versions.APIVersion, version_string) def test_version_comparisons(self): v1 = api_versions.APIVersion("2.0") v2 = api_versions.APIVersion("2.5") v3 = api_versions.APIVersion("5.23") v4 = api_versions.APIVersion("2.0") v_null = api_versions.APIVersion() self.assertLess(v1, v2) self.assertGreater(v3, v2) self.assertNotEqual(v1, v2) self.assertEqual(v1, v4) self.assertNotEqual(v1, v_null) self.assertEqual(v_null, v_null) self.assertRaises(TypeError, v1.__le__, "2.1") def test_version_matches(self): v1 = api_versions.APIVersion("2.0") v2 = api_versions.APIVersion("2.5") v3 = api_versions.APIVersion("2.45") v4 = api_versions.APIVersion("3.3") v5 = api_versions.APIVersion("3.23") v6 = api_versions.APIVersion("2.0") v7 = api_versions.APIVersion("3.3") v8 = api_versions.APIVersion("4.0") v_null = api_versions.APIVersion() self.assertTrue(v2.matches(v1, v3)) self.assertTrue(v2.matches(v1, v_null)) self.assertTrue(v1.matches(v6, v2)) self.assertTrue(v4.matches(v2, v7)) self.assertTrue(v4.matches(v_null, v7)) self.assertTrue(v4.matches(v_null, v8)) self.assertFalse(v1.matches(v2, v3)) self.assertFalse(v5.matches(v2, v4)) self.assertFalse(v2.matches(v3, v1)) self.assertRaises(ValueError, v_null.matches, v1, v3) def test_get_string(self): v1_string = "3.23" v1 = api_versions.APIVersion(v1_string) self.assertEqual(v1_string, v1.get_string()) self.assertRaises(ValueError, api_versions.APIVersion().get_string) class ManagerTest(utils.TestCase): def test_api_version(self): # The function manager.return_api_version has two versions, # when called with api version 3.1 it should return the # string '3.1' and when called with api version 3.2 or higher # it should return the string '3.2'. version = api_versions.APIVersion('3.1') api = client.Client(api_version=version) manager = test_utils.FakeManagerWithApi(api) self.assertEqual('3.1', manager.return_api_version()) version = api_versions.APIVersion('3.2') api = client.Client(api_version=version) manager = test_utils.FakeManagerWithApi(api) self.assertEqual('3.2', manager.return_api_version()) # pick up the highest version version = api_versions.APIVersion('3.3') api = client.Client(api_version=version) manager = test_utils.FakeManagerWithApi(api) self.assertEqual('3.2', manager.return_api_version()) version = api_versions.APIVersion('3.0') api = client.Client(api_version=version) manager = test_utils.FakeManagerWithApi(api) # An exception will be returned here because the function # return_api_version doesn't support version 3.0 self.assertRaises(exceptions.VersionNotFoundForAPIMethod, manager.return_api_version) class UpdateHeadersTestCase(utils.TestCase): def test_api_version_is_null(self): headers = {} api_versions.update_headers(headers, api_versions.APIVersion()) self.assertEqual({}, headers) def test_api_version_is_major(self): headers = {} api_versions.update_headers(headers, api_versions.APIVersion("7.0")) self.assertEqual({}, headers) def test_api_version_is_not_null(self): api_version = api_versions.APIVersion("2.3") headers = {} api_versions.update_headers(headers, api_version) self.assertEqual( {"OpenStack-API-Version": "volume " + api_version.get_string()}, headers) class GetAPIVersionTestCase(utils.TestCase): def test_get_available_client_versions(self): output = api_versions.get_available_major_versions() self.assertNotEqual([], output) def test_wrong_format(self): self.assertRaises(exceptions.UnsupportedVersion, api_versions.get_api_version, "something_wrong") def test_wrong_major_version(self): self.assertRaises(exceptions.UnsupportedVersion, api_versions.get_api_version, "4") @mock.patch("cinderclient.api_versions.get_available_major_versions") @mock.patch("cinderclient.api_versions.APIVersion") def test_only_major_part_is_presented(self, mock_apiversion, mock_get_majors): mock_get_majors.return_value = [ str(mock_apiversion.return_value.ver_major)] version = 7 self.assertEqual(mock_apiversion.return_value, api_versions.get_api_version(version)) mock_apiversion.assert_called_once_with("%s.0" % str(version)) @mock.patch("cinderclient.api_versions.get_available_major_versions") @mock.patch("cinderclient.api_versions.APIVersion") def test_major_and_minor_parts_is_presented(self, mock_apiversion, mock_get_majors): version = "2.7" mock_get_majors.return_value = [ str(mock_apiversion.return_value.ver_major)] self.assertEqual(mock_apiversion.return_value, api_versions.get_api_version(version)) mock_apiversion.assert_called_once_with(version) @ddt.ddt class DiscoverVersionTestCase(utils.TestCase): def setUp(self): super(DiscoverVersionTestCase, self).setUp() self.orig_max = api_versions.MAX_VERSION self.orig_min = api_versions.MIN_VERSION or None self.addCleanup(self._clear_fake_version) self.fake_client = mock.MagicMock() def _clear_fake_version(self): api_versions.MAX_VERSION = self.orig_max api_versions.MIN_VERSION = self.orig_min def _mock_returned_server_version(self, server_version, server_min_version): version_mock = mock.MagicMock(version=server_version, min_version=server_min_version, status='CURRENT') val = [version_mock] if not server_version and not server_min_version: val = [] self.fake_client.services.server_api_version.return_value = val @ddt.data( # what the data mean: # items 1, 2: client min, max # items 3, 4: server min, max # item 5: user's requested API version # item 6: should this raise an exception? # item 7: version that should be returned when no exception # item 8: what client.services.server_api_version should return # when called by _get_server_version_range in discover_version ("3.1", "3.3", "3.4", "3.7", "3.3", True), # Server too new ("3.9", "3.10", "3.0", "3.3", "3.10", True), # Server too old ("3.3", "3.9", "3.7", "3.17", "3.9", False), # Requested < server # downgraded because of server: ("3.5", "3.8", "3.0", "3.7", "3.8", False, "3.7"), # downgraded because of client: ("3.5", "3.8", "3.0", "3.9", "3.9", False, "3.8"), # downgraded because of both: ("3.5", "3.7", "3.0", "3.8", "3.9", False, "3.7"), ("3.5", "3.5", "3.0", "3.5", "3.5", False), # Server & client same ("3.5", "3.5", None, None, "3.5", True, None, []), # Pre-micro ("3.1", "3.11", "3.4", "3.7", "3.7", False), # Requested in range ("3.5", "3.5", "3.0", "3.5", "1.0", True) # Requested too old ) @ddt.unpack def test_microversion(self, client_min, client_max, server_min, server_max, requested_version, exp_range, end_version=None, ret_val=None): if ret_val is not None: self.fake_client.services.server_api_version.return_value = ret_val else: self._mock_returned_server_version(server_max, server_min) api_versions.MAX_VERSION = client_max api_versions.MIN_VERSION = client_min if exp_range: exc = self.assertRaises(exceptions.UnsupportedVersion, api_versions.discover_version, self.fake_client, api_versions.APIVersion(requested_version)) if ret_val is not None: self.assertIn("Server does not support microversions", str(exc)) else: self.assertIn("range is '%s' to '%s'" % (server_min, server_max), str(exc)) else: discovered_version = api_versions.discover_version( self.fake_client, api_versions.APIVersion(requested_version)) version = requested_version if end_version is not None: version = end_version self.assertEqual(version, discovered_version.get_string()) self.assertTrue( self.fake_client.services.server_api_version.called) def test_get_highest_version(self): self._mock_returned_server_version("3.14", "3.0") highest_version = api_versions.get_highest_version(self.fake_client) self.assertEqual("3.14", highest_version.get_string()) self.assertTrue(self.fake_client.services.server_api_version.called) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/test_auth_plugins.py0000664000175000017500000000320300000000000026424 0ustar00zuulzuul00000000000000# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient.contrib import noauth from cinderclient.tests.unit import utils class CinderNoAuthPluginTest(utils.TestCase): def setUp(self): super(CinderNoAuthPluginTest, self).setUp() self.plugin = noauth.CinderNoAuthPlugin('user', 'project', endpoint='example.com') def test_auth_token(self): auth_token = 'user:project' self.assertEqual(auth_token, self.plugin.auth_token) def test_auth_token_no_project(self): auth_token = 'user:user' plugin = noauth.CinderNoAuthPlugin('user') self.assertEqual(auth_token, plugin.auth_token) def test_get_headers(self): headers = {'x-user-id': 'user', 'x-project-id': 'project', 'X-Auth-Token': 'user:project'} self.assertEqual(headers, self.plugin.get_headers(None)) def test_get_endpoint(self): endpoint = 'example.com/project' self.assertEqual(endpoint, self.plugin.get_endpoint(None)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/test_base.py0000664000175000017500000001455100000000000024644 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock import requests from cinderclient import api_versions from cinderclient.apiclient import base as common_base from cinderclient import base from cinderclient import exceptions from cinderclient.tests.unit import test_utils from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3 import client from cinderclient.v3 import volumes cs = fakes.FakeClient() REQUEST_ID = test_utils.REQUEST_ID def create_response_obj_with_header(): resp = requests.Response() resp.headers['x-openstack-request-id'] = REQUEST_ID resp.headers['Etag'] = 'd5103bf7b26ff0310200d110da3ed186' resp.status_code = 200 return resp class BaseTest(utils.TestCase): def test_resource_repr(self): r = base.Resource(None, dict(foo="bar", baz="spam")) self.assertEqual("", repr(r)) self.assertNotIn("x_openstack_request_ids", repr(r)) def test_add_non_ascii_attr_to_resource(self): info = {'gigabytes_тест': -1, 'volumes_тест': -1, 'id': 'admin'} res = base.Resource(None, info) for key, value in info.items(): self.assertEqual(value, getattr(res, key, None)) def test_getid(self): self.assertEqual(4, base.getid(4)) class TmpObject(object): id = 4 self.assertEqual(4, base.getid(TmpObject)) def test_eq(self): # Two resources with same ID: never equal if their info is not equal r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) self.assertNotEqual(r1, r2) # Two resources with same ID: equal if their info is equal r1 = base.Resource(None, {'id': 1, 'name': 'hello'}) r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) self.assertEqual(r1, r2) # Two resources of different types: never equal r1 = base.Resource(None, {'id': 1}) r2 = volumes.Volume(None, {'id': 1}) self.assertNotEqual(r1, r2) # Two resources with no ID: equal if their info is equal r1 = base.Resource(None, {'name': 'joe', 'age': 12}) r2 = base.Resource(None, {'name': 'joe', 'age': 12}) self.assertEqual(r1, r2) def test_findall_invalid_attribute(self): # Make sure findall with an invalid attribute doesn't cause errors. # The following should not raise an exception. cs.volumes.findall(vegetable='carrot') # However, find() should raise an error self.assertRaises(exceptions.NotFound, cs.volumes.find, vegetable='carrot') def test_to_dict(self): r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) self.assertEqual({'id': 1, 'name': 'hi'}, r1.to_dict()) def test_resource_object_with_request_ids(self): resp_obj = create_response_obj_with_header() r = base.Resource(None, {"name": "1"}, resp=resp_obj) self.assertEqual([REQUEST_ID], r.request_ids) def test_api_version(self): version = api_versions.APIVersion('3.1') api = client.Client(api_version=version) manager = test_utils.FakeManagerWithApi(api) r1 = base.Resource(manager, {'id': 1}) self.assertEqual(version, r1.api_version) def test__list_no_link(self): api = mock.Mock() api.client.get.return_value = (mock.sentinel.resp, {'resp_keys': [{'name': '1'}]}) manager = test_utils.FakeManager(api) res = manager._list(mock.sentinel.url, 'resp_keys') api.client.get.assert_called_once_with(mock.sentinel.url) result = [r.name for r in res] self.assertListEqual(['1'], result) def test__list_with_link(self): api = mock.Mock() api.client.get.side_effect = [ (mock.sentinel.resp, {'resp_keys': [{'name': '1'}], 'resp_keys_links': [{'rel': 'next', 'href': mock.sentinel.u2}]}), (mock.sentinel.resp, {'resp_keys': [{'name': '2'}], 'resp_keys_links': [{'rel': 'next', 'href': mock.sentinel.u3}]}), (mock.sentinel.resp, {'resp_keys': [{'name': '3'}], 'resp_keys_links': [{'rel': 'next', 'href': None}]}), ] manager = test_utils.FakeManager(api) res = manager._list(mock.sentinel.url, 'resp_keys') api.client.get.assert_has_calls([mock.call(mock.sentinel.url), mock.call(mock.sentinel.u2), mock.call(mock.sentinel.u3)]) result = [r.name for r in res] self.assertListEqual(['1', '2', '3'], result) class ListWithMetaTest(utils.TestCase): def test_list_with_meta(self): resp = create_response_obj_with_header() obj = common_base.ListWithMeta([], resp) self.assertEqual([], obj) # Check request_ids attribute is added to obj self.assertTrue(hasattr(obj, 'request_ids')) self.assertEqual([REQUEST_ID], obj.request_ids) class DictWithMetaTest(utils.TestCase): def test_dict_with_meta(self): resp = create_response_obj_with_header() obj = common_base.DictWithMeta([], resp) self.assertEqual({}, obj) # Check request_ids attribute is added to obj self.assertTrue(hasattr(obj, 'request_ids')) self.assertEqual([REQUEST_ID], obj.request_ids) class TupleWithMetaTest(utils.TestCase): def test_tuple_with_meta(self): resp = create_response_obj_with_header() obj = common_base.TupleWithMeta((), resp) self.assertEqual((), obj) # Check request_ids attribute is added to obj self.assertTrue(hasattr(obj, 'request_ids')) self.assertEqual([REQUEST_ID], obj.request_ids) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/test_client.py0000664000175000017500000004076700000000000025220 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import json import logging from unittest import mock import ddt import fixtures from keystoneauth1 import adapter from keystoneauth1 import exceptions as keystone_exception from oslo_serialization import jsonutils from cinderclient import api_versions import cinderclient.client from cinderclient import exceptions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes @ddt.ddt class ClientTest(utils.TestCase): def test_get_client_class_v2(self): self.assertRaises(cinderclient.exceptions.UnsupportedVersion, cinderclient.client.get_client_class, '2') def test_get_client_class_unknown(self): self.assertRaises(cinderclient.exceptions.UnsupportedVersion, cinderclient.client.get_client_class, '0') @mock.patch.object(cinderclient.client.HTTPClient, '__init__') @mock.patch('cinderclient.client.SessionClient') def test_construct_http_client_endpoint_url( self, session_mock, httpclient_mock): os_endpoint = 'http://example.com/' httpclient_mock.return_value = None cinderclient.client._construct_http_client( os_endpoint=os_endpoint) self.assertTrue(httpclient_mock.called) self.assertEqual(os_endpoint, httpclient_mock.call_args[1].get('os_endpoint')) session_mock.assert_not_called() def test_log_req(self): self.logger = self.useFixture( fixtures.FakeLogger( format="%(message)s", level=logging.DEBUG, nuke_handlers=True ) ) kwargs = { 'headers': {"X-Foo": "bar"}, 'data': ('{"auth": {"tenantName": "fakeService",' ' "passwordCredentials": {"username": "fakeUser",' ' "password": "fakePassword"}}}') } cs = cinderclient.client.HTTPClient("user", None, None, "http://127.0.0.1:5000") cs.http_log_debug = True cs.http_log_req('PUT', kwargs) output = self.logger.output.split('\n') self.assertNotIn("fakePassword", output[1]) self.assertIn("fakeUser", output[1]) def test_versions(self): v2_url = 'http://fakeurl/v2/tenants' v3_url = 'http://fakeurl/v3/tenants' unknown_url = 'http://fakeurl/v9/tenants' self.assertRaises(cinderclient.exceptions.UnsupportedVersion, cinderclient.client.get_volume_api_from_url, v2_url) self.assertEqual('3', cinderclient.client.get_volume_api_from_url(v3_url)) self.assertRaises(cinderclient.exceptions.UnsupportedVersion, cinderclient.client.get_volume_api_from_url, unknown_url) @mock.patch('cinderclient.client.SessionClient.get_endpoint') @ddt.data( ('http://192.168.1.1:8776/v2', 'http://192.168.1.1:8776/'), ('http://192.168.1.1:8776/v3/e5526285ebd741b1819393f772f11fc3', 'http://192.168.1.1:8776/'), ('https://192.168.1.1:8080/volumes/v3/' 'e5526285ebd741b1819393f772f11fc3', 'https://192.168.1.1:8080/volumes/'), ('http://192.168.1.1/volumes/v3/e5526285ebd741b1819393f772f11fc3', 'http://192.168.1.1/volumes/'), ('https://volume.example.com/', 'https://volume.example.com/')) @ddt.unpack def test_get_base_url(self, url, expected_base, mock_get_endpoint): mock_get_endpoint.return_value = url cs = cinderclient.client.SessionClient(self, api_version='3.0') self.assertEqual(expected_base, cs._get_base_url()) @mock.patch.object(adapter.Adapter, 'request') @mock.patch.object(exceptions, 'from_response') def test_sessionclient_request_method( self, mock_from_resp, mock_request): kwargs = { "body": { "volume": { "status": "creating", "imageRef": "username", "attach_status": "detached" }, "authenticated": "True" } } resp = { "text": { "volume": { "status": "creating", "id": "431253c0-e203-4da2-88df-60c756942aaf", "size": 1 } }, "code": 202 } request_id = "req-f551871a-4950-4225-9b2c-29a14c8f075e" mock_response = utils.TestResponse({ "status_code": 202, "text": json.dumps(resp).encode("latin-1"), "headers": {"x-openstack-request-id": request_id}, }) # 'request' method of Adaptor will return 202 response mock_request.return_value = mock_response session_client = cinderclient.client.SessionClient(session=mock.Mock()) response, body = session_client.request(mock.sentinel.url, 'POST', **kwargs) self.assertIsNotNone(session_client._logger) # In this case, from_response method will not get called # because response status_code is < 400 self.assertEqual(202, response.status_code) self.assertFalse(mock_from_resp.called) @mock.patch.object(adapter.Adapter, 'request') def test_sessionclient_request_method_raises_badrequest( self, mock_request): kwargs = { "body": { "volume": { "status": "creating", "imageRef": "username", "attach_status": "detached" }, "authenticated": "True" } } resp = { "badRequest": { "message": "Invalid image identifier or unable to access " "requested image.", "code": 400 } } mock_response = utils.TestResponse({ "status_code": 400, "text": json.dumps(resp).encode("latin-1"), }) # 'request' method of Adaptor will return 400 response mock_request.return_value = mock_response session_client = cinderclient.client.SessionClient( session=mock.Mock()) # 'from_response' method will raise BadRequest because # resp.status_code is 400 self.assertRaises(exceptions.BadRequest, session_client.request, mock.sentinel.url, 'POST', **kwargs) self.assertIsNotNone(session_client._logger) @mock.patch.object(adapter.Adapter, 'request') def test_sessionclient_request_method_raises_overlimit( self, mock_request): resp = { "overLimitFault": { "message": "This request was rate-limited.", "code": 413 } } mock_response = utils.TestResponse({ "status_code": 413, "text": json.dumps(resp).encode("latin-1"), }) # 'request' method of Adaptor will return 413 response mock_request.return_value = mock_response session_client = cinderclient.client.SessionClient( session=mock.Mock()) self.assertRaises(exceptions.OverLimit, session_client.request, mock.sentinel.url, 'GET') self.assertIsNotNone(session_client._logger) @mock.patch.object(exceptions, 'from_response') def test_keystone_request_raises_auth_failure_exception( self, mock_from_resp): kwargs = { "body": { "volume": { "status": "creating", "imageRef": "username", "attach_status": "detached" }, "authenticated": "True" } } with mock.patch.object(adapter.Adapter, 'request', side_effect= keystone_exception.AuthorizationFailure()): session_client = cinderclient.client.SessionClient( session=mock.Mock()) self.assertRaises(keystone_exception.AuthorizationFailure, session_client.request, mock.sentinel.url, 'POST', **kwargs) # As keystonesession.request method will raise # AuthorizationFailure exception, check exceptions.from_response # is not getting called. self.assertFalse(mock_from_resp.called) class ClientTestSensitiveInfo(utils.TestCase): def test_req_does_not_log_sensitive_info(self): self.logger = self.useFixture( fixtures.FakeLogger( format="%(message)s", level=logging.DEBUG, nuke_handlers=True ) ) secret_auth_token = "MY_SECRET_AUTH_TOKEN" kwargs = { 'headers': {"X-Auth-Token": secret_auth_token}, 'data': ('{"auth": {"tenantName": "fakeService",' ' "passwordCredentials": {"username": "fakeUser",' ' "password": "fakePassword"}}}') } cs = cinderclient.client.HTTPClient("user", None, None, "http://127.0.0.1:5000") cs.http_log_debug = True cs.http_log_req('PUT', kwargs) output = self.logger.output.split('\n') self.assertNotIn(secret_auth_token, output[1]) def test_resp_does_not_log_sensitive_info(self): self.logger = self.useFixture( fixtures.FakeLogger( format="%(message)s", level=logging.DEBUG, nuke_handlers=True ) ) cs = cinderclient.client.HTTPClient("user", None, None, "http://127.0.0.1:5000") resp = mock.Mock() resp.status_code = 200 resp.headers = { 'x-compute-request-id': 'req-f551871a-4950-4225-9b2c-29a14c8f075e' } auth_password = "kk4qD6CpKFLyz9JD" body = { "connection_info": { "driver_volume_type": "iscsi", "data": { "auth_password": auth_password, "target_discovered": False, "encrypted": False, "qos_specs": None, "target_iqn": ("iqn.2010-10.org.openstack:volume-" "a2f33dcc-1bb7-45ba-b8fc-5b38179120f8"), "target_portal": "10.0.100.186:3260", "volume_id": "a2f33dcc-1bb7-45ba-b8fc-5b38179120f8", "target_lun": 1, "access_mode": "rw", "auth_username": "s4BfSfZ67Bo2mnpuFWY8", "auth_method": "CHAP" } } } resp.text = jsonutils.dumps(body) cs.http_log_debug = True cs.http_log_resp(resp) output = self.logger.output.split('\n') self.assertIn('***', output[1], output) self.assertNotIn(auth_password, output[1], output) @ddt.ddt class GetAPIVersionTestCase(utils.TestCase): @mock.patch('cinderclient.client.requests.get') def test_get_server_version_v2(self, mock_request): # Why are we testing this? Because we can! mock_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(fakes.fake_request_get_no_v3()) }) mock_request.return_value = mock_response url = "http://192.168.122.127:8776/v2/e5526285ebd741b1819393f772f11fc3" min_version, max_version = cinderclient.client.get_server_version(url) self.assertEqual(api_versions.APIVersion('2.0'), min_version) self.assertEqual(api_versions.APIVersion('2.0'), max_version) @mock.patch('cinderclient.client.requests.get') @ddt.data( 'http://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3', 'https://192.168.122.127:8776/v3/e55285ebd741b1819393f772f11fc3', 'http://192.168.122.127/volumesv3/e5526285ebd741b1819393f772f11fc3' ) def test_get_server_version(self, url, mock_request): mock_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(fakes.fake_request_get()) }) mock_request.return_value = mock_response min_version, max_version = cinderclient.client.get_server_version(url) self.assertEqual(min_version, api_versions.APIVersion('3.0')) self.assertEqual(max_version, api_versions.APIVersion('3.16')) @mock.patch('cinderclient.client.requests.get') def test_get_server_version_insecure(self, mock_request): mock_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(fakes.fake_request_get_no_v3()) }) mock_request.return_value = mock_response url = ( "https://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3") expected_url = "https://192.168.122.127:8776/" cinderclient.client.get_server_version(url, True) mock_request.assert_called_once_with(expected_url, verify=False, cert=None) @mock.patch('cinderclient.client.requests.get') def test_get_server_version_cacert(self, mock_request): mock_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(fakes.fake_request_get_no_v3()) }) mock_request.return_value = mock_response url = ( "https://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3") expected_url = "https://192.168.122.127:8776/" cacert = '/path/to/cert' cinderclient.client.get_server_version(url, cacert=cacert) mock_request.assert_called_once_with(expected_url, verify=cacert, cert=None) @mock.patch('cinderclient.client.requests.get') def test_get_server_version_cert(self, mock_request): mock_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(fakes.fake_request_get_no_v3()) }) mock_request.return_value = mock_response url = ( "https://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3") expected_url = "https://192.168.122.127:8776/" client_cert = '/path/to/cert' cinderclient.client.get_server_version(url, cert=client_cert) mock_request.assert_called_once_with(expected_url, verify=True, cert=client_cert) @mock.patch('cinderclient.client.requests.get') @ddt.data('3.12', '3.40') def test_get_highest_client_server_version(self, version, mock_request): mock_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(fakes.fake_request_get()) }) mock_request.return_value = mock_response url = "http://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3" with mock.patch.object(api_versions, 'MAX_VERSION', version): highest = ( cinderclient.client.get_highest_client_server_version(url)) expected = version if version == '3.12' else '3.16' self.assertEqual(expected, highest) @mock.patch('cinderclient.client.requests.get') def test_get_highest_client_server_version_negative(self, mock_request): mock_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(fakes.fake_request_get_no_v3()) }) mock_request.return_value = mock_response url = "http://192.168.122.127:8776/v3/e5526285ebd741b1819393f772f11fc3" self.assertRaises(exceptions.UnsupportedVersion, cinderclient.client. get_highest_client_server_version, url) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/test_exceptions.py0000664000175000017500000000465600000000000026120 0ustar00zuulzuul00000000000000# Copyright 2015 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Tests the cinderclient.exceptions module.""" import datetime from unittest import mock import requests from cinderclient import exceptions from cinderclient.tests.unit import utils class ExceptionsTest(utils.TestCase): def test_from_response_no_body_message(self): # Tests that we get ClientException back since we don't have 500 mapped response = requests.Response() response.status_code = 500 body = {'keys': ({})} ex = exceptions.from_response(response, body) self.assertIs(exceptions.ClientException, type(ex)) self.assertEqual('n/a', ex.message) def test_from_response_overlimit(self): response = requests.Response() response.status_code = 413 response.headers = {"Retry-After": '10'} body = {'keys': ({})} ex = exceptions.from_response(response, body) self.assertEqual(10, ex.retry_after) self.assertIs(exceptions.OverLimit, type(ex)) @mock.patch('oslo_utils.timeutils.utcnow', return_value=datetime.datetime(2016, 6, 30, 12, 41, 55)) def test_from_response_overlimit_gmt(self, mock_utcnow): response = requests.Response() response.status_code = 413 response.headers = {"Retry-After": "Thu, 30 Jun 2016 12:43:20 GMT"} body = {'keys': ({})} ex = exceptions.from_response(response, body) self.assertEqual(85, ex.retry_after) self.assertIs(exceptions.OverLimit, type(ex)) self.assertTrue(mock_utcnow.called) def test_from_response_overlimit_without_header(self): response = requests.Response() response.status_code = 413 response.headers = {} body = {'keys': ({})} ex = exceptions.from_response(response, body) self.assertEqual(0, ex.retry_after) self.assertIs(exceptions.OverLimit, type(ex)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/test_http.py0000664000175000017500000003236700000000000024716 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import json from unittest import mock import uuid import requests from cinderclient import client from cinderclient import exceptions from cinderclient.tests.unit import utils fake_auth_response = { "access": { "token": { "expires": "2014-11-01T03:32:15-05:00", "id": "FAKE_ID", }, "serviceCatalog": [ { "type": "volumev2", "endpoints": [ { "adminURL": "http://localhost:8776/v2", "region": "RegionOne", "internalURL": "http://localhost:8776/v2", "publicURL": "http://localhost:8776/v2", }, ], }, ], }, } fake_response = utils.TestResponse({ "status_code": 200, "text": '{"hi": "there"}', }) mock_request = mock.Mock(return_value=(fake_response)) fake_201_response = utils.TestResponse({ "status_code": 201, "text": json.dumps(fake_auth_response), }) mock_201_request = mock.Mock(return_value=(fake_201_response)) refused_response = utils.TestResponse({ "status_code": 400, "text": '[Errno 111] Connection refused', }) refused_mock_request = mock.Mock(return_value=(refused_response)) bad_400_response = utils.TestResponse({ "status_code": 400, "text": '', }) bad_400_request = mock.Mock(return_value=(bad_400_response)) bad_401_response = utils.TestResponse({ "status_code": 401, "text": '{"error": {"message": "FAILED!", "details": "DETAILS!"}}', }) bad_401_request = mock.Mock(return_value=(bad_401_response)) bad_413_response = utils.TestResponse({ "status_code": 413, "headers": {"Retry-After": "1", "x-compute-request-id": "1234"}, }) bad_413_request = mock.Mock(return_value=(bad_413_response)) bad_500_response = utils.TestResponse({ "status_code": 500, "text": '{"error": {"message": "FAILED!", "details": "DETAILS!"}}', }) bad_500_request = mock.Mock(return_value=(bad_500_response)) connection_error_request = mock.Mock( side_effect=requests.exceptions.ConnectionError) timeout_error_request = mock.Mock( side_effect=requests.exceptions.Timeout) def get_client(retries=0, **kwargs): cl = client.HTTPClient("username", "password", "project_id", "auth_test", retries=retries, **kwargs) return cl def get_authed_client(retries=0, **kwargs): cl = get_client(retries=retries, **kwargs) cl.management_url = "http://example.com" cl.auth_token = "token" cl.get_service_url = mock.Mock(return_value="http://example.com") return cl def get_authed_endpoint_url(retries=0): cl = client.HTTPClient("username", "password", "project_id", "auth_test", os_endpoint="volume/v100/", retries=retries) cl.auth_token = "token" return cl class ClientTest(utils.TestCase): def test_get(self): cl = get_authed_client() @mock.patch.object(requests, "request", mock_request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") headers = {"X-Auth-Token": "token", "X-Auth-Project-Id": "project_id", "User-Agent": cl.USER_AGENT, 'Accept': 'application/json', } mock_request.assert_called_with( "GET", "http://example.com/hi", headers=headers, **self.TEST_REQUEST_BASE) # Automatic JSON parsing self.assertEqual({"hi": "there"}, body) test_get_call() def test_get_global_id(self): global_id = "req-%s" % uuid.uuid4() cl = get_authed_client(global_request_id=global_id) @mock.patch.object(requests, "request", mock_request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") headers = {"X-Auth-Token": "token", "X-Auth-Project-Id": "project_id", "X-OpenStack-Request-ID": global_id, "User-Agent": cl.USER_AGENT, 'Accept': 'application/json', } mock_request.assert_called_with( "GET", "http://example.com/hi", headers=headers, **self.TEST_REQUEST_BASE) # Automatic JSON parsing self.assertEqual({"hi": "there"}, body) test_get_call() def test_get_reauth_0_retries(self): cl = get_authed_client(retries=0) self.requests = [bad_401_request, mock_request] def request(*args, **kwargs): next_request = self.requests.pop(0) return next_request(*args, **kwargs) def reauth(): cl.management_url = "http://example.com" cl.auth_token = "token" @mock.patch.object(cl, 'authenticate', reauth) @mock.patch.object(requests, "request", request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") test_get_call() self.assertEqual([], self.requests) def test_get_retry_500(self): cl = get_authed_client(retries=1) self.requests = [bad_500_request, mock_request] def request(*args, **kwargs): next_request = self.requests.pop(0) return next_request(*args, **kwargs) @mock.patch.object(requests, "request", request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") test_get_call() self.assertEqual([], self.requests) def test_get_retry_connection_error(self): cl = get_authed_client(retries=1) self.requests = [connection_error_request, mock_request] def request(*args, **kwargs): next_request = self.requests.pop(0) return next_request(*args, **kwargs) @mock.patch.object(requests, "request", request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") test_get_call() self.assertEqual([], self.requests) def test_rate_limit_overlimit_exception(self): cl = get_authed_client(retries=1) self.requests = [bad_413_request, bad_413_request, mock_request] def request(*args, **kwargs): next_request = self.requests.pop(0) return next_request(*args, **kwargs) @mock.patch.object(requests, "request", request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") self.assertRaises(exceptions.OverLimit, test_get_call) self.assertEqual([mock_request], self.requests) def test_rate_limit(self): cl = get_authed_client(retries=1) self.requests = [bad_413_request, mock_request] def request(*args, **kwargs): next_request = self.requests.pop(0) return next_request(*args, **kwargs) @mock.patch.object(requests, "request", request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") return resp, body resp, body = test_get_call() self.assertEqual(200, resp.status_code) self.assertEqual([], self.requests) def test_retry_limit(self): cl = get_authed_client(retries=1) self.requests = [bad_500_request, bad_500_request, mock_request] def request(*args, **kwargs): next_request = self.requests.pop(0) return next_request(*args, **kwargs) @mock.patch.object(requests, "request", request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") self.assertRaises(exceptions.ClientException, test_get_call) self.assertEqual([mock_request], self.requests) def test_get_no_retry_400(self): cl = get_authed_client(retries=0) self.requests = [bad_400_request, mock_request] def request(*args, **kwargs): next_request = self.requests.pop(0) return next_request(*args, **kwargs) @mock.patch.object(requests, "request", request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") self.assertRaises(exceptions.BadRequest, test_get_call) self.assertEqual([mock_request], self.requests) def test_get_retry_400_socket(self): cl = get_authed_client(retries=1) self.requests = [bad_400_request, mock_request] def request(*args, **kwargs): next_request = self.requests.pop(0) return next_request(*args, **kwargs) @mock.patch.object(requests, "request", request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") test_get_call() self.assertEqual([], self.requests) def test_get_no_auth_url(self): client.HTTPClient("username", "password", "project_id", retries=0) def test_post(self): cl = get_authed_client() @mock.patch.object(requests, "request", mock_request) def test_post_call(): cl.post("/hi", body=[1, 2, 3]) headers = { "X-Auth-Token": "token", "X-Auth-Project-Id": "project_id", "Content-Type": "application/json", 'Accept': 'application/json', "User-Agent": cl.USER_AGENT } mock_request.assert_called_with( "POST", "http://example.com/hi", headers=headers, data='[1, 2, 3]', **self.TEST_REQUEST_BASE) test_post_call() def test_os_endpoint_url(self): cl = get_authed_endpoint_url() self.assertEqual("volume/v100", cl.os_endpoint) self.assertEqual("volume/v100", cl.management_url) def test_auth_failure(self): cl = get_client() # response must not have x-server-management-url header @mock.patch.object(requests, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.AuthorizationFailure, cl.authenticate) test_auth_call() def test_auth_with_keystone_v3(self): cl = get_authed_client() cl.auth_url = 'http://example.com:5000/v3' @mock.patch.object(requests, "request", mock_201_request) def test_auth_call(): cl.authenticate() headers = { "Content-Type": "application/json", 'Accept': 'application/json', "User-Agent": cl.USER_AGENT } data = { "auth": { "scope": { "project": { "domain": {"name": "Default"}, "name": "project_id" } }, "identity": { "methods": ["password"], "password": { "user": {"domain": {"name": "Default"}, "password": "password", "name": "username" } } } } } # Check data, we cannot do it on the call because the JSON # dictionary to string can generated different strings. actual_data = mock_201_request.call_args[1]['data'] self.assertDictEqual(data, json.loads(actual_data)) mock_201_request.assert_called_with( "POST", "http://example.com:5000/v3/auth/tokens", headers=headers, allow_redirects=True, data=actual_data, **self.TEST_REQUEST_BASE) test_auth_call() def test_get_retry_timeout_error(self): cl = get_authed_client(retries=1) self.requests = [timeout_error_request, mock_request] def request(*args, **kwargs): next_request = self.requests.pop(0) return next_request(*args, **kwargs) @mock.patch.object(requests, "request", request) @mock.patch('time.time', mock.Mock(return_value=1234)) def test_get_call(): resp, body = cl.get("/hi") test_get_call() self.assertEqual([], self.requests) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/test_shell.py0000664000175000017500000006213600000000000025043 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import io import json import re import sys from unittest import mock import ddt import fixtures import keystoneauth1.exceptions as ks_exc from keystoneauth1.exceptions import DiscoveryFailure from keystoneauth1.identity.generic.password import Password as ks_password from keystoneauth1 import session import requests_mock from testtools import matchers import cinderclient from cinderclient import api_versions from cinderclient.contrib import noauth from cinderclient import exceptions from cinderclient import shell from cinderclient.tests.unit import fake_actions_module from cinderclient.tests.unit.fixture_data import keystone_client from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes @ddt.ddt class ShellTest(utils.TestCase): FAKE_ENV = { 'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_PROJECT_NAME': 'tenant_name', 'OS_AUTH_URL': 'http://no.where/v2.0', } # Patch os.environ to avoid required auth info. def make_env(self, exclude=None, include=None): env = dict((k, v) for k, v in self.FAKE_ENV.items() if k != exclude) env.update(include or {}) self.useFixture(fixtures.MonkeyPatch('os.environ', env)) def setUp(self): super(ShellTest, self).setUp() for var in self.FAKE_ENV: self.useFixture(fixtures.EnvironmentVariable(var, self.FAKE_ENV[var])) self.mock_completion() def shell(self, argstr): orig = sys.stdout try: sys.stdout = io.StringIO() _shell = shell.OpenStackCinderShell() _shell.main(argstr.split()) except SystemExit: exc_type, exc_value, exc_traceback = sys.exc_info() self.assertEqual(0, exc_value.code) finally: out = sys.stdout.getvalue() sys.stdout.close() sys.stdout = orig return out def test_default_auth_env(self): _shell = shell.OpenStackCinderShell() args, __ = _shell.get_base_parser().parse_known_args([]) self.assertEqual('', args.os_auth_type) def test_auth_type_env(self): self.make_env(exclude='OS_PASSWORD', include={'OS_AUTH_SYSTEM': 'non existent auth', 'OS_AUTH_TYPE': 'noauth'}) _shell = shell.OpenStackCinderShell() args, __ = _shell.get_base_parser().parse_known_args([]) self.assertEqual('noauth', args.os_auth_type) def test_auth_system_env(self): self.make_env(exclude='OS_PASSWORD', include={'OS_AUTH_SYSTEM': 'noauth'}) _shell = shell.OpenStackCinderShell() args, __ = _shell.get_base_parser().parse_known_args([]) self.assertEqual('noauth', args.os_auth_type) @mock.patch.object(cinderclient.shell.OpenStackCinderShell, '_get_keystone_session') @mock.patch.object(cinderclient.client.SessionClient, 'authenticate', side_effect=RuntimeError()) def test_password_auth_type(self, mock_authenticate, mock_get_session): self.make_env(include={'OS_AUTH_TYPE': 'password'}) _shell = shell.OpenStackCinderShell() # We crash the command after Client instantiation because this test # focuses only keystoneauth1 indentity cli opts parsing. self.assertRaises(RuntimeError, _shell.main, ['list']) self.assertIsInstance(_shell.cs.client.session.auth, ks_password) def test_help_unknown_command(self): self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo') def test_help(self): # Some expected help output, including microversioned commands required = [ r'.*?^usage: ', r'.*?(?m)^\s+create\s+Creates a volume.', r'.*?(?m)^\s+summary\s+Get volumes summary.', r'.*?(?m)^Run "cinder help SUBCOMMAND" for help on a subcommand.', ] help_text = self.shell('help') for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_help_on_subcommand(self): required = [ r'.*?^usage: cinder list', r'.*?(?m)^Lists all volumes.', ] help_text = self.shell('help list') for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_help_on_subcommand_mv(self): required = [ r'.*?^usage: cinder summary', r'.*?(?m)^Get volumes summary.', ] help_text = self.shell('help summary') for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_help_arg_no_subcommand(self): required = [ r'.*?^usage: ', r'.*?(?m)^\s+create\s+Creates a volume.', r'.*?(?m)^\s+summary\s+Get volumes summary.', r'.*?(?m)^Run "cinder help SUBCOMMAND" for help on a subcommand.', ] help_text = self.shell('--os-volume-api-version 3.40') for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) @ddt.data('backup-create --help', '--help backup-create') def test_dash_dash_help_on_subcommand(self, cmd): required = ['.*?^Creates a volume backup.'] help_text = self.shell(cmd) for r in required: self.assertThat(help_text, matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def register_keystone_auth_fixture(self, mocker, url): mocker.register_uri('GET', url, text=keystone_client.keystone_request_callback) @requests_mock.Mocker() def test_version_discovery(self, mocker): _shell = shell.OpenStackCinderShell() sess = session.Session() os_auth_url = "https://wrongdiscoveryresponse.discovery.com:35357/v2.0" self.register_keystone_auth_fixture(mocker, os_auth_url) self.assertRaises(DiscoveryFailure, _shell._discover_auth_versions, sess, auth_url=os_auth_url) os_auth_url = "https://DiscoveryNotSupported.discovery.com:35357/v2.0" self.register_keystone_auth_fixture(mocker, os_auth_url) v2_url, v3_url = _shell._discover_auth_versions(sess, auth_url=os_auth_url) self.assertEqual(os_auth_url, v2_url, "Expected v2 url") self.assertIsNone(v3_url, "Expected no v3 url") os_auth_url = "https://DiscoveryNotSupported.discovery.com:35357/v3.0" self.register_keystone_auth_fixture(mocker, os_auth_url) v2_url, v3_url = _shell._discover_auth_versions(sess, auth_url=os_auth_url) self.assertEqual(os_auth_url, v3_url, "Expected v3 url") self.assertIsNone(v2_url, "Expected no v2 url") @requests_mock.Mocker() def list_volumes_on_service(self, count, mocker): os_auth_url = "http://multiple.service.names/v2.0" mocker.register_uri('POST', os_auth_url + "/tokens", text=keystone_client.keystone_request_callback) # microversion support requires us to make a versions request # to the endpoint to see exactly what is supported by the server mocker.register_uri('GET', "http://cinder%i.api.com/" % count, text=json.dumps(fakes.fake_request_get())) mocker.register_uri('GET', "http://cinder%i.api.com/v3/volumes/detail" % count, text='{"volumes": []}') self.make_env(include={'OS_AUTH_URL': os_auth_url, 'CINDER_SERVICE_NAME': 'cinder%i' % count}) _shell = shell.OpenStackCinderShell() _shell.main(['list']) def test_duplicate_filters(self): _shell = shell.OpenStackCinderShell() self.assertRaises(exceptions.CommandError, _shell.main, ['list', '--name', 'abc', '--filters', 'name=xyz']) def test_cinder_service_name(self): # Failing with 'No mock address' means we are not # choosing the correct endpoint for count in range(1, 4): self.list_volumes_on_service(count) @mock.patch('keystoneauth1.identity.v2.Password') @mock.patch('keystoneauth1.adapter.Adapter.get_token', side_effect=ks_exc.ConnectFailure()) @mock.patch('keystoneauth1.discover.Discover', side_effect=ks_exc.ConnectFailure()) @mock.patch('sys.stdin', side_effect=mock.Mock) @mock.patch('getpass.getpass', return_value='password') def test_password_prompted(self, mock_getpass, mock_stdin, mock_discover, mock_token, mock_password): self.make_env(exclude='OS_PASSWORD') _shell = shell.OpenStackCinderShell() self.assertRaises(ks_exc.ConnectFailure, _shell.main, ['list']) mock_getpass.assert_called_with('OS Password: ') # Verify that Password() is called with value of param 'password' # equal to mock_getpass.return_value. mock_password.assert_called_with( self.FAKE_ENV['OS_AUTH_URL'], password=mock_getpass.return_value, tenant_id='', tenant_name=self.FAKE_ENV['OS_PROJECT_NAME'], username=self.FAKE_ENV['OS_USERNAME']) @mock.patch('cinderclient.api_versions.discover_version', return_value=api_versions.APIVersion("3.0")) @requests_mock.Mocker() def test_noauth_plugin(self, mock_disco, mocker): # just to prove i'm not crazy about the mock parameter ordering self.assertTrue(requests_mock.mocker.Mocker, type(mocker)) os_volume_url = "http://example.com/volumes/v3" mocker.register_uri('GET', "%s/volumes/detail" % os_volume_url, text='{"volumes": []}') _shell = shell.OpenStackCinderShell() args = ['--os-endpoint', os_volume_url, '--os-auth-type', 'noauth', '--os-user-id', 'admin', '--os-project-id', 'admin', 'list'] _shell.main(args) self.assertIsInstance(_shell.cs.client.session.auth, noauth.CinderNoAuthPlugin) @mock.patch.object(cinderclient.client.HTTPClient, 'authenticate', side_effect=exceptions.Unauthorized('No')) # Easiest way to make cinderclient use httpclient is a None session @mock.patch.object(cinderclient.shell.OpenStackCinderShell, '_get_keystone_session', return_value=None) def test_http_client_insecure(self, mock_authenticate, mock_session): self.make_env(include={'CINDERCLIENT_INSECURE': True}) _shell = shell.OpenStackCinderShell() # This "fails" but instantiates the client. self.assertRaises(exceptions.CommandError, _shell.main, ['list']) self.assertEqual(False, _shell.cs.client.verify_cert) @mock.patch.object(cinderclient.client.SessionClient, 'authenticate', side_effect=exceptions.Unauthorized('No')) def test_session_client_debug_logger(self, mock_session): _shell = shell.OpenStackCinderShell() # This "fails" but instantiates the client. self.assertRaises(exceptions.CommandError, _shell.main, ['--debug', 'list']) # In case of SessionClient when --debug switch is specified # 'keystoneauth' logger should be initialized. self.assertEqual('keystoneauth', _shell.cs.client.logger.name) @mock.patch('keystoneauth1.session.Session.__init__', side_effect=RuntimeError()) def test_http_client_with_cert(self, mock_session): _shell = shell.OpenStackCinderShell() # We crash the command after Session instantiation because this test # focuses only on arguments provided to Session.__init__ args = '--os-cert', 'minnie', 'list' self.assertRaises(RuntimeError, _shell.main, args) mock_session.assert_called_once_with(cert='minnie', verify=mock.ANY) @mock.patch('keystoneauth1.session.Session.__init__', side_effect=RuntimeError()) def test_http_client_with_cert_and_key(self, mock_session): _shell = shell.OpenStackCinderShell() # We crash the command after Session instantiation because this test # focuses only on arguments provided to Session.__init__ args = '--os-cert', 'minnie', '--os-key', 'mickey', 'list' self.assertRaises(RuntimeError, _shell.main, args) mock_session.assert_called_once_with(cert=('minnie', 'mickey'), verify=mock.ANY) class CinderClientArgumentParserTest(utils.TestCase): def setUp(self): super(CinderClientArgumentParserTest, self).setUp() self.mock_completion() def test_ambiguity_solved_for_one_visible_argument(self): parser = shell.CinderClientArgumentParser(add_help=False) parser.add_argument('--test-parameter', dest='visible_param', action='store_true') parser.add_argument('--test_parameter', dest='hidden_param', action='store_true', help=argparse.SUPPRESS) opts = parser.parse_args(['--test']) # visible argument must be set self.assertTrue(opts.visible_param) self.assertFalse(opts.hidden_param) def test_raise_ambiguity_error_two_visible_argument(self): parser = shell.CinderClientArgumentParser(add_help=False) parser.add_argument('--test-parameter', dest="visible_param1", action='store_true') parser.add_argument('--test_parameter', dest="visible_param2", action='store_true') self.assertRaises(SystemExit, parser.parse_args, ['--test']) def test_raise_ambiguity_error_two_hidden_argument(self): parser = shell.CinderClientArgumentParser(add_help=False) parser.add_argument('--test-parameter', dest="hidden_param1", action='store_true', help=argparse.SUPPRESS) parser.add_argument('--test_parameter', dest="hidden_param2", action='store_true', help=argparse.SUPPRESS) self.assertRaises(SystemExit, parser.parse_args, ['--test']) class TestLoadVersionedActions(utils.TestCase): def setUp(self): super(TestLoadVersionedActions, self).setUp() self.mock_completion() def test_load_versioned_actions(self): parser = cinderclient.shell.CinderClientArgumentParser() subparsers = parser.add_subparsers(metavar='') shell = cinderclient.shell.OpenStackCinderShell() shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("3.0"), False, []) self.assertIn('fake-action', shell.subcommands.keys()) self.assertEqual( "fake_action 3.0 to 3.1", shell.subcommands['fake-action'].get_default('func')()) shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("3.2"), False, []) self.assertIn('fake-action', shell.subcommands.keys()) self.assertEqual( "fake_action 3.2 to 3.3", shell.subcommands['fake-action'].get_default('func')()) self.assertIn('fake-action2', shell.subcommands.keys()) self.assertEqual( "fake_action2", shell.subcommands['fake-action2'].get_default('func')()) def test_load_versioned_actions_not_in_version_range(self): parser = cinderclient.shell.CinderClientArgumentParser() subparsers = parser.add_subparsers(metavar='') shell = cinderclient.shell.OpenStackCinderShell() shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion('3.10000'), False, []) self.assertNotIn('fake-action', shell.subcommands.keys()) self.assertIn('fake-action2', shell.subcommands.keys()) def test_load_versioned_actions_unsupported_input(self): parser = cinderclient.shell.CinderClientArgumentParser() subparsers = parser.add_subparsers(metavar='') shell = cinderclient.shell.OpenStackCinderShell() shell.subcommands = {} self.assertRaises(exceptions.UnsupportedAttribute, shell._find_actions, subparsers, fake_actions_module, api_versions.APIVersion('3.6'), False, ['another-fake-action', '--foo']) def test_load_versioned_actions_with_help(self): parser = cinderclient.shell.CinderClientArgumentParser() subparsers = parser.add_subparsers(metavar='') shell = cinderclient.shell.OpenStackCinderShell() shell.subcommands = {} with mock.patch.object(subparsers, 'add_parser') as mock_add_parser: shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("3.1"), True, []) self.assertIn('fake-action', shell.subcommands.keys()) expected_help = ("help message (Supported by API versions " "%(start)s - %(end)s)") % { 'start': '3.0', 'end': '3.3'} expected_desc = ("help message\n\n " "This will not show up in help message\n ") mock_add_parser.assert_any_call( 'fake-action', help=expected_help, description=expected_desc, add_help=False, formatter_class=cinderclient.shell.OpenStackHelpFormatter) def test_load_versioned_actions_with_help_on_latest(self): parser = cinderclient.shell.CinderClientArgumentParser() subparsers = parser.add_subparsers(metavar='') shell = cinderclient.shell.OpenStackCinderShell() shell.subcommands = {} with mock.patch.object(subparsers, 'add_parser') as mock_add_parser: shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("3.latest"), True, []) self.assertIn('another-fake-action', shell.subcommands.keys()) expected_help = (" (Supported by API versions %(start)s - " "%(end)s)%(hint)s") % { 'start': '3.6', 'end': '3.latest', 'hint': cinderclient.shell.HINT_HELP_MSG} mock_add_parser.assert_any_call( 'another-fake-action', help=expected_help, description='', add_help=False, formatter_class=cinderclient.shell.OpenStackHelpFormatter) @mock.patch.object(cinderclient.shell.CinderClientArgumentParser, 'add_argument') def test_load_versioned_actions_with_args(self, mock_add_arg): parser = cinderclient.shell.CinderClientArgumentParser(add_help=False) subparsers = parser.add_subparsers(metavar='') shell = cinderclient.shell.OpenStackCinderShell() shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("3.1"), False, []) self.assertIn('fake-action2', shell.subcommands.keys()) mock_add_arg.assert_has_calls([ mock.call('-h', '--help', action='help', help='==SUPPRESS=='), mock.call('--foo')]) @mock.patch.object(cinderclient.shell.CinderClientArgumentParser, 'add_argument') def test_load_versioned_actions_with_args2(self, mock_add_arg): parser = cinderclient.shell.CinderClientArgumentParser(add_help=False) subparsers = parser.add_subparsers(metavar='') shell = cinderclient.shell.OpenStackCinderShell() shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("3.4"), False, []) self.assertIn('fake-action2', shell.subcommands.keys()) mock_add_arg.assert_has_calls([ mock.call('-h', '--help', action='help', help='==SUPPRESS=='), mock.call('--bar', help="bar help")]) @mock.patch.object(cinderclient.shell.CinderClientArgumentParser, 'add_argument') def test_load_versioned_actions_with_args_not_in_version_range( self, mock_add_arg): parser = cinderclient.shell.CinderClientArgumentParser(add_help=False) subparsers = parser.add_subparsers(metavar='') shell = cinderclient.shell.OpenStackCinderShell() shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("3.10000"), False, []) self.assertIn('fake-action2', shell.subcommands.keys()) mock_add_arg.assert_has_calls([ mock.call('-h', '--help', action='help', help='==SUPPRESS==')]) @mock.patch.object(cinderclient.shell.CinderClientArgumentParser, 'add_argument') def test_load_versioned_actions_with_args_and_help(self, mock_add_arg): parser = cinderclient.shell.CinderClientArgumentParser(add_help=False) subparsers = parser.add_subparsers(metavar='') shell = cinderclient.shell.OpenStackCinderShell() shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("3.4"), True, []) mock_add_arg.assert_has_calls([ mock.call('-h', '--help', action='help', help='==SUPPRESS=='), mock.call('--bar', help="bar help (Supported by API versions" " 3.3 - 3.4)")]) @mock.patch.object(cinderclient.shell.CinderClientArgumentParser, 'add_argument') def test_load_actions_with_versioned_args(self, mock_add_arg): parser = cinderclient.shell.CinderClientArgumentParser(add_help=False) subparsers = parser.add_subparsers(metavar='') shell = cinderclient.shell.OpenStackCinderShell() shell.subcommands = {} shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("3.6"), False, []) self.assertIn(mock.call('--foo', help="first foo"), mock_add_arg.call_args_list) self.assertNotIn(mock.call('--foo', help="second foo"), mock_add_arg.call_args_list) mock_add_arg.reset_mock() shell._find_actions(subparsers, fake_actions_module, api_versions.APIVersion("3.9"), False, []) self.assertNotIn(mock.call('--foo', help="first foo"), mock_add_arg.call_args_list) self.assertIn(mock.call('--foo', help="second foo"), mock_add_arg.call_args_list) class ShellUtilsTest(utils.TestCase): @mock.patch.object(cinderclient.utils, 'print_dict') def test_print_volume_image(self, mock_print_dict): response = {'os-volume_upload_image': {'name': 'myimg1'}} image_resp_tuple = (202, response) cinderclient.shell_utils.print_volume_image(image_resp_tuple) response = {'os-volume_upload_image': {'name': 'myimg2', 'volume_type': None}} image_resp_tuple = (202, response) cinderclient.shell_utils.print_volume_image(image_resp_tuple) response = {'os-volume_upload_image': {'name': 'myimg3', 'volume_type': {'id': '1234', 'name': 'sometype'}}} image_resp_tuple = (202, response) cinderclient.shell_utils.print_volume_image(image_resp_tuple) mock_print_dict.assert_has_calls( (mock.call({'name': 'myimg1'}), mock.call({'name': 'myimg2', 'volume_type': None}), mock.call({'name': 'myimg3', 'volume_type': 'sometype'}))) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/test_utils.py0000664000175000017500000002432400000000000025071 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import collections import io import sys from unittest import mock import ddt from cinderclient import api_versions from cinderclient.apiclient import base as common_base from cinderclient import base from cinderclient import exceptions from cinderclient import shell_utils from cinderclient.tests.unit import utils as test_utils from cinderclient import utils REQUEST_ID = 'req-test-request-id' UUID = '8e8ec658-c7b0-4243-bdf8-6f7f2952c0d0' class FakeResource(object): NAME_ATTR = 'name' def __init__(self, _id, properties, **kwargs): self.id = _id try: self.name = properties['name'] except KeyError: pass def append_request_ids(self, resp): pass class FakeManager(base.ManagerWithFind): resource_class = FakeResource resources = [ FakeResource('1234', {'name': 'entity_one'}), FakeResource(UUID, {'name': 'entity_two'}), FakeResource('5678', {'name': '9876'}) ] def get(self, resource_id, **kwargs): for resource in self.resources: if resource.id == str(resource_id): return resource raise exceptions.NotFound(resource_id) def list(self, search_opts, **kwargs): return common_base.ListWithMeta(self.resources, REQUEST_ID) class FakeManagerWithApi(base.Manager): @api_versions.wraps('3.1') def return_api_version(self): return '3.1' @api_versions.wraps('3.2') def return_api_version(self): # noqa: F811 return '3.2' class FakeDisplayResource(object): NAME_ATTR = 'display_name' def __init__(self, _id, properties): self.id = _id try: self.display_name = properties['display_name'] except KeyError: pass def append_request_ids(self, resp): pass class FakeDisplayManager(FakeManager): resource_class = FakeDisplayResource resources = [ FakeDisplayResource('4242', {'display_name': 'entity_three'}), ] class FindResourceTestCase(test_utils.TestCase): def setUp(self): super(FindResourceTestCase, self).setUp() self.manager = FakeManager(None) def test_find_none(self): self.manager.find = mock.Mock(side_effect=self.manager.find) self.assertRaises(exceptions.CommandError, utils.find_resource, self.manager, 'asdf') self.assertEqual(2, self.manager.find.call_count) def test_find_by_integer_id(self): output = utils.find_resource(self.manager, 1234) self.assertEqual(self.manager.get('1234'), output) def test_find_by_str_id(self): output = utils.find_resource(self.manager, '1234') self.assertEqual(self.manager.get('1234'), output) def test_find_by_uuid(self): output = utils.find_resource(self.manager, UUID) self.assertEqual(self.manager.get(UUID), output) def test_find_by_str_name(self): output = utils.find_resource(self.manager, 'entity_one') self.assertEqual(self.manager.get('1234'), output) def test_find_by_str_displayname(self): display_manager = FakeDisplayManager(None) output = utils.find_resource(display_manager, 'entity_three') self.assertEqual(display_manager.get('4242'), output) def test_find_by_group_id(self): output = utils.find_resource(self.manager, 1234, is_group=True, list_volume=True) self.assertEqual(self.manager.get('1234', list_volume=True), output) def test_find_by_group_name(self): display_manager = FakeDisplayManager(None) output = utils.find_resource(display_manager, 'entity_three', is_group=True, list_volume=True) self.assertEqual(display_manager.get('4242', list_volume=True), output) class CaptureStdout(object): """Context manager for capturing stdout from statements in its block.""" def __enter__(self): self.real_stdout = sys.stdout self.stringio = io.StringIO() sys.stdout = self.stringio return self def __exit__(self, *args): sys.stdout = self.real_stdout self.stringio.seek(0) self.read = self.stringio.read @ddt.ddt class BuildQueryParamTestCase(test_utils.TestCase): def test_build_param_without_sort_switch(self): dict_param = { 'key1': 'val1', 'key2': 'val2', 'key3': 'val3', } result = utils.build_query_param(dict_param, True) self.assertIn('key1=val1', result) self.assertIn('key2=val2', result) self.assertIn('key3=val3', result) def test_build_param_with_sort_switch(self): dict_param = { 'key1': 'val1', 'key2': 'val2', 'key3': 'val3', } result = utils.build_query_param(dict_param, True) expected = "?key1=val1&key2=val2&key3=val3" self.assertEqual(expected, result) @ddt.data({}, None, {'key1': 'val1', 'key2': None, 'key3': False, 'key4': ''}) def test_build_param_with_nones(self, dict_param): result = utils.build_query_param(dict_param) expected = ("key1=val1", "key3=False") if dict_param else () for exp in expected: self.assertIn(exp, result) if not expected: self.assertEqual("", result) @ddt.ddt class ExtractFilterTestCase(test_utils.TestCase): @ddt.data({'content': ['key1=value1'], 'expected': {'key1': 'value1'}}, {'content': ['key1={key2:value2}'], 'expected': {'key1': {'key2': 'value2'}}}, {'content': ['key1=value1', 'key2={key22:value22}'], 'expected': {'key1': 'value1', 'key2': {'key22': 'value22'}}}) @ddt.unpack def test_extract_filters(self, content, expected): result = shell_utils.extract_filters(content) self.assertEqual(expected, result) class PrintListTestCase(test_utils.TestCase): def test_print_list_with_list(self): Row = collections.namedtuple('Row', ['a', 'b']) to_print = [Row(a=3, b=4), Row(a=1, b=2)] with CaptureStdout() as cso: utils.print_list(to_print, ['a', 'b']) # Output should be sorted by the first key (a) self.assertEqual("""\ +---+---+ | a | b | +---+---+ | 1 | 2 | | 3 | 4 | +---+---+ """, cso.read()) def test_print_list_with_None_data(self): Row = collections.namedtuple('Row', ['a', 'b']) to_print = [Row(a=3, b=None), Row(a=1, b=2)] with CaptureStdout() as cso: utils.print_list(to_print, ['a', 'b']) # Output should be sorted by the first key (a) self.assertEqual("""\ +---+---+ | a | b | +---+---+ | 1 | 2 | | 3 | - | +---+---+ """, cso.read()) def test_print_list_with_list_sortby(self): Row = collections.namedtuple('Row', ['a', 'b']) to_print = [Row(a=4, b=3), Row(a=2, b=1)] with CaptureStdout() as cso: utils.print_list(to_print, ['a', 'b'], sortby_index=1) # Output should be sorted by the second key (b) self.assertEqual("""\ +---+---+ | a | b | +---+---+ | 2 | 1 | | 4 | 3 | +---+---+ """, cso.read()) def test_print_list_with_list_no_sort(self): Row = collections.namedtuple('Row', ['a', 'b']) to_print = [Row(a=3, b=4), Row(a=1, b=2)] with CaptureStdout() as cso: utils.print_list(to_print, ['a', 'b'], sortby_index=None) # Output should be in the order given self.assertEqual("""\ +---+---+ | a | b | +---+---+ | 3 | 4 | | 1 | 2 | +---+---+ """, cso.read()) def test_print_list_with_generator(self): Row = collections.namedtuple('Row', ['a', 'b']) def gen_rows(): for row in [Row(a=1, b=2), Row(a=3, b=4)]: yield row with CaptureStdout() as cso: utils.print_list(gen_rows(), ['a', 'b']) self.assertEqual("""\ +---+---+ | a | b | +---+---+ | 1 | 2 | | 3 | 4 | +---+---+ """, cso.read()) def test_print_list_with_return(self): Row = collections.namedtuple('Row', ['a', 'b']) to_print = [Row(a=3, b='a\r'), Row(a=1, b='c\rd')] with CaptureStdout() as cso: utils.print_list(to_print, ['a', 'b']) # Output should be sorted by the first key (a) self.assertEqual("""\ +---+-----+ | a | b | +---+-----+ | 1 | c d | | 3 | a | +---+-----+ """, cso.read()) class PrintDictTestCase(test_utils.TestCase): def test__pretty_format_dict(self): content = {'key1': 'value1', 'key2': 'value2'} expected = "key1 : value1\nkey2 : value2" result = utils._pretty_format_dict(content) self.assertEqual(expected, result) def test_print_dict_with_return(self): d = {'a': 'A', 'b': 'B', 'c': 'C', 'd': 'test\rcarriage\n\rreturn'} with CaptureStdout() as cso: utils.print_dict(d) self.assertEqual("""\ +----------+---------------+ | Property | Value | +----------+---------------+ | a | A | | b | B | | c | C | | d | test carriage | | | return | +----------+---------------+ """, cso.read()) def test_print_dict_with_dict_inside(self): content = {'a': 'A', 'b': 'B', 'f_key': {'key1': 'value1', 'key2': 'value2'}} with CaptureStdout() as cso: utils.print_dict(content, formatters='f_key') self.assertEqual("""\ +----------+---------------+ | Property | Value | +----------+---------------+ | a | A | | b | B | | f_key | key1 : value1 | | | key2 : value2 | +----------+---------------+ """, cso.read()) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/utils.py0000664000175000017500000001061200000000000024025 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import json import os from unittest import mock import fixtures import requests from requests_mock.contrib import fixture as requests_mock_fixture import testtools REQUEST_ID = ['req-test-request-id'] class TestCase(testtools.TestCase): TEST_REQUEST_BASE = { 'verify': True, 'cert': None } def setUp(self): super(TestCase, self).setUp() if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or os.environ.get('OS_STDOUT_CAPTURE') == '1'): stdout = self.useFixture(fixtures.StringStream('stdout')).stream self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or os.environ.get('OS_STDERR_CAPTURE') == '1'): stderr = self.useFixture(fixtures.StringStream('stderr')).stream self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) # FIXME(eharney) - this should only be needed for shell tests self.mock_completion() def _assert_request_id(self, obj, count=1): self.assertTrue(hasattr(obj, 'request_ids')) self.assertEqual(REQUEST_ID * count, obj.request_ids) def assert_called_anytime(self, method, url, body=None, partial_body=None): return self.shell.cs.assert_called_anytime(method, url, body, partial_body) def mock_completion(self): patcher = mock.patch( 'cinderclient.base.Manager.write_to_completion_cache') patcher.start() self.addCleanup(patcher.stop) patcher = mock.patch('cinderclient.base.Manager.completion_cache') patcher.start() self.addCleanup(patcher.stop) class FixturedTestCase(TestCase): client_fixture_class = None data_fixture_class = None def setUp(self): super(FixturedTestCase, self).setUp() self.requests = self.useFixture(requests_mock_fixture.Fixture()) self.data_fixture = None self.client_fixture = None self.cs = None if self.client_fixture_class: fix = self.client_fixture_class(self.requests) self.client_fixture = self.useFixture(fix) self.cs = self.client_fixture.new_client() if self.data_fixture_class: fix = self.data_fixture_class(self.requests) self.data_fixture = self.useFixture(fix) def assert_called(self, method, path, body=None): self.assertEqual(method, self.requests.last_request.method) self.assertEqual(path, self.requests.last_request.path_url) if body: req_data = self.requests.last_request.body if isinstance(req_data, bytes): req_data = req_data.decode('utf-8') if not isinstance(body, str): # json load if the input body to match against is not a string req_data = json.loads(req_data) self.assertEqual(body, req_data) class TestResponse(requests.Response): """Class used to wrap requests.Response. Provides some convenience to initialize with a dict. """ def __init__(self, data): super(TestResponse, self).__init__() self._content = None self._text = None if isinstance(data, dict): self.status_code = data.get('status_code', None) self.headers = data.get('headers', None) self.reason = data.get('reason', '') # Fake text and content attributes to streamline Response creation text = data.get('text', None) self._content = text self._text = text else: self.status_code = data def __eq__(self, other): return self.__dict__ == other.__dict__ @property def content(self): return self._content @property def text(self): return self._text ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1645793255.277896 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/0000775000175000017500000000000000000000000022643 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/__init__.py0000664000175000017500000000000000000000000024742 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1645793255.277896 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/contrib/0000775000175000017500000000000000000000000024303 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/contrib/__init__.py0000664000175000017500000000000000000000000026402 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/contrib/test_list_extensions.py0000664000175000017500000000240500000000000031147 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import extension from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3.contrib import list_extensions extensions = [ extension.Extension(list_extensions.__name__.split(".")[-1], list_extensions), ] cs = fakes.FakeClient(extensions=extensions) class ListExtensionsTests(utils.TestCase): def test_list_extensions(self): all_exts = cs.list_extensions.show_all() cs.assert_called('GET', '/extensions') self.assertGreater(len(all_exts), 0) for r in all_exts: self.assertGreater(len(r.summary), 0) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/fakes.py0000664000175000017500000006534500000000000024323 0ustar00zuulzuul00000000000000# Copyright (c) 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. from datetime import datetime from cinderclient.tests.unit import fakes from cinderclient.tests.unit.v3 import fakes_base from cinderclient.v3 import client fake_attachment = {'attachment': { 'status': 'reserved', 'detached_at': '', 'connection_info': {}, 'attached_at': '', 'attach_mode': None, 'id': 'a232e9ae', 'instance': 'e84fda45-4de4-4ce4-8f39-fc9d3b0aa05e', 'volume_id': '557ad76c-ce54-40a3-9e91-c40d21665cc3', }} fake_attachment_without_instance_id = {'attachment': { 'status': 'reserved', 'detached_at': '', 'connection_info': {}, 'attached_at': '', 'attach_mode': None, 'id': 'a232e9ae', 'instance': None, 'volume_id': '557ad76c-ce54-40a3-9e91-c40d21665cc3', }} fake_attachment_list = {'attachments': [ {'instance': 'instance_1', 'name': 'attachment-1', 'volume_id': 'fake_volume_1', 'status': 'reserved', 'id': 'attachmentid_1'}, {'instance': 'instance_2', 'name': 'attachment-2', 'volume_id': 'fake_volume_2', 'status': 'reserverd', 'id': 'attachmentid_2'}]} fake_connection_info = { 'auth_password': 'i6h9E5HQqSkcGX3H', 'attachment_id': 'a232e9ae', 'target_discovered': False, 'encrypted': False, 'driver_volume_type': 'iscsi', 'qos_specs': None, 'target_iqn': 'iqn.2010-10.org.openstack:volume-557ad76c', 'target_portal': '10.117.36.28:3260', 'volume_id': '557ad76c-ce54-40a3-9e91-c40d21665cc3', 'target_lun': 0, 'access_mode': 'rw', 'auth_username': 'MwRrnAFLHN7enw5R95yM', 'auth_method': 'CHAP'} fake_connector = { 'initiator': 'iqn.1993-08.org.debian:01:b79dbce99387', 'mount_device': '/dev/vdb', 'ip': '10.117.36.28', 'platform': 'x86_64', 'host': 'os-2', 'do_local_attach': False, 'os_type': 'linux2', 'multipath': False} def _stub_group(detailed=True, **kwargs): group = { "name": "test-1", "id": "1234", } if detailed: details = { "created_at": "2012-08-28T16:30:31.000000", "description": "test-1-desc", "availability_zone": "zone1", "status": "available", "group_type": "my_group_type", } group.update(details) group.update(kwargs) return group def _stub_group_snapshot(detailed=True, **kwargs): group_snapshot = { "name": None, "id": "5678", } if detailed: details = { "created_at": "2012-08-28T16:30:31.000000", "description": None, "name": None, "id": "5678", "status": "available", "group_id": "1234", } group_snapshot.update(details) group_snapshot.update(kwargs) return group_snapshot def _stub_snapshot(**kwargs): snapshot = { "created_at": "2012-08-28T16:30:31.000000", "display_description": None, "display_name": None, "id": '11111111-1111-1111-1111-111111111111', "size": 1, "status": "available", "volume_id": '00000000-0000-0000-0000-000000000000', } snapshot.update(kwargs) return snapshot class FakeClient(fakes.FakeClient, client.Client): def __init__(self, api_version=None, *args, **kwargs): client.Client.__init__(self, 'username', 'password', 'project_id', 'auth_url', extensions=kwargs.get('extensions')) self.api_version = api_version global_id = "req-f551871a-4950-4225-9b2c-29a14c8f075e" self.client = FakeHTTPClient(api_version=api_version, global_request_id=global_id, **kwargs) def get_volume_api_version_from_endpoint(self): return self.client.get_volume_api_version_from_endpoint() class FakeHTTPClient(fakes_base.FakeHTTPClient): def __init__(self, **kwargs): super(FakeHTTPClient, self).__init__() self.management_url = 'http://10.0.2.15:8776/v3/fake' vars(self).update(kwargs) # # Services # def get_os_services(self, **kw): host = kw.get('host', None) binary = kw.get('binary', None) services = [ { 'id': 1, 'binary': 'cinder-volume', 'host': 'host1', 'zone': 'cinder', 'status': 'enabled', 'state': 'up', 'updated_at': datetime(2012, 10, 29, 13, 42, 2), 'cluster': 'cluster1', 'backend_state': 'up', }, { 'id': 2, 'binary': 'cinder-volume', 'host': 'host2', 'zone': 'cinder', 'status': 'disabled', 'state': 'down', 'updated_at': datetime(2012, 9, 18, 8, 3, 38), 'cluster': 'cluster1', 'backend_state': 'down', }, { 'id': 3, 'binary': 'cinder-scheduler', 'host': 'host2', 'zone': 'cinder', 'status': 'disabled', 'state': 'down', 'updated_at': datetime(2012, 9, 18, 8, 3, 38), 'cluster': 'cluster2', }, ] if host: services = [i for i in services if i['host'] == host] if binary: services = [i for i in services if i['binary'] == binary] if not self.api_version.matches('3.7'): for svc in services: del svc['cluster'] if not self.api_version.matches('3.49'): for svc in services: if svc['binary'] == 'cinder-volume': del svc['backend_state'] return (200, {}, {'services': services}) def put_os_services_enable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], 'status': 'enabled'}) def put_os_services_disable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], 'status': 'disabled'}) def put_os_services_disable_log_reason(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], 'status': 'disabled', 'disabled_reason': body['disabled_reason']}) # # Clusters # def _filter_clusters(self, return_keys, **kw): date = datetime(2012, 10, 29, 13, 42, 2), clusters = [ { 'id': '1', 'name': 'cluster1@lvmdriver-1', 'state': 'up', 'status': 'enabled', 'binary': 'cinder-volume', 'is_up': 'True', 'disabled': 'False', 'disabled_reason': None, 'num_hosts': '3', 'num_down_hosts': '2', 'updated_at': date, 'created_at': date, 'last_heartbeat': date, }, { 'id': '2', 'name': 'cluster1@lvmdriver-2', 'state': 'down', 'status': 'enabled', 'binary': 'cinder-volume', 'is_up': 'False', 'disabled': 'False', 'disabled_reason': None, 'num_hosts': '2', 'num_down_hosts': '2', 'updated_at': date, 'created_at': date, 'last_heartbeat': date, }, { 'id': '3', 'name': 'cluster2', 'state': 'up', 'status': 'disabled', 'binary': 'cinder-backup', 'is_up': 'True', 'disabled': 'True', 'disabled_reason': 'Reason', 'num_hosts': '1', 'num_down_hosts': '0', 'updated_at': date, 'created_at': date, 'last_heartbeat': date, }, ] for key, value in kw.items(): clusters = [cluster for cluster in clusters if cluster[key] == str(value)] result = [] for cluster in clusters: result.append({key: cluster[key] for key in return_keys}) return result CLUSTER_SUMMARY_KEYS = ('name', 'binary', 'state', 'status') CLUSTER_DETAIL_KEYS = (CLUSTER_SUMMARY_KEYS + ('num_hosts', 'num_down_hosts', 'last_heartbeat', 'disabled_reason', 'created_at', 'updated_at')) def get_clusters(self, **kw): clusters = self._filter_clusters(self.CLUSTER_SUMMARY_KEYS, **kw) return (200, {}, {'clusters': clusters}) def get_clusters_detail(self, **kw): clusters = self._filter_clusters(self.CLUSTER_DETAIL_KEYS, **kw) return (200, {}, {'clusters': clusters}) def get_clusters_1(self): res = self.get_clusters_detail(id=1) return (200, {}, {'cluster': res[2]['clusters'][0]}) def put_clusters_enable(self, body): res = self.get_clusters(id=1) return (200, {}, {'cluster': res[2]['clusters'][0]}) def put_clusters_disable(self, body): res = self.get_clusters(id=3) return (200, {}, {'cluster': res[2]['clusters'][0]}) # # Backups # def put_backups_1234(self, **kw): backup = fakes_base._stub_backup( id='1234', base_uri='http://localhost:8776', tenant_id='0fa851f6668144cf9cd8c8419c1646c1') return (200, {}, {'backups': backup}) # # Attachments # def post_attachments(self, **kw): if kw['body']['attachment'].get('instance_uuid'): return (200, {}, fake_attachment) return (200, {}, fake_attachment_without_instance_id) def get_attachments(self, **kw): return (200, {}, fake_attachment_list) def post_attachments_a232e9ae_action(self, **kw): # noqa: E501 attached_fake = fake_attachment attached_fake['status'] = 'attached' return (200, {}, attached_fake) def post_attachments_1234_action(self, **kw): # noqa: E501 attached_fake = fake_attachment attached_fake['status'] = 'attached' return (200, {}, attached_fake) def get_attachments_1234(self, **kw): return (200, {}, { 'attachment': {'instance': 1234, 'name': 'attachment-1', 'volume_id': 'fake_volume_1', 'status': 'reserved'}}) def put_attachments_1234(self, **kw): return (200, {}, { 'attachment': {'instance': 1234, 'name': 'attachment-1', 'volume_id': 'fake_volume_1', 'status': 'reserved'}}) def delete_attachments_1234(self, **kw): return 204, {}, None # # GroupTypes # def get_group_types(self, **kw): return (200, {}, { 'group_types': [{'id': 1, 'name': 'test-type-1', 'description': 'test_type-1-desc', 'group_specs': {}}, {'id': 2, 'name': 'test-type-2', 'description': 'test_type-2-desc', 'group_specs': {}}]}) def get_group_types_1(self, **kw): return (200, {}, {'group_type': {'id': 1, 'name': 'test-type-1', 'description': 'test_type-1-desc', 'group_specs': {'key': 'value'}}}) def get_group_types_2(self, **kw): return (200, {}, {'group_type': {'id': 2, 'name': 'test-type-2', 'description': 'test_type-2-desc', 'group_specs': {}}}) def get_group_types_3(self, **kw): return (200, {}, {'group_type': {'id': 3, 'name': 'test-type-3', 'description': 'test_type-3-desc', 'group_specs': {}, 'is_public': False}}) def get_group_types_default(self, **kw): return self.get_group_types_1() def post_group_types(self, body, **kw): return (202, {}, {'group_type': {'id': 3, 'name': 'test-type-3', 'description': 'test_type-3-desc', 'group_specs': {}}}) def post_group_types_1_group_specs(self, body, **kw): assert list(body) == ['group_specs'] return (200, {}, {'group_specs': {'k': 'v'}}) def delete_group_types_1_group_specs_k(self, **kw): return(204, {}, None) def delete_group_types_1_group_specs_m(self, **kw): return(204, {}, None) def delete_group_types_1(self, **kw): return (202, {}, None) def delete_group_types_3_group_specs_k(self, **kw): return(204, {}, None) def delete_group_types_3(self, **kw): return (202, {}, None) def put_group_types_1(self, **kw): return self.get_group_types_1() # # Groups # def get_groups_detail(self, **kw): return (200, {}, {"groups": [ _stub_group(id='1234'), _stub_group(id='4567')]}) def get_groups(self, **kw): return (200, {}, {"groups": [ _stub_group(detailed=False, id='1234'), _stub_group(detailed=False, id='4567')]}) def get_groups_1234(self, **kw): return (200, {}, {'group': _stub_group(id='1234')}) def post_groups(self, **kw): group = _stub_group(id='1234', group_type='my_group_type', volume_types=['type1', 'type2']) return (202, {}, {'group': group}) def put_groups_1234(self, **kw): return (200, {}, {'group': {}}) def post_groups_1234_action(self, body, **kw): resp = 202 assert len(list(body)) == 1 action = list(body)[0] if action == 'delete': assert 'delete-volumes' in body[action] elif action in ('enable_replication', 'disable_replication', 'failover_replication', 'list_replication_targets', 'reset_status'): assert action in body elif action == 'os-reimage': assert 'image_id' in body[action] else: raise AssertionError("Unexpected action: %s" % action) return (resp, {}, {}) def post_groups_action(self, body, **kw): group = _stub_group(id='1234', group_type='my_group_type', volume_types=['type1', 'type2']) resp = 202 assert len(list(body)) == 1 action = list(body)[0] if action == 'create-from-src': assert ('group_snapshot_id' in body[action] or 'source_group_id' in body[action]) else: raise AssertionError("Unexpected action: %s" % action) return (resp, {}, {'group': group}) # # group_snapshots # def get_group_snapshots_detail(self, **kw): return (200, {}, {"group_snapshots": [ _stub_group_snapshot(id='1234'), _stub_group_snapshot(id='4567')]}) def get_group_snapshots(self, **kw): return (200, {}, {"group_snapshots": [ _stub_group_snapshot(detailed=False, id='1234'), _stub_group_snapshot(detailed=False, id='4567')]}) def get_group_snapshots_1234(self, **kw): return (200, {}, {'group_snapshot': _stub_group_snapshot(id='1234')}) def get_group_snapshots_5678(self, **kw): return (200, {}, {'group_snapshot': _stub_group_snapshot(id='5678')}) def post_group_snapshots(self, **kw): group_snap = _stub_group_snapshot() return (202, {}, {'group_snapshot': group_snap}) def put_group_snapshots_1234(self, **kw): return (200, {}, {'group_snapshot': {}}) def get_groups_5678(self, **kw): return (200, {}, {'group': _stub_group(id='5678')}) def post_groups_5678_action(self, **kw): return (202, {}, {}) def post_snapshots_1234_action(self, **kw): return (202, {}, {}) def get_snapshots_1234(self, **kw): return (200, {}, {'snapshot': _stub_snapshot(id='1234')}) def post_snapshots_5678_action(self, **kw): return (202, {}, {}) def get_snapshots_5678(self, **kw): return (200, {}, {'snapshot': _stub_snapshot(id='5678')}) def post_group_snapshots_1234_action(self, **kw): return (202, {}, {}) def post_group_snapshots_5678_action(self, **kw): return (202, {}, {}) def delete_group_snapshots_1234(self, **kw): return (202, {}, {}) # # Manageable volumes/snapshots # def get_manageable_volumes(self, **kw): vol_id = "volume-ffffffff-0000-ffff-0000-ffffffffffff" vols = [{"size": 4, "safe_to_manage": False, "actual_size": 4.0, "reference": {"source-name": vol_id}}, {"size": 5, "safe_to_manage": True, "actual_size": 4.3, "reference": {"source-name": "myvol"}}] return (200, {}, {"manageable-volumes": vols}) def get_manageable_volumes_detail(self, **kw): vol_id = "volume-ffffffff-0000-ffff-0000-ffffffffffff" vols = [{"size": 4, "reason_not_safe": "volume in use", "safe_to_manage": False, "extra_info": "qos_setting:high", "reference": {"source-name": vol_id}, "actual_size": 4.0}, {"size": 5, "reason_not_safe": None, "safe_to_manage": True, "extra_info": "qos_setting:low", "actual_size": 4.3, "reference": {"source-name": "myvol"}}] return (200, {}, {"manageable-volumes": vols}) def get_manageable_snapshots(self, **kw): snap_id = "snapshot-ffffffff-0000-ffff-0000-ffffffffffff" snaps = [{"actual_size": 4.0, "size": 4, "safe_to_manage": False, "source_id_type": "source-name", "source_cinder_id": "00000000-ffff-0000-ffff-00000000", "reference": {"source-name": snap_id}, "source_identifier": "volume-00000000-ffff-0000-ffff-000000"}, {"actual_size": 4.3, "reference": {"source-name": "mysnap"}, "source_id_type": "source-name", "source_identifier": "myvol", "safe_to_manage": True, "source_cinder_id": None, "size": 5}] return (200, {}, {"manageable-snapshots": snaps}) def get_manageable_snapshots_detail(self, **kw): snap_id = "snapshot-ffffffff-0000-ffff-0000-ffffffffffff" snaps = [{"actual_size": 4.0, "size": 4, "safe_to_manage": False, "source_id_type": "source-name", "source_cinder_id": "00000000-ffff-0000-ffff-00000000", "reference": {"source-name": snap_id}, "source_identifier": "volume-00000000-ffff-0000-ffff-000000", "extra_info": "qos_setting:high", "reason_not_safe": "snapshot in use"}, {"actual_size": 4.3, "reference": {"source-name": "mysnap"}, "safe_to_manage": True, "source_cinder_id": None, "source_id_type": "source-name", "identifier": "mysnap", "source_identifier": "myvol", "size": 5, "extra_info": "qos_setting:low", "reason_not_safe": None}] return (200, {}, {"manageable-snapshots": snaps}) # # Messages # def get_messages(self, **kw): return 200, {}, {'messages': [ { 'id': '1234', 'event_id': 'VOLUME_000002', 'user_message': 'Fake Message', 'created_at': '2012-08-27T00:00:00.000000', 'guaranteed_until': "2013-11-12T21:00:00.000000", }, { 'id': '12345', 'event_id': 'VOLUME_000002', 'user_message': 'Fake Message', 'created_at': '2012-08-27T00:00:00.000000', 'guaranteed_until': "2013-11-12T21:00:00.000000", } ]} def delete_messages_1234(self, **kw): return 204, {}, None def delete_messages_12345(self, **kw): return 204, {}, None def get_messages_1234(self, **kw): message = { 'id': '1234', 'event_id': 'VOLUME_000002', 'user_message': 'Fake Message', 'created_at': '2012-08-27T00:00:00.000000', 'guaranteed_until': "2013-11-12T21:00:00.000000", } return 200, {}, {'message': message} def get_messages_12345(self, **kw): message = { 'id': '12345', 'event_id': 'VOLUME_000002', 'user_message': 'Fake Message', 'created_at': '2012-08-27T00:00:00.000000', 'guaranteed_until': "2013-11-12T21:00:00.000000", } return 200, {}, {'message': message} def put_os_services_set_log(self, body): return (202, {}, {}) def put_os_services_get_log(self, body): levels = [{'binary': 'cinder-api', 'host': 'host1', 'levels': {'prefix1': 'DEBUG', 'prefix2': 'INFO'}}, {'binary': 'cinder-volume', 'host': 'host@backend#pool', 'levels': {'prefix3': 'WARNING', 'prefix4': 'ERROR'}}] return (200, {}, {'log_levels': levels}) def get_volumes_summary(self, **kw): return 200, {}, {"volume-summary": {'total_size': 5, 'total_count': 5, 'metadata': { "test_key": ["test_value"] } } } def post_workers_cleanup(self, **kw): response = { 'cleaning': [{'id': '1', 'cluster_name': 'cluster1', 'host': 'host1', 'binary': 'binary'}, {'id': '3', 'cluster_name': 'cluster1', 'host': 'host3', 'binary': 'binary'}], 'unavailable': [{'id': '2', 'cluster_name': 'cluster2', 'host': 'host2', 'binary': 'binary'}], } return 200, {}, response # # resource filters # def get_resource_filters(self, **kw): return 200, {}, {'resource_filters': []} def get_volume_transfers_detail(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' transfer1 = '5678' transfer2 = 'f625ec3e-13dd-4498-a22a-50afd534cc41' return (200, {}, {'transfers': [ fakes_base._stub_transfer_full(transfer1, base_uri, tenant_id), fakes_base._stub_transfer_full(transfer2, base_uri, tenant_id)]}) def get_volume_transfers_5678(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' transfer1 = '5678' return (200, {}, {'transfer': fakes_base._stub_transfer_full(transfer1, base_uri, tenant_id)}) def delete_volume_transfers_5678(self, **kw): return (202, {}, None) def post_volume_transfers(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' transfer1 = '5678' return (202, {}, {'transfer': fakes_base._stub_transfer(transfer1, base_uri, tenant_id)}) def post_volume_transfers_5678_accept(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' transfer1 = '5678' return (200, {}, {'transfer': fakes_base._stub_transfer(transfer1, base_uri, tenant_id)}) def fake_request_get(): versions = {'versions': [{'id': 'v2.0', 'links': [{'href': 'http://docs.openstack.org/', 'rel': 'describedby', 'type': 'text/html'}, {'href': 'http://192.168.122.197/v2/', 'rel': 'self'}], 'media-types': [{'base': 'application/json', 'type': 'application/'}], 'min_version': '', 'status': 'DEPRECATED', 'updated': '2014-06-28T12:20:21Z', 'version': ''}, {'id': 'v3.0', 'links': [{'href': 'http://docs.openstack.org/', 'rel': 'describedby', 'type': 'text/html'}, {'href': 'http://192.168.122.197/v3/', 'rel': 'self'}], 'media-types': [{'base': 'application/json', 'type': 'application/'}], 'min_version': '3.0', 'status': 'CURRENT', 'updated': '2016-02-08T12:20:21Z', 'version': '3.16'}]} return versions def fake_request_get_no_v3(): versions = {'versions': [{'id': 'v2.0', 'links': [{'href': 'http://docs.openstack.org/', 'rel': 'describedby', 'type': 'text/html'}, {'href': 'http://192.168.122.197/v2/', 'rel': 'self'}], 'media-types': [{'base': 'application/json', 'type': 'application/'}], 'min_version': '', 'status': 'DEPRECATED', 'updated': '2014-06-28T12:20:21Z', 'version': ''}]} return versions ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/fakes_base.py0000664000175000017500000013023500000000000025304 0ustar00zuulzuul00000000000000# Copyright (c) 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. from datetime import datetime from urllib import parse as urlparse from cinderclient import client as base_client from cinderclient.tests.unit import fakes import cinderclient.tests.unit.utils as utils REQUEST_ID = 'req-test-request-id' def _stub_volume(*args, **kwargs): volume = { "migration_status": None, "attachments": [{'server_id': '1234', 'id': '3f88836f-adde-4296-9f6b-2c59a0bcda9a', 'attachment_id': '5678'}], "links": [ { "href": "http://localhost/v2/fake/volumes/1234", "rel": "self" }, { "href": "http://localhost/fake/volumes/1234", "rel": "bookmark" } ], "availability_zone": "cinder", "os-vol-host-attr:host": "ip-192-168-0-2", "encrypted": "false", "updated_at": "2013-11-12T21:00:00.000000", "os-volume-replication:extended_status": "None", "replication_status": "disabled", "snapshot_id": None, 'id': 1234, "size": 1, "user_id": "1b2d6e8928954ca4ae7c243863404bdc", "os-vol-tenant-attr:tenant_id": "eb72eb33a0084acf8eb21356c2b021a7", "os-vol-mig-status-attr:migstat": None, "metadata": {}, "status": "available", 'description': None, "os-volume-replication:driver_data": None, "source_volid": None, "consistencygroup_id": None, "os-vol-mig-status-attr:name_id": None, "name": "sample-volume", "bootable": "false", "created_at": "2012-08-27T00:00:00.000000", "volume_type": "None", } volume.update(kwargs) return volume def _stub_snapshot(**kwargs): snapshot = { "created_at": "2012-08-28T16:30:31.000000", "display_description": None, "display_name": None, "id": '11111111-1111-1111-1111-111111111111', "size": 1, "status": "available", "volume_id": '00000000-0000-0000-0000-000000000000', } snapshot.update(kwargs) return snapshot def _stub_consistencygroup(detailed=True, **kwargs): consistencygroup = { "name": "cg", "id": "11111111-1111-1111-1111-111111111111", } if detailed: details = { "created_at": "2012-08-28T16:30:31.000000", "description": None, "availability_zone": "myzone", "status": "available", } consistencygroup.update(details) consistencygroup.update(kwargs) return consistencygroup def _stub_cgsnapshot(detailed=True, **kwargs): cgsnapshot = { "name": None, "id": "11111111-1111-1111-1111-111111111111", } if detailed: details = { "created_at": "2012-08-28T16:30:31.000000", "description": None, "name": None, "id": "11111111-1111-1111-1111-111111111111", "status": "available", "consistencygroup_id": "00000000-0000-0000-0000-000000000000", } cgsnapshot.update(details) cgsnapshot.update(kwargs) return cgsnapshot def _stub_type_access(**kwargs): access = {'volume_type_id': '11111111-1111-1111-1111-111111111111', 'project_id': '00000000-0000-0000-0000-000000000000'} access.update(kwargs) return access def _self_href(base_uri, tenant_id, backup_id): return '%s/v2/%s/backups/%s' % (base_uri, tenant_id, backup_id) def _bookmark_href(base_uri, tenant_id, backup_id): return '%s/%s/backups/%s' % (base_uri, tenant_id, backup_id) def _stub_backup_full(id, base_uri, tenant_id): return { 'id': id, 'name': 'backup', 'description': 'nightly backup', 'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b', 'container': 'volumebackups', 'object_count': 220, 'size': 10, 'availability_zone': 'az1', 'created_at': '2013-04-12T08:16:37.000000', 'status': 'available', 'links': [ { 'href': _self_href(base_uri, tenant_id, id), 'rel': 'self' }, { 'href': _bookmark_href(base_uri, tenant_id, id), 'rel': 'bookmark' } ] } def _stub_backup(id, base_uri, tenant_id): return { 'id': id, 'name': 'backup', 'links': [ { 'href': _self_href(base_uri, tenant_id, id), 'rel': 'self' }, { 'href': _bookmark_href(base_uri, tenant_id, id), 'rel': 'bookmark' } ] } def _stub_qos_full(id, base_uri, tenant_id, name=None, specs=None): if not name: name = 'fake-name' if not specs: specs = {} return { 'qos_specs': { 'id': id, 'name': name, 'consumer': 'back-end', 'specs': specs, }, 'links': { 'href': _bookmark_href(base_uri, tenant_id, id), 'rel': 'bookmark' } } def _stub_qos_associates(id, name): return { 'assoications_type': 'volume_type', 'name': name, 'id': id, } def _stub_restore(): return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} def _stub_transfer_full(id, base_uri, tenant_id): return { 'id': id, 'name': 'transfer', 'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc', 'created_at': '2013-04-12T08:16:37.000000', 'auth_key': '123456', 'links': [ { 'href': _self_href(base_uri, tenant_id, id), 'rel': 'self' }, { 'href': _bookmark_href(base_uri, tenant_id, id), 'rel': 'bookmark' } ] } def _stub_transfer(id, base_uri, tenant_id): return { 'id': id, 'name': 'transfer', 'volume_id': '8c05f861-6052-4df6-b3e0-0aebfbe686cc', 'links': [ { 'href': _self_href(base_uri, tenant_id, id), 'rel': 'self' }, { 'href': _bookmark_href(base_uri, tenant_id, id), 'rel': 'bookmark' } ] } def _stub_extend(id, new_size): return {'volume_id': '712f4980-5ac1-41e5-9383-390aa7c9f58b'} def _stub_server_versions(): return [ { "status": "SUPPORTED", "updated": "2015-07-30T11:33:21Z", "links": [ { "href": "http://docs.openstack.org/", "type": "text/html", "rel": "describedby", }, { "href": "http://localhost:8776/v1/", "rel": "self", } ], "min_version": "", "version": "", "id": "v1.0", }, { "status": "SUPPORTED", "updated": "2015-09-30T11:33:21Z", "links": [ { "href": "http://docs.openstack.org/", "type": "text/html", "rel": "describedby", }, { "href": "http://localhost:8776/v2/", "rel": "self", } ], "min_version": "", "version": "", "id": "v2.0", }, { "status": "CURRENT", "updated": "2016-04-01T11:33:21Z", "links": [ { "href": "http://docs.openstack.org/", "type": "text/html", "rel": "describedby", }, { "href": "http://localhost:8776/v3/", "rel": "self", } ], "min_version": "3.0", "version": "3.1", "id": "v3.0", } ] def stub_default_type(): return { 'default_type': { 'project_id': '629632e7-99d2-4c40-9ae3-106fa3b1c9b7', 'volume_type_id': '4c298f16-e339-4c80-b934-6cbfcb7525a0' } } def stub_default_types(): return { 'default_types': [ { 'project_id': '629632e7-99d2-4c40-9ae3-106fa3b1c9b7', 'volume_type_id': '4c298f16-e339-4c80-b934-6cbfcb7525a0' }, { 'project_id': 'a0c01994-1245-416e-8fc9-1aca86329bfd', 'volume_type_id': 'ff094b46-f82a-4a74-9d9e-d3d08116ad93' } ] } class FakeHTTPClient(base_client.HTTPClient): def __init__(self, version_header=None, **kwargs): self.username = 'username' self.password = 'password' self.auth_url = 'auth_url' self.callstack = [] self.management_url = 'http://10.0.2.15:8776/v2/fake' self.osapi_max_limit = 1000 self.marker = None self.version_header = version_header def _cs_request(self, url, method, **kwargs): # Check that certain things are called correctly if method in ['GET', 'DELETE']: assert 'body' not in kwargs elif method == 'PUT': assert 'body' in kwargs # Call the method args = urlparse.parse_qsl(urlparse.urlparse(url)[4]) kwargs.update(args) url_split = url.rsplit('?', 1) munged_url = url_split[0] if len(url_split) > 1: parameters = url_split[1] if 'marker' in parameters: self.marker = int(parameters.rsplit('marker=', 1)[1]) else: self.marker = None else: self.marker = None munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') munged_url = munged_url.replace('-', '_') callback = "%s_%s" % (method.lower(), munged_url) if not hasattr(self, callback): raise AssertionError('Called unknown API method: %s %s, ' 'expected fakes method name: %s' % (method, url, callback)) # Note the call self.callstack.append((method, url, kwargs.get('body', None))) status, headers, body = getattr(self, callback)(**kwargs) # add fake request-id header headers['x-openstack-request-id'] = REQUEST_ID if self.version_header: headers['OpenStack-API-version'] = self.version_header r = utils.TestResponse({ "status_code": status, "text": body, "headers": headers, }) return r, body def get_volume_api_version_from_endpoint(self): magic_tuple = urlparse.urlsplit(self.management_url) scheme, netloc, path, query, frag = magic_tuple return path.lstrip('/').split('/')[0][1:] # # Snapshots # def get_snapshots_detail(self, **kw): if kw.get('with_count', False): return (200, {}, {'snapshots': [ _stub_snapshot(), ], 'count': 1}) return (200, {}, {'snapshots': [ _stub_snapshot()]}) def get_snapshots_1234(self, **kw): return (200, {}, {'snapshot': _stub_snapshot(id='1234')}) def get_snapshots_5678(self, **kw): return (200, {}, {'snapshot': _stub_snapshot(id='5678')}) def post_snapshots(self, **kw): metadata = kw['body']['snapshot'].get('metadata', None) snapshot = _stub_snapshot(id='1234', volume_id='1234') if snapshot is not None: snapshot.update({'metadata': metadata}) return (202, {}, {'snapshot': snapshot}) def put_snapshots_1234(self, **kw): snapshot = _stub_snapshot(id='1234') snapshot.update(kw['body']['snapshot']) return (200, {}, {'snapshot': snapshot}) def post_snapshots_1234_action(self, body, **kw): _body = None resp = 202 assert len(list(body)) == 1 action = list(body)[0] if action == 'os-reset_status': assert 'status' in body['os-reset_status'] elif action == 'os-update_snapshot_status': assert 'status' in body['os-update_snapshot_status'] elif action == 'os-force_delete': assert body[action] is None elif action == 'os-unmanage': assert body[action] is None else: raise AssertionError('Unexpected action: %s' % action) return (resp, {}, _body) def post_snapshots_5678_action(self, body, **kw): return self.post_snapshots_1234_action(body, **kw) def delete_snapshots_1234(self, **kw): return (202, {}, {}) def delete_snapshots_5678(self, **kw): return (202, {}, {}) # # Volumes # def put_volumes_1234(self, **kw): volume = _stub_volume(id='1234') volume.update(kw['body']['volume']) return (200, {}, {'volume': volume}) def get_volumes(self, **kw): if self.marker == 1234: return (200, {}, {"volumes": [ {'id': 5678, 'name': 'sample-volume2'} ]}) elif self.osapi_max_limit == 1: return (200, {}, {"volumes": [ {'id': 1234, 'name': 'sample-volume'} ], "volumes_links": [ {'href': "/volumes?limit=1&marker=1234", 'rel': 'next'} ]}) else: return (200, {}, {"volumes": [ {'id': 1234, 'name': 'sample-volume'}, {'id': 5678, 'name': 'sample-volume2'} ]}) def get_volumes_detail(self, **kw): if kw.get('with_count', False): return (200, {}, {"volumes": [ _stub_volume(id=kw.get('id', 1234)) ], "count": 1}) return (200, {}, {"volumes": [ _stub_volume(id=kw.get('id', 1234)) ]}) def get_volumes_1234(self, **kw): r = {'volume': self.get_volumes_detail(id=1234)[2]['volumes'][0]} return (200, {}, r) def get_volumes_5678(self, **kw): r = {'volume': self.get_volumes_detail(id=5678)[2]['volumes'][0]} return (200, {}, r) def get_volumes_1234_metadata(self, **kw): r = {"metadata": {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}} return (200, {}, r) def get_volumes_1234_encryption(self, **kw): r = {'encryption_key_id': 'id'} return (200, {}, r) def post_volumes_1234_action(self, body, **kw): _body = None resp = 202 assert len(list(body)) == 1 action = list(body)[0] if action == 'os-attach': keys = sorted(list(body[action])) assert (keys == ['instance_uuid', 'mode', 'mountpoint'] or keys == ['host_name', 'mode', 'mountpoint']) elif action == 'os-detach': assert list(body[action]) == ['attachment_id'] elif action == 'os-reserve': assert body[action] is None elif action == 'os-unreserve': assert body[action] is None elif action == 'os-initialize_connection': assert list(body[action]) == ['connector'] return (202, {}, {'connection_info': {'foos': 'bars'}}) elif action == 'os-terminate_connection': assert list(body[action]) == ['connector'] elif action == 'os-begin_detaching': assert body[action] is None elif action == 'os-roll_detaching': assert body[action] is None elif action == 'os-reset_status': assert ('status' or 'attach_status' or 'migration_status' in body[action]) elif action == 'os-extend': assert list(body[action]) == ['new_size'] elif action == 'os-migrate_volume': assert 'host' in body[action] assert 'force_host_copy' in body[action] elif action == 'os-update_readonly_flag': assert list(body[action]) == ['readonly'] elif action == 'os-retype': assert 'new_type' in body[action] elif action == 'os-set_bootable': assert list(body[action]) == ['bootable'] elif action == 'os-unmanage': assert body[action] is None elif action == 'os-set_image_metadata': assert list(body[action]) == ['metadata'] elif action == 'os-unset_image_metadata': assert 'key' in body[action] elif action == 'os-show_image_metadata': assert body[action] is None elif action == 'os-volume_upload_image': assert 'image_name' in body[action] _body = body elif action == 'revert': assert 'snapshot_id' in body[action] elif action == 'os-reimage': assert 'image_id' in body[action] else: raise AssertionError("Unexpected action: %s" % action) return (resp, {}, _body) def get_volumes_fake(self, **kw): r = {'volume': self.get_volumes_detail(id='fake')[2]['volumes'][0]} return (200, {}, r) def post_volumes_fake_action(self, body, **kw): _body = None resp = 202 return (resp, {}, _body) def post_volumes_5678_action(self, body, **kw): return self.post_volumes_1234_action(body, **kw) def post_volumes(self, **kw): size = kw['body']['volume'].get('size', 1) volume = _stub_volume(id='1234', size=size) return (202, {}, {'volume': volume}) def delete_volumes_1234(self, **kw): return (202, {}, None) def delete_volumes_5678(self, **kw): return (202, {}, None) # # Consistencygroups # def get_consistencygroups_detail(self, **kw): return (200, {}, {"consistencygroups": [ _stub_consistencygroup(id='1234'), _stub_consistencygroup(id='4567')]}) def get_consistencygroups(self, **kw): return (200, {}, {"consistencygroups": [ _stub_consistencygroup(detailed=False, id='1234'), _stub_consistencygroup(detailed=False, id='4567')]}) def get_consistencygroups_1234(self, **kw): return (200, {}, {'consistencygroup': _stub_consistencygroup(id='1234')}) def post_consistencygroups(self, **kw): return (202, {}, {'consistencygroup': {}}) def put_consistencygroups_1234(self, **kw): return (200, {}, {'consistencygroup': {}}) def post_consistencygroups_1234_delete(self, **kw): return (202, {}, {}) def post_consistencygroups_create_from_src(self, **kw): return (200, {}, {'consistencygroup': _stub_consistencygroup( id='1234', cgsnapshot_id='1234')}) # # Cgsnapshots # def get_cgsnapshots_detail(self, **kw): return (200, {}, {"cgsnapshots": [ _stub_cgsnapshot(id='1234'), _stub_cgsnapshot(id='4567')]}) def get_cgsnapshots(self, **kw): return (200, {}, {"cgsnapshots": [ _stub_cgsnapshot(detailed=False, id='1234'), _stub_cgsnapshot(detailed=False, id='4567')]}) def get_cgsnapshots_1234(self, **kw): return (200, {}, {'cgsnapshot': _stub_cgsnapshot(id='1234')}) def post_cgsnapshots(self, **kw): return (202, {}, {'cgsnapshot': {}}) def put_cgsnapshots_1234(self, **kw): return (200, {}, {'cgsnapshot': {}}) def delete_cgsnapshots_1234(self, **kw): return (202, {}, {}) # # Quotas # def get_os_quota_sets_test(self, **kw): return (200, {}, {'quota_set': { 'tenant_id': 'test', 'metadata_items': [], 'volumes': 1, 'snapshots': 1, 'gigabytes': 1, 'backups': 1, 'backup_gigabytes': 1, 'per_volume_gigabytes': 1, }}) def get_os_quota_sets_test_defaults(self): return (200, {}, {'quota_set': { 'tenant_id': 'test', 'metadata_items': [], 'volumes': 1, 'snapshots': 1, 'gigabytes': 1, 'backups': 1, 'backup_gigabytes': 1, 'per_volume_gigabytes': 1, }}) def put_os_quota_sets_test(self, body, **kw): assert list(body) == ['quota_set'] fakes.assert_has_keys(body['quota_set'], required=['tenant_id']) return (200, {}, {'quota_set': { 'tenant_id': 'test', 'metadata_items': [], 'volumes': 2, 'snapshots': 2, 'gigabytes': 1, 'backups': 1, 'backup_gigabytes': 1, 'per_volume_gigabytes': 1, }}) def delete_os_quota_sets_1234(self, **kw): return (200, {}, {}) def delete_os_quota_sets_test(self, **kw): return (200, {}, {}) # # Quota Classes # def get_os_quota_class_sets_test(self, **kw): return (200, {}, {'quota_class_set': { 'class_name': 'test', 'volumes': 1, 'snapshots': 1, 'gigabytes': 1, 'backups': 1, 'backup_gigabytes': 1, 'per_volume_gigabytes': 1, }}) def put_os_quota_class_sets_test(self, body, **kw): assert list(body) == ['quota_class_set'] fakes.assert_has_keys(body['quota_class_set']) return (200, {}, {'quota_class_set': { 'volumes': 2, 'snapshots': 2, 'gigabytes': 1, 'backups': 1, 'backup_gigabytes': 1, 'per_volume_gigabytes': 1}}) # # VolumeTypes # def get_types(self, **kw): return (200, {}, { 'volume_types': [{'id': 1, 'name': 'test-type-1', 'description': 'test_type-1-desc', 'extra_specs': {}}, {'id': 2, 'name': 'test-type-2', 'description': 'test_type-2-desc', 'extra_specs': {}}]}) def get_types_1(self, **kw): return (200, {}, {'volume_type': {'id': 1, 'name': 'test-type-1', 'description': 'test_type-1-desc', 'extra_specs': {'key': 'value'}}}) def get_types_2(self, **kw): return (200, {}, {'volume_type': {'id': 2, 'name': 'test-type-2', 'description': 'test_type-2-desc', 'extra_specs': {}}}) def get_types_3(self, **kw): return (200, {}, {'volume_type': {'id': 3, 'name': 'test-type-3', 'description': 'test_type-3-desc', 'extra_specs': {}, 'os-volume-type-access:is_public': False}}) def get_types_default(self, **kw): return self.get_types_1() def post_types(self, body, **kw): return (202, {}, {'volume_type': {'id': 3, 'name': 'test-type-3', 'description': 'test_type-3-desc', 'extra_specs': {}}}) def post_types_3_action(self, body, **kw): _body = None resp = 202 assert len(list(body)) == 1 action = list(body)[0] if action == 'addProjectAccess': assert 'project' in body['addProjectAccess'] elif action == 'removeProjectAccess': assert 'project' in body['removeProjectAccess'] else: raise AssertionError('Unexpected action: %s' % action) return (resp, {}, _body) def post_types_1_extra_specs(self, body, **kw): assert list(body) == ['extra_specs'] return (200, {}, {'extra_specs': {'k': 'v'}}) def delete_types_1_extra_specs_k(self, **kw): return(204, {}, None) def delete_types_1_extra_specs_m(self, **kw): return(204, {}, None) def delete_types_1(self, **kw): return (202, {}, None) def delete_types_3_extra_specs_k(self, **kw): return(204, {}, None) def delete_types_3(self, **kw): return (202, {}, None) def put_types_1(self, **kw): return self.get_types_1() def put_types_3(self, **kw): return (200, {}, {'volume_type': {'id': 3, 'name': 'test-type-2', 'description': 'test_type-3-desc', 'is_public': True, 'extra_specs': {}}}) # # VolumeAccess # def get_types_3_os_volume_type_access(self, **kw): return (200, {}, {'volume_type_access': [ _stub_type_access() ]}) # # VolumeEncryptionTypes # def get_types_1_encryption(self, **kw): return (200, {}, {'id': 1, 'volume_type_id': 1, 'provider': 'test', 'cipher': 'test', 'key_size': 1, 'control_location': 'front-end'}) def get_types_2_encryption(self, **kw): return (200, {}, {}) def post_types_2_encryption(self, body, **kw): return (200, {}, {'encryption': body}) def put_types_1_encryption_provider(self, body, **kw): get_body = self.get_types_1_encryption()[2] for k, v in body.items(): if k in get_body.keys(): get_body.update([(k, v)]) return (200, {}, get_body) def delete_types_1_encryption_provider(self, **kw): return (202, {}, None) # # Set/Unset metadata # def delete_volumes_1234_metadata_test_key(self, **kw): return (204, {}, None) def delete_volumes_1234_metadata_key1(self, **kw): return (204, {}, None) def delete_volumes_1234_metadata_key2(self, **kw): return (204, {}, None) def post_volumes_1234_metadata(self, **kw): return (204, {}, {'metadata': {'test_key': 'test_value'}}) # # List all extensions # def get_extensions(self, **kw): exts = [ { "alias": "FAKE-1", "description": "Fake extension number 1", "links": [], "name": "Fake1", "namespace": ("http://docs.openstack.org/" "/ext/fake1/api/v1.1"), "updated": "2011-06-09T00:00:00+00:00" }, { "alias": "FAKE-2", "description": "Fake extension number 2", "links": [], "name": "Fake2", "namespace": ("http://docs.openstack.org/" "/ext/fake1/api/v1.1"), "updated": "2011-06-09T00:00:00+00:00" }, ] return (200, {}, {"extensions": exts, }) # # VolumeBackups # def get_backups_76a17945_3c6f_435c_975b_b5685db10b62(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' backup1 = '76a17945-3c6f-435c-975b-b5685db10b62' return (200, {}, {'backup': _stub_backup_full(backup1, base_uri, tenant_id)}) def get_backups_1234(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' backup1 = '1234' return (200, {}, {'backup': _stub_backup_full(backup1, base_uri, tenant_id)}) def get_backups_5678(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' backup1 = '5678' return (200, {}, {'backup': _stub_backup_full(backup1, base_uri, tenant_id)}) def get_backups_detail(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' backup1 = '76a17945-3c6f-435c-975b-b5685db10b62' backup2 = 'd09534c6-08b8-4441-9e87-8976f3a8f699' if kw.get('with_count', False): return (200, {}, {'backups': [ _stub_backup_full(backup1, base_uri, tenant_id), _stub_backup_full(backup2, base_uri, tenant_id)], 'count': 2}) return (200, {}, {'backups': [ _stub_backup_full(backup1, base_uri, tenant_id), _stub_backup_full(backup2, base_uri, tenant_id)]}) def delete_backups_76a17945_3c6f_435c_975b_b5685db10b62(self, **kw): return (202, {}, None) def delete_backups_1234(self, **kw): return (202, {}, None) def delete_backups_5678(self, **kw): return (202, {}, None) def post_backups(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' backup1 = '76a17945-3c6f-435c-975b-b5685db10b62' return (202, {}, {'backup': _stub_backup(backup1, base_uri, tenant_id)}) def post_backups_76a17945_3c6f_435c_975b_b5685db10b62_restore(self, **kw): return (200, {}, {'restore': _stub_restore()}) def post_backups_1234_restore(self, **kw): return (200, {}, {'restore': _stub_restore()}) def post_backups_76a17945_3c6f_435c_975b_b5685db10b62_action(self, **kw): return(200, {}, None) def post_backups_1234_action(self, **kw): return(200, {}, None) def post_backups_5678_action(self, **kw): return(200, {}, None) def get_backups_76a17945_3c6f_435c_975b_b5685db10b62_export_record(self, **kw): return (200, {}, {'backup-record': {'backup_service': 'fake-backup-service', 'backup_url': 'fake-backup-url'}}) def get_backups_1234_export_record(self, **kw): return (200, {}, {'backup-record': {'backup_service': 'fake-backup-service', 'backup_url': 'fake-backup-url'}}) def post_backups_import_record(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' backup1 = '76a17945-3c6f-435c-975b-b5685db10b62' return (200, {}, {'backup': _stub_backup(backup1, base_uri, tenant_id)}) # # QoSSpecs # def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' return (200, {}, _stub_qos_full(qos_id1, base_uri, tenant_id)) def get_qos_specs(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' qos_id1 = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' qos_id2 = '0FD8DD14-A396-4E55-9573-1FE59042E95B' return (200, {}, {'qos_specs': [ _stub_qos_full(qos_id1, base_uri, tenant_id, 'name-1'), _stub_qos_full(qos_id2, base_uri, tenant_id)]}) def post_qos_specs(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' qos_name = 'qos-name' return (202, {}, _stub_qos_full(qos_id, base_uri, tenant_id, qos_name)) def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): return (202, {}, None) def put_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_delete_keys( self, **kw): return (202, {}, None) def delete_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C(self, **kw): return (202, {}, None) def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associations( self, **kw): type_id1 = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' type_id2 = '4230B13A-AB37-4E84-B777-EFBA6FCEE4FF' type_name1 = 'type1' type_name2 = 'type2' return (202, {}, {'qos_associations': [ _stub_qos_associates(type_id1, type_name1), _stub_qos_associates(type_id2, type_name2)]}) def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_associate( self, **kw): return (202, {}, None) def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate( self, **kw): return (202, {}, None) def get_qos_specs_1B6B6A04_A927_4AEB_810B_B7BAAD49F57C_disassociate_all( self, **kw): return (202, {}, None) # # # VolumeTransfers # def get_os_volume_transfer_5678(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' transfer1 = '5678' return (200, {}, {'transfer': _stub_transfer_full(transfer1, base_uri, tenant_id)}) def get_os_volume_transfer_detail(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' transfer1 = '5678' transfer2 = 'f625ec3e-13dd-4498-a22a-50afd534cc41' return (200, {}, {'transfers': [ _stub_transfer_full(transfer1, base_uri, tenant_id), _stub_transfer_full(transfer2, base_uri, tenant_id)]}) def delete_os_volume_transfer_5678(self, **kw): return (202, {}, None) def post_os_volume_transfer(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' transfer1 = '5678' return (202, {}, {'transfer': _stub_transfer(transfer1, base_uri, tenant_id)}) def post_os_volume_transfer_5678_accept(self, **kw): base_uri = 'http://localhost:8776' tenant_id = '0fa851f6668144cf9cd8c8419c1646c1' transfer1 = '5678' return (200, {}, {'transfer': _stub_transfer(transfer1, base_uri, tenant_id)}) def get_with_base_url(self, url, **kw): if 'default-types' in url: return self._cs_request(url, 'GET', **kw) server_versions = _stub_server_versions() return (200, {'versions': server_versions}) def create_update_with_base_url(self, url, **kwargs): return self._cs_request(url, 'PUT', **kwargs) def put_v3_default_types_629632e7_99d2_4c40_9ae3_106fa3b1c9b7( self, **kwargs): default_type = stub_default_type() return (200, {}, default_type) def get_v3_default_types_629632e7_99d2_4c40_9ae3_106fa3b1c9b7( self, **kw): default_types = stub_default_type() return (200, {}, default_types) def get_v3_default_types(self, **kw): default_types = stub_default_types() return (200, {}, default_types) def delete_with_base_url(self, url, **kwargs): return self._cs_request(url, 'DELETE', **kwargs) def delete_v3_default_types_629632e7_99d2_4c40_9ae3_106fa3b1c9b7( self, **kwargs): return (204, {}, {}) # # Services # def get_os_services(self, **kw): host = kw.get('host', None) binary = kw.get('binary', None) services = [ { 'binary': 'cinder-volume', 'host': 'host1', 'zone': 'cinder', 'status': 'enabled', 'state': 'up', 'updated_at': datetime(2012, 10, 29, 13, 42, 2) }, { 'binary': 'cinder-volume', 'host': 'host2', 'zone': 'cinder', 'status': 'disabled', 'state': 'down', 'updated_at': datetime(2012, 9, 18, 8, 3, 38) }, { 'binary': 'cinder-scheduler', 'host': 'host2', 'zone': 'cinder', 'status': 'disabled', 'state': 'down', 'updated_at': datetime(2012, 9, 18, 8, 3, 38) }, ] if host: services = [i for i in services if i['host'] == host] if binary: services = [i for i in services if i['binary'] == binary] return (200, {}, {'services': services}) def put_os_services_enable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], 'status': 'enabled'}) def put_os_services_disable(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], 'status': 'disabled'}) def put_os_services_disable_log_reason(self, body, **kw): return (200, {}, {'host': body['host'], 'binary': body['binary'], 'status': 'disabled', 'disabled_reason': body['disabled_reason']}) def get_os_availability_zone(self, **kw): return (200, {}, { "availabilityZoneInfo": [ { "zoneName": "zone-1", "zoneState": {"available": True}, "hosts": None, }, { "zoneName": "zone-2", "zoneState": {"available": False}, "hosts": None, }, ] }) def get_os_availability_zone_detail(self, **kw): return (200, {}, { "availabilityZoneInfo": [ { "zoneName": "zone-1", "zoneState": {"available": True}, "hosts": { "fake_host-1": { "cinder-volume": { "active": True, "available": True, "updated_at": datetime(2012, 12, 26, 14, 45, 25, 0) } } } }, { "zoneName": "internal", "zoneState": {"available": True}, "hosts": { "fake_host-1": { "cinder-sched": { "active": True, "available": True, "updated_at": datetime(2012, 12, 26, 14, 45, 24, 0) } } } }, { "zoneName": "zone-2", "zoneState": {"available": False}, "hosts": None, }, ] }) def post_snapshots_1234_metadata(self, **kw): return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}}) def delete_snapshots_1234_metadata_key1(self, **kw): return (200, {}, None) def delete_snapshots_1234_metadata_key2(self, **kw): return (200, {}, None) def put_volumes_1234_metadata(self, **kw): return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}}) def put_snapshots_1234_metadata(self, **kw): return (200, {}, {"metadata": {"key1": "val1", "key2": "val2"}}) def get_os_volume_manage(self, **kw): vol_id = "volume-ffffffff-0000-ffff-0000-ffffffffffff" vols = [{"size": 4, "safe_to_manage": False, "actual_size": 4.0, "reference": {"source-name": vol_id}}, {"size": 5, "safe_to_manage": True, "actual_size": 4.3, "reference": {"source-name": "myvol"}}] return (200, {}, {"manageable-volumes": vols}) def get_os_volume_manage_detail(self, **kw): vol_id = "volume-ffffffff-0000-ffff-0000-ffffffffffff" vols = [{"size": 4, "reason_not_safe": "volume in use", "safe_to_manage": False, "extra_info": "qos_setting:high", "reference": {"source-name": vol_id}, "actual_size": 4.0}, {"size": 5, "reason_not_safe": None, "safe_to_manage": True, "extra_info": "qos_setting:low", "actual_size": 4.3, "reference": {"source-name": "myvol"}}] return (200, {}, {"manageable-volumes": vols}) def post_os_volume_manage(self, **kw): volume = _stub_volume(id='1234') volume.update(kw['body']['volume']) return (202, {}, {'volume': volume}) def get_os_snapshot_manage(self, **kw): snap_id = "snapshot-ffffffff-0000-ffff-0000-ffffffffffff" snaps = [{"actual_size": 4.0, "size": 4, "safe_to_manage": False, "source_id_type": "source-name", "source_cinder_id": "00000000-ffff-0000-ffff-00000000", "reference": {"source-name": snap_id}, "source_identifier": "volume-00000000-ffff-0000-ffff-000000"}, {"actual_size": 4.3, "reference": {"source-name": "mysnap"}, "source_id_type": "source-name", "source_identifier": "myvol", "safe_to_manage": True, "source_cinder_id": None, "size": 5}] return (200, {}, {"manageable-snapshots": snaps}) def get_os_snapshot_manage_detail(self, **kw): snap_id = "snapshot-ffffffff-0000-ffff-0000-ffffffffffff" snaps = [{"actual_size": 4.0, "size": 4, "safe_to_manage": False, "source_id_type": "source-name", "source_cinder_id": "00000000-ffff-0000-ffff-00000000", "reference": {"source-name": snap_id}, "source_identifier": "volume-00000000-ffff-0000-ffff-000000", "extra_info": "qos_setting:high", "reason_not_safe": "snapshot in use"}, {"actual_size": 4.3, "reference": {"source-name": "mysnap"}, "safe_to_manage": True, "source_cinder_id": None, "source_id_type": "source-name", "identifier": "mysnap", "source_identifier": "myvol", "size": 5, "extra_info": "qos_setting:low", "reason_not_safe": None}] return (200, {}, {"manageable-snapshots": snaps}) def post_os_snapshot_manage(self, **kw): snapshot = _stub_snapshot(id='1234', volume_id='volume_id1') snapshot.update(kw['body']['snapshot']) return (202, {}, {'snapshot': snapshot}) def get_scheduler_stats_get_pools(self, **kw): stats = [ { "name": "ubuntu@lvm#backend_name", "capabilities": { "pool_name": "backend_name", "QoS_support": False, "timestamp": "2014-11-21T18:15:28.141161", "allocated_capacity_gb": 0, "volume_backend_name": "backend_name", "free_capacity_gb": 7.01, "driver_version": "2.0.0", "total_capacity_gb": 10.01, "reserved_percentage": 0, "vendor_name": "Open Source", "storage_protocol": "iSCSI", } }, ] return (200, {}, {"pools": stats}) def get_capabilities_host(self, **kw): return (200, {}, { 'namespace': 'OS::Storage::Capabilities::fake', 'vendor_name': 'OpenStack', 'volume_backend_name': 'lvm', 'pool_name': 'pool', 'storage_protocol': 'iSCSI', 'properties': { 'compression': { 'title': 'Compression', 'description': 'Enables compression.', 'type': 'boolean'}, } } ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_attachments.py0000664000175000017500000000341100000000000026566 0ustar00zuulzuul00000000000000# Copyright (C) 2016 EMC Corporation. # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes class AttachmentsTest(utils.TestCase): def test_create_attachment(self): cs = fakes.FakeClient(api_versions.APIVersion('3.27')) att = cs.attachments.create( 'e84fda45-4de4-4ce4-8f39-fc9d3b0aa05e', {}, '557ad76c-ce54-40a3-9e91-c40d21665cc3', 'null') cs.assert_called('POST', '/attachments') self.assertEqual(fakes.fake_attachment['attachment'], att) def test_create_attachment_without_instance_uuid(self): cs = fakes.FakeClient(api_versions.APIVersion('3.27')) att = cs.attachments.create( 'e84fda45-4de4-4ce4-8f39-fc9d3b0aa05e', {}, None, 'null') cs.assert_called('POST', '/attachments') self.assertEqual( fakes.fake_attachment_without_instance_id['attachment'], att) def test_complete_attachment(self): cs = fakes.FakeClient(api_versions.APIVersion('3.44')) att = cs.attachments.complete('a232e9ae') self.assertTrue(att.ok) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_auth.py0000664000175000017500000003052000000000000025215 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json from unittest import mock import requests from cinderclient import exceptions from cinderclient.tests.unit import utils from cinderclient.v3 import client class AuthenticateAgainstKeystoneTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", "http://localhost:8776/v2", service_type='volumev2') resp = { "access": { "token": { "expires": "2014-11-01T03:32:15-05:00", "id": "FAKE_ID", }, "serviceCatalog": [ { "type": "volumev2", "endpoints": [ { "region": "RegionOne", "adminURL": "http://localhost:8776/v2", "internalURL": "http://localhost:8776/v2", "publicURL": "http://localhost:8776/v2", }, ], }, ], }, } auth_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(resp), }) mock_request = mock.Mock(return_value=(auth_response)) @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { 'User-Agent': cs.client.USER_AGENT, 'Content-Type': 'application/json', 'Accept': 'application/json', } body = { 'auth': { 'passwordCredentials': { 'username': cs.client.user, 'password': cs.client.password, }, 'tenantName': cs.client.projectid, }, } token_url = cs.client.auth_url + "/tokens" mock_request.assert_called_with( "POST", token_url, headers=headers, data=json.dumps(body), allow_redirects=True, **self.TEST_REQUEST_BASE) endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] public_url = endpoints[0]["publicURL"].rstrip('/') self.assertEqual(public_url, cs.client.management_url) token_id = resp["access"]["token"]["id"] self.assertEqual(token_id, cs.client.auth_token) test_auth_call() def test_authenticate_tenant_id(self): cs = client.Client("username", "password", auth_url="http://localhost:8776/v2", tenant_id='tenant_id', service_type='volumev2') resp = { "access": { "token": { "expires": "2014-11-01T03:32:15-05:00", "id": "FAKE_ID", "tenant": { "description": None, "enabled": True, "id": "tenant_id", "name": "demo" } # tenant associated with token }, "serviceCatalog": [ { "type": 'volumev2', "endpoints": [ { "region": "RegionOne", "adminURL": "http://localhost:8776/v2", "internalURL": "http://localhost:8776/v2", "publicURL": "http://localhost:8776/v2", }, ], }, ], }, } auth_response = utils.TestResponse({ "status_code": 200, "text": json.dumps(resp), }) mock_request = mock.Mock(return_value=(auth_response)) @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { 'User-Agent': cs.client.USER_AGENT, 'Content-Type': 'application/json', 'Accept': 'application/json', } body = { 'auth': { 'passwordCredentials': { 'username': cs.client.user, 'password': cs.client.password, }, 'tenantId': cs.client.tenant_id, }, } token_url = cs.client.auth_url + "/tokens" mock_request.assert_called_with( "POST", token_url, headers=headers, data=json.dumps(body), allow_redirects=True, **self.TEST_REQUEST_BASE) endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] public_url = endpoints[0]["publicURL"].rstrip('/') self.assertEqual(public_url, cs.client.management_url) token_id = resp["access"]["token"]["id"] self.assertEqual(token_id, cs.client.auth_token) tenant_id = resp["access"]["token"]["tenant"]["id"] self.assertEqual(tenant_id, cs.client.tenant_id) test_auth_call() def test_authenticate_failure(self): cs = client.Client("username", "password", "project_id", "http://localhost:8776/v2") resp = {"unauthorized": {"message": "Unauthorized", "code": "401"}} auth_response = utils.TestResponse({ "status_code": 401, "text": json.dumps(resp), }) mock_request = mock.Mock(return_value=(auth_response)) @mock.patch.object(requests, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) test_auth_call() def test_auth_redirect(self): cs = client.Client("username", "password", "project_id", "http://localhost:8776/v2", service_type='volumev2') dict_correct_response = { "access": { "token": { "expires": "2014-11-01T03:32:15-05:00", "id": "FAKE_ID", }, "serviceCatalog": [ { "type": "volumev2", "endpoints": [ { "adminURL": "http://localhost:8776/v2", "region": "RegionOne", "internalURL": "http://localhost:8776/v2", "publicURL": "http://localhost:8776/v2/", }, ], }, ], }, } correct_response = json.dumps(dict_correct_response) dict_responses = [ {"headers": {'location': 'http://127.0.0.1:5001'}, "status_code": 305, "text": "Use proxy"}, # Configured on admin port, cinder redirects to v2.0 port. # When trying to connect on it, keystone auth succeed by v1.0 # protocol (through headers) but tokens are being returned in # body (looks like keystone bug). Leaved for compatibility. {"headers": {}, "status_code": 200, "text": correct_response}, {"headers": {}, "status_code": 200, "text": correct_response} ] responses = [(utils.TestResponse(resp)) for resp in dict_responses] def side_effect(*args, **kwargs): return responses.pop(0) mock_request = mock.Mock(side_effect=side_effect) @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { 'User-Agent': cs.client.USER_AGENT, 'Content-Type': 'application/json', 'Accept': 'application/json', } body = { 'auth': { 'passwordCredentials': { 'username': cs.client.user, 'password': cs.client.password, }, 'tenantName': cs.client.projectid, }, } token_url = cs.client.auth_url + "/tokens" mock_request.assert_called_with( "POST", token_url, headers=headers, data=json.dumps(body), allow_redirects=True, **self.TEST_REQUEST_BASE) resp = dict_correct_response endpoints = resp["access"]["serviceCatalog"][0]['endpoints'] public_url = endpoints[0]["publicURL"].rstrip('/') self.assertEqual(public_url, cs.client.management_url) token_id = resp["access"]["token"]["id"] self.assertEqual(token_id, cs.client.auth_token) test_auth_call() class AuthenticationTests(utils.TestCase): def test_authenticate_success(self): cs = client.Client("username", "password", "project_id", "auth_url") management_url = 'https://localhost/v2.1/443470' auth_response = utils.TestResponse({ 'status_code': 204, 'headers': { 'x-server-management-url': management_url, 'x-auth-token': '1b751d74-de0c-46ae-84f0-915744b582d1', }, }) mock_request = mock.Mock(return_value=(auth_response)) @mock.patch.object(requests, "request", mock_request) def test_auth_call(): cs.client.authenticate() headers = { 'Accept': 'application/json', 'X-Auth-User': 'username', 'X-Auth-Key': 'password', 'X-Auth-Project-Id': 'project_id', 'User-Agent': cs.client.USER_AGENT } mock_request.assert_called_with( "GET", cs.client.auth_url, headers=headers, **self.TEST_REQUEST_BASE) self.assertEqual(auth_response.headers['x-server-management-url'], cs.client.management_url) self.assertEqual(auth_response.headers['x-auth-token'], cs.client.auth_token) test_auth_call() def test_authenticate_failure(self): cs = client.Client("username", "password", "project_id", "auth_url") auth_response = utils.TestResponse({"status_code": 401}) mock_request = mock.Mock(return_value=(auth_response)) @mock.patch.object(requests, "request", mock_request) def test_auth_call(): self.assertRaises(exceptions.Unauthorized, cs.client.authenticate) test_auth_call() def test_auth_automatic(self): cs = client.Client("username", "password", "project_id", "auth_url") http_client = cs.client http_client.management_url = '' mock_request = mock.Mock(return_value=(None, None)) @mock.patch.object(http_client, 'request', mock_request) @mock.patch.object(http_client, 'authenticate') def test_auth_call(m): http_client.get('/') self.assertTrue(m.called) self.assertTrue(mock_request.called) test_auth_call() def test_auth_manual(self): cs = client.Client("username", "password", "project_id", "auth_url") @mock.patch.object(cs.client, 'authenticate') def test_auth_call(m): cs.authenticate() self.assertTrue(m.called) test_auth_call() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_availability_zone.py0000664000175000017500000000624400000000000027767 0ustar00zuulzuul00000000000000# Copyright 2011-2013 OpenStack Foundation # Copyright 2013 IBM Corp. # 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 cinderclient.v3 import availability_zones from cinderclient.v3 import shell from cinderclient.tests.unit.fixture_data import availability_zones as azfixture # noqa from cinderclient.tests.unit.fixture_data import client from cinderclient.tests.unit import utils class AvailabilityZoneTest(utils.FixturedTestCase): client_fixture_class = client.V3 data_fixture_class = azfixture.Fixture def _assertZone(self, zone, name, status): self.assertEqual(name, zone.zoneName) self.assertEqual(status, zone.zoneState) def test_list_availability_zone(self): zones = self.cs.availability_zones.list(detailed=False) self.assert_called('GET', '/os-availability-zone') self._assert_request_id(zones) for zone in zones: self.assertIsInstance(zone, availability_zones.AvailabilityZone) self.assertEqual(2, len(zones)) l0 = ['zone-1', 'available'] l1 = ['zone-2', 'not available'] z0 = shell.treeizeAvailabilityZone(zones[0]) z1 = shell.treeizeAvailabilityZone(zones[1]) self.assertEqual((1, 1), (len(z0), len(z1))) self._assertZone(z0[0], l0[0], l0[1]) self._assertZone(z1[0], l1[0], l1[1]) def test_detail_availability_zone(self): zones = self.cs.availability_zones.list(detailed=True) self.assert_called('GET', '/os-availability-zone/detail') self._assert_request_id(zones) for zone in zones: self.assertIsInstance(zone, availability_zones.AvailabilityZone) self.assertEqual(3, len(zones)) l0 = ['zone-1', 'available'] l1 = ['|- fake_host-1', ''] l2 = ['| |- cinder-volume', 'enabled :-) 2012-12-26 14:45:25'] l3 = ['internal', 'available'] l4 = ['|- fake_host-1', ''] l5 = ['| |- cinder-sched', 'enabled :-) 2012-12-26 14:45:24'] l6 = ['zone-2', 'not available'] z0 = shell.treeizeAvailabilityZone(zones[0]) z1 = shell.treeizeAvailabilityZone(zones[1]) z2 = shell.treeizeAvailabilityZone(zones[2]) self.assertEqual((3, 3, 1), (len(z0), len(z1), len(z2))) self._assertZone(z0[0], l0[0], l0[1]) self._assertZone(z0[1], l1[0], l1[1]) self._assertZone(z0[2], l2[0], l2[1]) self._assertZone(z1[0], l3[0], l3[1]) self._assertZone(z1[1], l4[0], l4[1]) self._assertZone(z1[2], l5[0], l5[1]) self._assertZone(z2[0], l6[0], l6[1]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_capabilities.py0000664000175000017500000000373100000000000026711 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Hitachi Data Systems, 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 cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3.capabilities import Capabilities cs = fakes.FakeClient(api_versions.APIVersion('3.0')) FAKE_CAPABILITY = { 'namespace': 'OS::Storage::Capabilities::fake', 'vendor_name': 'OpenStack', 'volume_backend_name': 'lvm', 'pool_name': 'pool', 'storage_protocol': 'iSCSI', 'properties': { 'compression': { 'title': 'Compression', 'description': 'Enables compression.', 'type': 'boolean', }, }, } class CapabilitiesTest(utils.TestCase): def test_get_capabilities(self): capabilities = cs.capabilities.get('host') cs.assert_called('GET', '/capabilities/host') self.assertEqual(FAKE_CAPABILITY, capabilities._info) self._assert_request_id(capabilities) def test___repr__(self): """ Unit test for Capabilities.__repr__ Verify that Capabilities object can be printed. """ cap = Capabilities(None, FAKE_CAPABILITY) self.assertEqual( "" % FAKE_CAPABILITY['namespace'], repr(cap)) def test__repr__when_empty(self): cap = Capabilities(None, {}) self.assertEqual( "", repr(cap)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_cgsnapshots.py0000664000175000017500000000721700000000000026617 0ustar00zuulzuul00000000000000# Copyright (C) 2012 - 2014 EMC Corporation. # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes cs = fakes.FakeClient(api_versions.APIVersion('3.0')) class cgsnapshotsTest(utils.TestCase): def test_delete_cgsnapshot(self): v = cs.cgsnapshots.list()[0] vol = v.delete() self._assert_request_id(vol) cs.assert_called('DELETE', '/cgsnapshots/1234') vol = cs.cgsnapshots.delete('1234') cs.assert_called('DELETE', '/cgsnapshots/1234') self._assert_request_id(vol) vol = cs.cgsnapshots.delete(v) cs.assert_called('DELETE', '/cgsnapshots/1234') self._assert_request_id(vol) def test_create_cgsnapshot(self): vol = cs.cgsnapshots.create('cgsnap') cs.assert_called('POST', '/cgsnapshots') self._assert_request_id(vol) def test_create_cgsnapshot_with_cg_id(self): vol = cs.cgsnapshots.create('1234') expected = {'cgsnapshot': {'status': 'creating', 'description': None, 'user_id': None, 'name': None, 'consistencygroup_id': '1234', 'project_id': None}} cs.assert_called('POST', '/cgsnapshots', body=expected) self._assert_request_id(vol) def test_update_cgsnapshot(self): v = cs.cgsnapshots.list()[0] expected = {'cgsnapshot': {'name': 'cgs2'}} vol = v.update(name='cgs2') cs.assert_called('PUT', '/cgsnapshots/1234', body=expected) self._assert_request_id(vol) vol = cs.cgsnapshots.update('1234', name='cgs2') cs.assert_called('PUT', '/cgsnapshots/1234', body=expected) self._assert_request_id(vol) vol = cs.cgsnapshots.update(v, name='cgs2') cs.assert_called('PUT', '/cgsnapshots/1234', body=expected) self._assert_request_id(vol) def test_update_cgsnapshot_no_props(self): cs.cgsnapshots.update('1234') def test_list_cgsnapshot(self): lst = cs.cgsnapshots.list() cs.assert_called('GET', '/cgsnapshots/detail') self._assert_request_id(lst) def test_list_cgsnapshot_detailed_false(self): lst = cs.cgsnapshots.list(detailed=False) cs.assert_called('GET', '/cgsnapshots') self._assert_request_id(lst) def test_list_cgsnapshot_with_search_opts(self): lst = cs.cgsnapshots.list(search_opts={'foo': 'bar'}) cs.assert_called('GET', '/cgsnapshots/detail?foo=bar') self._assert_request_id(lst) def test_list_cgsnapshot_with_empty_search_opt(self): lst = cs.cgsnapshots.list(search_opts={'foo': 'bar', '123': None}) cs.assert_called('GET', '/cgsnapshots/detail?foo=bar') self._assert_request_id(lst) def test_get_cgsnapshot(self): cgsnapshot_id = '1234' vol = cs.cgsnapshots.get(cgsnapshot_id) cs.assert_called('GET', '/cgsnapshots/%s' % cgsnapshot_id) self._assert_request_id(vol) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_clusters.py0000664000175000017500000001266100000000000026126 0ustar00zuulzuul00000000000000# Copyright (c) 2016 Red Hat Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ddt from cinderclient import api_versions from cinderclient import exceptions as exc from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.7')) @ddt.ddt class ClusterTest(utils.TestCase): def _check_fields_present(self, clusters, detailed=False): expected_keys = {'name', 'binary', 'state', 'status'} if detailed: expected_keys.update(('num_hosts', 'num_down_hosts', 'last_heartbeat', 'disabled_reason', 'created_at', 'updated_at')) for cluster in clusters: self.assertEqual(expected_keys, set(cluster.to_dict())) def _assert_call(self, base_url, detailed, params=None, method='GET', body=None): url = base_url if detailed: url += '/detail' if params: url += '?' + params if body: cs.assert_called(method, url, body) else: cs.assert_called(method, url) @ddt.data(True, False) def test_clusters_list(self, detailed): lst = cs.clusters.list(detailed=detailed) self._assert_call('/clusters', detailed) self.assertEqual(3, len(lst)) self._assert_request_id(lst) self._check_fields_present(lst, detailed) @ddt.data(True, False) def test_clusters_list_pre_version(self, detailed): pre_cs = fakes.FakeClient(api_version= api_versions.APIVersion('3.6')) self.assertRaises(exc.VersionNotFoundForAPIMethod, pre_cs.clusters.list, detailed=detailed) @ddt.data(True, False) def test_cluster_list_name(self, detailed): lst = cs.clusters.list(name='cluster1@lvmdriver-1', detailed=detailed) self._assert_call('/clusters', detailed, 'name=cluster1@lvmdriver-1') self.assertEqual(1, len(lst)) self._assert_request_id(lst) self._check_fields_present(lst, detailed) @ddt.data(True, False) def test_clusters_list_binary(self, detailed): lst = cs.clusters.list(binary='cinder-volume', detailed=detailed) self._assert_call('/clusters', detailed, 'binary=cinder-volume') self.assertEqual(2, len(lst)) self._assert_request_id(lst) self._check_fields_present(lst, detailed) @ddt.data(True, False) def test_clusters_list_is_up(self, detailed): lst = cs.clusters.list(is_up=True, detailed=detailed) self._assert_call('/clusters', detailed, 'is_up=True') self.assertEqual(2, len(lst)) self._assert_request_id(lst) self._check_fields_present(lst, detailed) @ddt.data(True, False) def test_clusters_list_disabled(self, detailed): lst = cs.clusters.list(disabled=True, detailed=detailed) self._assert_call('/clusters', detailed, 'disabled=True') self.assertEqual(1, len(lst)) self._assert_request_id(lst) self._check_fields_present(lst, detailed) @ddt.data(True, False) def test_clusters_list_num_hosts(self, detailed): lst = cs.clusters.list(num_hosts=1, detailed=detailed) self._assert_call('/clusters', detailed, 'num_hosts=1') self.assertEqual(1, len(lst)) self._assert_request_id(lst) self._check_fields_present(lst, detailed) @ddt.data(True, False) def test_clusters_list_num_down_hosts(self, detailed): lst = cs.clusters.list(num_down_hosts=2, detailed=detailed) self._assert_call('/clusters', detailed, 'num_down_hosts=2') self.assertEqual(2, len(lst)) self._assert_request_id(lst) self._check_fields_present(lst, detailed) def test_cluster_show(self): result = cs.clusters.show('1') self._assert_call('/clusters/1', False) self._assert_request_id(result) self._check_fields_present([result], True) def test_cluster_enable(self): body = {'binary': 'cinder-volume', 'name': 'cluster@lvmdriver-1'} result = cs.clusters.update(body['name'], body['binary'], False, disabled_reason='is ignored') self._assert_call('/clusters/enable', False, method='PUT', body=body) self._assert_request_id(result) self._check_fields_present([result], False) def test_cluster_disable(self): body = {'binary': 'cinder-volume', 'name': 'cluster@lvmdriver-1', 'disabled_reason': 'is passed'} result = cs.clusters.update(body['name'], body['binary'], True, body['disabled_reason']) self._assert_call('/clusters/disable', False, method='PUT', body=body) self._assert_request_id(result) self._check_fields_present([result], False) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_consistencygroups.py0000664000175000017500000001674500000000000030072 0ustar00zuulzuul00000000000000# Copyright (C) 2012 - 2014 EMC Corporation. # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes cs = fakes.FakeClient(api_versions.APIVersion('3.0')) class ConsistencygroupsTest(utils.TestCase): def test_delete_consistencygroup(self): v = cs.consistencygroups.list()[0] vol = v.delete(force='True') self._assert_request_id(vol) cs.assert_called('POST', '/consistencygroups/1234/delete') vol = cs.consistencygroups.delete('1234', force=True) self._assert_request_id(vol) cs.assert_called('POST', '/consistencygroups/1234/delete') vol = cs.consistencygroups.delete(v, force=True) self._assert_request_id(vol) cs.assert_called('POST', '/consistencygroups/1234/delete') def test_create_consistencygroup(self): vol = cs.consistencygroups.create('type1,type2', 'cg') cs.assert_called('POST', '/consistencygroups') self._assert_request_id(vol) def test_create_consistencygroup_with_volume_types(self): vol = cs.consistencygroups.create('type1,type2', 'cg') expected = {'consistencygroup': {'status': 'creating', 'description': None, 'availability_zone': None, 'user_id': None, 'name': 'cg', 'volume_types': 'type1,type2', 'project_id': None}} cs.assert_called('POST', '/consistencygroups', body=expected) self._assert_request_id(vol) def test_update_consistencygroup_name(self): v = cs.consistencygroups.list()[0] expected = {'consistencygroup': {'name': 'cg2'}} vol = v.update(name='cg2') cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) vol = cs.consistencygroups.update('1234', name='cg2') cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) vol = cs.consistencygroups.update(v, name='cg2') cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) def test_update_consistencygroup_description(self): v = cs.consistencygroups.list()[0] expected = {'consistencygroup': {'description': 'cg2 desc'}} vol = v.update(description='cg2 desc') cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) vol = cs.consistencygroups.update('1234', description='cg2 desc') cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) vol = cs.consistencygroups.update(v, description='cg2 desc') cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) def test_update_consistencygroup_add_volumes(self): v = cs.consistencygroups.list()[0] uuids = 'uuid1,uuid2' expected = {'consistencygroup': {'add_volumes': uuids}} vol = v.update(add_volumes=uuids) cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) vol = cs.consistencygroups.update('1234', add_volumes=uuids) cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) vol = cs.consistencygroups.update(v, add_volumes=uuids) cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) def test_update_consistencygroup_remove_volumes(self): v = cs.consistencygroups.list()[0] uuids = 'uuid3,uuid4' expected = {'consistencygroup': {'remove_volumes': uuids}} vol = v.update(remove_volumes=uuids) cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) vol = cs.consistencygroups.update('1234', remove_volumes=uuids) cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) vol = cs.consistencygroups.update(v, remove_volumes=uuids) cs.assert_called('PUT', '/consistencygroups/1234', body=expected) self._assert_request_id(vol) def test_update_consistencygroup_none(self): self.assertIsNone(cs.consistencygroups.update('1234')) def test_update_consistencygroup_no_props(self): cs.consistencygroups.update('1234') def test_create_consistencygroup_from_src_snap(self): vol = cs.consistencygroups.create_from_src('5678', None, name='cg') expected = { 'consistencygroup-from-src': { 'status': 'creating', 'description': None, 'user_id': None, 'name': 'cg', 'cgsnapshot_id': '5678', 'project_id': None, 'source_cgid': None } } cs.assert_called('POST', '/consistencygroups/create_from_src', body=expected) self._assert_request_id(vol) def test_create_consistencygroup_from_src_cg(self): vol = cs.consistencygroups.create_from_src(None, '5678', name='cg') expected = { 'consistencygroup-from-src': { 'status': 'creating', 'description': None, 'user_id': None, 'name': 'cg', 'source_cgid': '5678', 'project_id': None, 'cgsnapshot_id': None } } cs.assert_called('POST', '/consistencygroups/create_from_src', body=expected) self._assert_request_id(vol) def test_list_consistencygroup(self): lst = cs.consistencygroups.list() cs.assert_called('GET', '/consistencygroups/detail') self._assert_request_id(lst) def test_list_consistencygroup_detailed_false(self): lst = cs.consistencygroups.list(detailed=False) cs.assert_called('GET', '/consistencygroups') self._assert_request_id(lst) def test_list_consistencygroup_with_search_opts(self): lst = cs.consistencygroups.list(search_opts={'foo': 'bar'}) cs.assert_called('GET', '/consistencygroups/detail?foo=bar') self._assert_request_id(lst) def test_list_consistencygroup_with_empty_search_opt(self): lst = cs.consistencygroups.list( search_opts={'foo': 'bar', 'abc': None} ) cs.assert_called('GET', '/consistencygroups/detail?foo=bar') self._assert_request_id(lst) def test_get_consistencygroup(self): consistencygroup_id = '1234' vol = cs.consistencygroups.get(consistencygroup_id) cs.assert_called('GET', '/consistencygroups/%s' % consistencygroup_id) self._assert_request_id(vol) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_default_types.py0000664000175000017500000000341300000000000027125 0ustar00zuulzuul00000000000000# 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 cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes defaults = fakes.FakeClient(api_versions.APIVersion('3.62')) class VolumeTypeDefaultTest(utils.TestCase): def test_set(self): defaults.default_types.create('4c298f16-e339-4c80-b934-6cbfcb7525a0', '629632e7-99d2-4c40-9ae3-106fa3b1c9b7') defaults.assert_called( 'PUT', 'v3/default-types/629632e7-99d2-4c40-9ae3-106fa3b1c9b7', body={'default_type': {'volume_type': '4c298f16-e339-4c80-b934-6cbfcb7525a0'}} ) def test_get(self): defaults.default_types.list('629632e7-99d2-4c40-9ae3-106fa3b1c9b7') defaults.assert_called( 'GET', 'v3/default-types/629632e7-99d2-4c40-9ae3-106fa3b1c9b7') def test_get_all(self): defaults.default_types.list() defaults.assert_called( 'GET', 'v3/default-types') def test_unset(self): defaults.default_types.delete('629632e7-99d2-4c40-9ae3-106fa3b1c9b7') defaults.assert_called( 'DELETE', 'v3/default-types/629632e7-99d2-4c40-9ae3-106fa3b1c9b7') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_group_snapshots.py0000664000175000017500000000734700000000000027525 0ustar00zuulzuul00000000000000# Copyright (C) 2016 EMC Corporation. # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ddt from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes cs = fakes.FakeClient(api_versions.APIVersion('3.14')) @ddt.ddt class GroupSnapshotsTest(utils.TestCase): def test_delete_group_snapshot(self): s1 = cs.group_snapshots.list()[0] snap = s1.delete() self._assert_request_id(snap) cs.assert_called('DELETE', '/group_snapshots/1234') snap = cs.group_snapshots.delete('1234') cs.assert_called('DELETE', '/group_snapshots/1234') self._assert_request_id(snap) snap = cs.group_snapshots.delete(s1) cs.assert_called('DELETE', '/group_snapshots/1234') self._assert_request_id(snap) def test_create_group_snapshot(self): snap = cs.group_snapshots.create('group_snap') cs.assert_called('POST', '/group_snapshots') self._assert_request_id(snap) def test_create_group_snapshot_with_group_id(self): snap = cs.group_snapshots.create('1234') expected = {'group_snapshot': {'description': None, 'name': None, 'group_id': '1234'}} cs.assert_called('POST', '/group_snapshots', body=expected) self._assert_request_id(snap) def test_update_group_snapshot(self): s1 = cs.group_snapshots.list()[0] expected = {'group_snapshot': {'name': 'grp_snap2'}} snap = s1.update(name='grp_snap2') cs.assert_called('PUT', '/group_snapshots/1234', body=expected) self._assert_request_id(snap) snap = cs.group_snapshots.update('1234', name='grp_snap2') cs.assert_called('PUT', '/group_snapshots/1234', body=expected) self._assert_request_id(snap) snap = cs.group_snapshots.update(s1, name='grp_snap2') cs.assert_called('PUT', '/group_snapshots/1234', body=expected) self._assert_request_id(snap) def test_update_group_snapshot_no_props(self): ret = cs.group_snapshots.update('1234') self.assertIsNone(ret) def test_list_group_snapshot(self): lst = cs.group_snapshots.list() cs.assert_called('GET', '/group_snapshots/detail') self._assert_request_id(lst) @ddt.data( {'detailed': True, 'url': '/group_snapshots/detail'}, {'detailed': False, 'url': '/group_snapshots'} ) @ddt.unpack def test_list_group_snapshot_detailed(self, detailed, url): lst = cs.group_snapshots.list(detailed=detailed) cs.assert_called('GET', url) self._assert_request_id(lst) @ddt.data( {'foo': 'bar'}, {'foo': 'bar', '123': None} ) def test_list_group_snapshot_with_search_opts(self, opts): lst = cs.group_snapshots.list(search_opts=opts) cs.assert_called('GET', '/group_snapshots/detail?foo=bar') self._assert_request_id(lst) def test_get_group_snapshot(self): group_snapshot_id = '1234' snap = cs.group_snapshots.get(group_snapshot_id) cs.assert_called('GET', '/group_snapshots/%s' % group_snapshot_id) self._assert_request_id(snap) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_group_types.py0000664000175000017500000001030000000000000026626 0ustar00zuulzuul00000000000000# Copyright (c) 2016 EMC Corporation # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import api_versions from cinderclient import exceptions as exc from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3 import group_types cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.11')) pre_cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.10')) class GroupTypesTest(utils.TestCase): def test_list_group_types(self): tl = cs.group_types.list() cs.assert_called('GET', '/group_types?is_public=None') self._assert_request_id(tl) for t in tl: self.assertIsInstance(t, group_types.GroupType) def test_list_group_types_pre_version(self): self.assertRaises(exc.VersionNotFoundForAPIMethod, pre_cs.group_types.list) def test_list_group_types_not_public(self): t1 = cs.group_types.list(is_public=None) cs.assert_called('GET', '/group_types?is_public=None') self._assert_request_id(t1) def test_create(self): t = cs.group_types.create('test-type-3', 'test-type-3-desc') cs.assert_called('POST', '/group_types', {'group_type': { 'name': 'test-type-3', 'description': 'test-type-3-desc', 'is_public': True }}) self.assertIsInstance(t, group_types.GroupType) self._assert_request_id(t) def test_create_non_public(self): t = cs.group_types.create('test-type-3', 'test-type-3-desc', False) cs.assert_called('POST', '/group_types', {'group_type': { 'name': 'test-type-3', 'description': 'test-type-3-desc', 'is_public': False }}) self.assertIsInstance(t, group_types.GroupType) self._assert_request_id(t) def test_update(self): t = cs.group_types.update('1', 'test_type_1', 'test_desc_1', False) cs.assert_called('PUT', '/group_types/1', {'group_type': {'name': 'test_type_1', 'description': 'test_desc_1', 'is_public': False}}) self.assertIsInstance(t, group_types.GroupType) self._assert_request_id(t) def test_get(self): t = cs.group_types.get('1') cs.assert_called('GET', '/group_types/1') self.assertIsInstance(t, group_types.GroupType) self._assert_request_id(t) def test_default(self): t = cs.group_types.default() cs.assert_called('GET', '/group_types/default') self.assertIsInstance(t, group_types.GroupType) self._assert_request_id(t) def test_set_key(self): t = cs.group_types.get(1) res = t.set_keys({'k': 'v'}) cs.assert_called('POST', '/group_types/1/group_specs', {'group_specs': {'k': 'v'}}) self._assert_request_id(res) def test_set_key_pre_version(self): t = group_types.GroupType(pre_cs, {'id': 1}) self.assertRaises(exc.VersionNotFoundForAPIMethod, t.set_keys, {'k': 'v'}) def test_unset_keys(self): t = cs.group_types.get(1) res = t.unset_keys(['k']) cs.assert_called('DELETE', '/group_types/1/group_specs/k') self._assert_request_id(res) def test_delete(self): t = cs.group_types.delete(1) cs.assert_called('DELETE', '/group_types/1') self._assert_request_id(t) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_groups.py0000664000175000017500000002103100000000000025570 0ustar00zuulzuul00000000000000# Copyright (C) 2016 EMC Corporation. # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ddt from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes cs = fakes.FakeClient(api_versions.APIVersion('3.13')) @ddt.ddt class GroupsTest(utils.TestCase): def test_delete_group(self): expected = {'delete': {'delete-volumes': True}} v = cs.groups.list()[0] grp = v.delete(delete_volumes=True) self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) grp = cs.groups.delete('1234', delete_volumes=True) self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) grp = cs.groups.delete(v, delete_volumes=True) self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) def test_create_group(self): grp = cs.groups.create('my_group_type', 'type1,type2', name='group') cs.assert_called('POST', '/groups') self._assert_request_id(grp) def test_create_group_with_volume_types(self): grp = cs.groups.create('my_group_type', 'type1,type2', name='group') expected = {'group': {'description': None, 'availability_zone': None, 'name': 'group', 'group_type': 'my_group_type', 'volume_types': ['type1', 'type2']}} cs.assert_called('POST', '/groups', body=expected) self._assert_request_id(grp) @ddt.data( {'name': 'group2', 'desc': None, 'add': None, 'remove': None}, {'name': None, 'desc': 'group2 desc', 'add': None, 'remove': None}, {'name': None, 'desc': None, 'add': 'uuid1,uuid2', 'remove': None}, {'name': None, 'desc': None, 'add': None, 'remove': 'uuid3,uuid4'}, ) @ddt.unpack def test_update_group_name(self, name, desc, add, remove): v = cs.groups.list()[0] expected = {'group': {'name': name, 'description': desc, 'add_volumes': add, 'remove_volumes': remove}} grp = v.update(name=name, description=desc, add_volumes=add, remove_volumes=remove) cs.assert_called('PUT', '/groups/1234', body=expected) self._assert_request_id(grp) grp = cs.groups.update('1234', name=name, description=desc, add_volumes=add, remove_volumes=remove) cs.assert_called('PUT', '/groups/1234', body=expected) self._assert_request_id(grp) grp = cs.groups.update(v, name=name, description=desc, add_volumes=add, remove_volumes=remove) cs.assert_called('PUT', '/groups/1234', body=expected) self._assert_request_id(grp) def test_update_group_none(self): self.assertIsNone(cs.groups.update('1234')) def test_update_group_no_props(self): cs.groups.update('1234') def test_list_group(self): lst = cs.groups.list() cs.assert_called('GET', '/groups/detail') self._assert_request_id(lst) def test_list_group_detailed_false(self): lst = cs.groups.list(detailed=False) cs.assert_called('GET', '/groups') self._assert_request_id(lst) def test_list_group_with_search_opts(self): lst = cs.groups.list(search_opts={'foo': 'bar'}) cs.assert_called('GET', '/groups/detail?foo=bar') self._assert_request_id(lst) def test_list_group_with_volume(self): lst = cs.groups.list(list_volume=True) cs.assert_called('GET', '/groups/detail?list_volume=True') self._assert_request_id(lst) def test_list_group_with_empty_search_opt(self): lst = cs.groups.list( search_opts={'foo': 'bar', 'abc': None} ) cs.assert_called('GET', '/groups/detail?foo=bar') self._assert_request_id(lst) def test_get_group(self): group_id = '1234' grp = cs.groups.get(group_id) cs.assert_called('GET', '/groups/%s' % group_id) self._assert_request_id(grp) def test_get_group_with_list_volume(self): group_id = '1234' grp = cs.groups.get(group_id, list_volume=True) cs.assert_called('GET', '/groups/%s?list_volume=True' % group_id) self._assert_request_id(grp) def test_create_group_from_src_snap(self): cs = fakes.FakeClient(api_versions.APIVersion('3.14')) grp = cs.groups.create_from_src('5678', None, name='group') expected = { 'create-from-src': { 'description': None, 'name': 'group', 'group_snapshot_id': '5678' } } cs.assert_called('POST', '/groups/action', body=expected) self._assert_request_id(grp) def test_create_group_from_src_group_(self): cs = fakes.FakeClient(api_versions.APIVersion('3.14')) grp = cs.groups.create_from_src(None, '5678', name='group') expected = { 'create-from-src': { 'description': None, 'name': 'group', 'source_group_id': '5678' } } cs.assert_called('POST', '/groups/action', body=expected) self._assert_request_id(grp) def test_enable_replication_group(self): cs = fakes.FakeClient(api_versions.APIVersion('3.38')) expected = {'enable_replication': {}} g0 = cs.groups.list()[0] grp = g0.enable_replication() self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) grp = cs.groups.enable_replication('1234') self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) grp = cs.groups.enable_replication(g0) self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) def test_disable_replication_group(self): cs = fakes.FakeClient(api_versions.APIVersion('3.38')) expected = {'disable_replication': {}} g0 = cs.groups.list()[0] grp = g0.disable_replication() self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) grp = cs.groups.disable_replication('1234') self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) grp = cs.groups.disable_replication(g0) self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) def test_failover_replication_group(self): cs = fakes.FakeClient(api_versions.APIVersion('3.38')) expected = {'failover_replication': {'allow_attached_volume': False, 'secondary_backend_id': None}} g0 = cs.groups.list()[0] grp = g0.failover_replication() self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) grp = cs.groups.failover_replication('1234') self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) grp = cs.groups.failover_replication(g0) self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) def test_list_replication_targets(self): cs = fakes.FakeClient(api_versions.APIVersion('3.38')) expected = {'list_replication_targets': {}} g0 = cs.groups.list()[0] grp = g0.list_replication_targets() self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) grp = cs.groups.list_replication_targets('1234') self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) grp = cs.groups.list_replication_targets(g0) self._assert_request_id(grp) cs.assert_called('POST', '/groups/1234/action', body=expected) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_limits.py0000664000175000017500000001404700000000000025563 0ustar00zuulzuul00000000000000# Copyright 2014 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock import ddt from cinderclient.tests.unit import utils from cinderclient.v3 import limits REQUEST_ID = 'req-test-request-id' def _get_default_RateLimit(verb="verb1", uri="uri1", regex="regex1", value="value1", remain="remain1", unit="unit1", next_available="next1"): return limits.RateLimit(verb, uri, regex, value, remain, unit, next_available) class TestLimits(utils.TestCase): def test_repr(self): limit = limits.Limits(None, {"foo": "bar"}, resp=REQUEST_ID) self.assertEqual("", repr(limit)) self._assert_request_id(limit) def test_absolute(self): limit = limits.Limits( None, {"absolute": {"name1": "value1", "name2": "value2"}}, resp=REQUEST_ID) l1 = limits.AbsoluteLimit("name1", "value1") l2 = limits.AbsoluteLimit("name2", "value2") for item in limit.absolute: self.assertIn(item, [l1, l2]) self._assert_request_id(limit) def test_rate(self): limit = limits.Limits( None, { "rate": [ { "uri": "uri1", "regex": "regex1", "limit": [ { "verb": "verb1", "value": "value1", "remaining": "remain1", "unit": "unit1", "next-available": "next1", }, ], }, { "uri": "uri2", "regex": "regex2", "limit": [ { "verb": "verb2", "value": "value2", "remaining": "remain2", "unit": "unit2", "next-available": "next2", }, ], }, ], }, resp=REQUEST_ID) l1 = limits.RateLimit("verb1", "uri1", "regex1", "value1", "remain1", "unit1", "next1") l2 = limits.RateLimit("verb2", "uri2", "regex2", "value2", "remain2", "unit2", "next2") for item in limit.rate: self.assertIn(item, [l1, l2]) self._assert_request_id(limit) class TestRateLimit(utils.TestCase): def test_equal(self): l1 = _get_default_RateLimit() l2 = _get_default_RateLimit() self.assertEqual(l1, l2) def test_not_equal_verbs(self): l1 = _get_default_RateLimit() l2 = _get_default_RateLimit(verb="verb2") self.assertNotEqual(l1, l2) def test_not_equal_uris(self): l1 = _get_default_RateLimit() l2 = _get_default_RateLimit(uri="uri2") self.assertNotEqual(l1, l2) def test_not_equal_regexps(self): l1 = _get_default_RateLimit() l2 = _get_default_RateLimit(regex="regex2") self.assertNotEqual(l1, l2) def test_not_equal_values(self): l1 = _get_default_RateLimit() l2 = _get_default_RateLimit(value="value2") self.assertNotEqual(l1, l2) def test_not_equal_remains(self): l1 = _get_default_RateLimit() l2 = _get_default_RateLimit(remain="remain2") self.assertNotEqual(l1, l2) def test_not_equal_units(self): l1 = _get_default_RateLimit() l2 = _get_default_RateLimit(unit="unit2") self.assertNotEqual(l1, l2) def test_not_equal_next_available(self): l1 = _get_default_RateLimit() l2 = _get_default_RateLimit(next_available="next2") self.assertNotEqual(l1, l2) def test_repr(self): l1 = _get_default_RateLimit() self.assertEqual("", repr(l1)) class TestAbsoluteLimit(utils.TestCase): def test_equal(self): l1 = limits.AbsoluteLimit("name1", "value1") l2 = limits.AbsoluteLimit("name1", "value1") self.assertEqual(l1, l2) def test_not_equal_values(self): l1 = limits.AbsoluteLimit("name1", "value1") l2 = limits.AbsoluteLimit("name1", "value2") self.assertNotEqual(l1, l2) def test_not_equal_names(self): l1 = limits.AbsoluteLimit("name1", "value1") l2 = limits.AbsoluteLimit("name2", "value1") self.assertNotEqual(l1, l2) def test_repr(self): l1 = limits.AbsoluteLimit("name1", "value1") self.assertEqual("", repr(l1)) @ddt.ddt class TestLimitsManager(utils.TestCase): @ddt.data(None, 'test') def test_get(self, tenant_id): api = mock.Mock() api.client.get.return_value = ( None, {"limits": {"absolute": {"name1": "value1", }}, "no-limits": {"absolute": {"name2": "value2", }}}) l1 = limits.AbsoluteLimit("name1", "value1") limitsManager = limits.LimitsManager(api) lim = limitsManager.get(tenant_id) query_str = '' if tenant_id: query_str = '?tenant_id=%s' % tenant_id api.client.get.assert_called_once_with('/limits%s' % query_str) self.assertIsInstance(lim, limits.Limits) for limit in lim.absolute: self.assertEqual(l1, limit) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_messages.py0000664000175000017500000000463100000000000026067 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from urllib import parse import ddt from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes @ddt.ddt class MessagesTest(utils.TestCase): def test_list_messages(self): cs = fakes.FakeClient(api_versions.APIVersion('3.3')) cs.messages.list() cs.assert_called('GET', '/messages') @ddt.data('id', 'id:asc', 'id:desc', 'resource_type', 'event_id', 'resource_uuid', 'message_level', 'guaranteed_until', 'request_id') def test_list_messages_with_sort(self, sort_string): cs = fakes.FakeClient(api_versions.APIVersion('3.5')) cs.messages.list(sort=sort_string) cs.assert_called('GET', '/messages?sort=%s' % parse.quote(sort_string)) @ddt.data('id', 'resource_type', 'event_id', 'resource_uuid', 'message_level', 'guaranteed_until', 'request_id') def test_list_messages_with_filters(self, filter_string): cs = fakes.FakeClient(api_versions.APIVersion('3.5')) cs.messages.list(search_opts={filter_string: 'value'}) cs.assert_called('GET', '/messages?%s=value' % parse.quote( filter_string)) @ddt.data('fake', 'fake:asc', 'fake:desc') def test_list_messages_with_invalid_sort(self, sort_string): cs = fakes.FakeClient(api_versions.APIVersion('3.5')) self.assertRaises(ValueError, cs.messages.list, sort=sort_string) def test_get_messages(self): cs = fakes.FakeClient(api_versions.APIVersion('3.3')) fake_id = '1234' cs.messages.get(fake_id) cs.assert_called('GET', '/messages/%s' % fake_id) def test_delete_messages(self): cs = fakes.FakeClient(api_versions.APIVersion('3.3')) fake_id = '1234' cs.messages.delete(fake_id) cs.assert_called('DELETE', '/messages/%s' % fake_id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_pools.py0000664000175000017500000000361700000000000025417 0ustar00zuulzuul00000000000000# Copyright (C) 2015 Hewlett-Packard Development Company, L.P. # 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 cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3.pools import Pool cs = fakes.FakeClient(api_versions.APIVersion('3.0')) class PoolsTest(utils.TestCase): def test_get_pool_stats(self): sl = cs.pools.list() cs.assert_called('GET', '/scheduler-stats/get_pools') self._assert_request_id(sl) for s in sl: self.assertIsInstance(s, Pool) self.assertTrue(hasattr(s, "name")) self.assertFalse(hasattr(s, "capabilities")) # basic list should not have volume_backend_name (or any other # entries from capabilities) self.assertFalse(hasattr(s, "volume_backend_name")) def test_get_detail_pool_stats(self): sl = cs.pools.list(detailed=True) self._assert_request_id(sl) cs.assert_called('GET', '/scheduler-stats/get_pools?detail=True') for s in sl: self.assertIsInstance(s, Pool) self.assertTrue(hasattr(s, "name")) self.assertFalse(hasattr(s, "capabilities")) # detail list should have a volume_backend_name (from capabilities) self.assertTrue(hasattr(s, "volume_backend_name")) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_qos.py0000664000175000017500000000663300000000000025066 0ustar00zuulzuul00000000000000# Copyright (C) 2013 eBay 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 cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes cs = fakes.FakeClient(api_versions.APIVersion('3.0')) class QoSSpecsTest(utils.TestCase): def test_create(self): specs = dict(k1='v1', k2='v2') qos = cs.qos_specs.create('qos-name', specs) cs.assert_called('POST', '/qos-specs') self._assert_request_id(qos) def test_get(self): qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' qos = cs.qos_specs.get(qos_id) cs.assert_called('GET', '/qos-specs/%s' % qos_id) self._assert_request_id(qos) def test_list(self): lst = cs.qos_specs.list() cs.assert_called('GET', '/qos-specs') self._assert_request_id(lst) def test_delete(self): qos = cs.qos_specs.delete('1B6B6A04-A927-4AEB-810B-B7BAAD49F57C') cs.assert_called('DELETE', '/qos-specs/1B6B6A04-A927-4AEB-810B-B7BAAD49F57C?' 'force=False') self._assert_request_id(qos) def test_set_keys(self): body = {'qos_specs': dict(k1='v1')} qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' qos = cs.qos_specs.set_keys(qos_id, body) cs.assert_called('PUT', '/qos-specs/%s' % qos_id) self._assert_request_id(qos) def test_unset_keys(self): qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' body = {'keys': ['k1']} qos = cs.qos_specs.unset_keys(qos_id, body) cs.assert_called('PUT', '/qos-specs/%s/delete_keys' % qos_id) self._assert_request_id(qos) def test_get_associations(self): qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' qos = cs.qos_specs.get_associations(qos_id) cs.assert_called('GET', '/qos-specs/%s/associations' % qos_id) self._assert_request_id(qos) def test_associate(self): qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' qos = cs.qos_specs.associate(qos_id, type_id) cs.assert_called('GET', '/qos-specs/%s/associate?vol_type_id=%s' % (qos_id, type_id)) self._assert_request_id(qos) def test_disassociate(self): qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' type_id = '4230B13A-7A37-4E84-B777-EFBA6FCEE4FF' qos = cs.qos_specs.disassociate(qos_id, type_id) cs.assert_called('GET', '/qos-specs/%s/disassociate?vol_type_id=%s' % (qos_id, type_id)) self._assert_request_id(qos) def test_disassociate_all(self): qos_id = '1B6B6A04-A927-4AEB-810B-B7BAAD49F57C' qos = cs.qos_specs.disassociate_all(qos_id) cs.assert_called('GET', '/qos-specs/%s/disassociate_all' % qos_id) self._assert_request_id(qos) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_quota_classes.py0000664000175000017500000000540100000000000027122 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes cs = fakes.FakeClient(api_versions.APIVersion('3.0')) class QuotaClassSetsTest(utils.TestCase): def test_class_quotas_get(self): class_name = 'test' cls = cs.quota_classes.get(class_name) cs.assert_called('GET', '/os-quota-class-sets/%s' % class_name) self._assert_request_id(cls) def test_update_quota(self): q = cs.quota_classes.get('test') q.update(volumes=2, snapshots=2, gigabytes=2000, backups=2, backup_gigabytes=2000, per_volume_gigabytes=100) cs.assert_called('PUT', '/os-quota-class-sets/test') self._assert_request_id(q) def test_refresh_quota(self): q = cs.quota_classes.get('test') q2 = cs.quota_classes.get('test') self.assertEqual(q.volumes, q2.volumes) self.assertEqual(q.snapshots, q2.snapshots) self.assertEqual(q.gigabytes, q2.gigabytes) self.assertEqual(q.backups, q2.backups) self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes) self.assertEqual(q.per_volume_gigabytes, q2.per_volume_gigabytes) q2.volumes = 0 self.assertNotEqual(q.volumes, q2.volumes) q2.snapshots = 0 self.assertNotEqual(q.snapshots, q2.snapshots) q2.gigabytes = 0 self.assertNotEqual(q.gigabytes, q2.gigabytes) q2.backups = 0 self.assertNotEqual(q.backups, q2.backups) q2.backup_gigabytes = 0 self.assertNotEqual(q.backup_gigabytes, q2.backup_gigabytes) q2.per_volume_gigabytes = 0 self.assertNotEqual(q.per_volume_gigabytes, q2.per_volume_gigabytes) q2.get() self.assertEqual(q.volumes, q2.volumes) self.assertEqual(q.snapshots, q2.snapshots) self.assertEqual(q.gigabytes, q2.gigabytes) self.assertEqual(q.backups, q2.backups) self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes) self.assertEqual(q.per_volume_gigabytes, q2.per_volume_gigabytes) self._assert_request_id(q) self._assert_request_id(q2) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_quotas.py0000664000175000017500000000665400000000000025603 0ustar00zuulzuul00000000000000# Copyright (c) 2017 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes cs = fakes.FakeClient(api_versions.APIVersion('3.0')) class QuotaSetsTest(utils.TestCase): def test_tenant_quotas_get(self): tenant_id = 'test' quota = cs.quotas.get(tenant_id) cs.assert_called('GET', '/os-quota-sets/%s?usage=False' % tenant_id) self._assert_request_id(quota) def test_tenant_quotas_defaults(self): tenant_id = 'test' quota = cs.quotas.defaults(tenant_id) cs.assert_called('GET', '/os-quota-sets/%s/defaults' % tenant_id) self._assert_request_id(quota) def test_update_quota(self): q = cs.quotas.get('test') q.update(volumes=2) q.update(snapshots=2) q.update(gigabytes=2000) q.update(backups=2) q.update(backup_gigabytes=2000) q.update(per_volume_gigabytes=100) cs.assert_called('PUT', '/os-quota-sets/test') self._assert_request_id(q) def test_update_quota_with_skip_(self): q = cs.quotas.get('test') q.update(skip_validation=False) cs.assert_called('PUT', '/os-quota-sets/test?skip_validation=False') self._assert_request_id(q) def test_refresh_quota(self): q = cs.quotas.get('test') q2 = cs.quotas.get('test') self.assertEqual(q.volumes, q2.volumes) self.assertEqual(q.snapshots, q2.snapshots) self.assertEqual(q.gigabytes, q2.gigabytes) self.assertEqual(q.backups, q2.backups) self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes) self.assertEqual(q.per_volume_gigabytes, q2.per_volume_gigabytes) q2.volumes = 0 self.assertNotEqual(q.volumes, q2.volumes) q2.snapshots = 0 self.assertNotEqual(q.snapshots, q2.snapshots) q2.gigabytes = 0 self.assertNotEqual(q.gigabytes, q2.gigabytes) q2.backups = 0 self.assertNotEqual(q.backups, q2.backups) q2.backup_gigabytes = 0 self.assertNotEqual(q.backup_gigabytes, q2.backup_gigabytes) q2.per_volume_gigabytes = 0 self.assertNotEqual(q.per_volume_gigabytes, q2.per_volume_gigabytes) q2.get() self.assertEqual(q.volumes, q2.volumes) self.assertEqual(q.snapshots, q2.snapshots) self.assertEqual(q.gigabytes, q2.gigabytes) self.assertEqual(q.backups, q2.backups) self.assertEqual(q.backup_gigabytes, q2.backup_gigabytes) self.assertEqual(q.per_volume_gigabytes, q2.per_volume_gigabytes) self._assert_request_id(q) self._assert_request_id(q2) def test_delete_quota(self): tenant_id = 'test' quota = cs.quotas.delete(tenant_id) cs.assert_called('DELETE', '/os-quota-sets/test') self._assert_request_id(quota) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_resource_filters.py0000664000175000017500000000236400000000000027640 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import ddt from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes cs = fakes.FakeClient(api_versions.APIVersion('3.33')) @ddt.ddt class ResourceFilterTests(utils.TestCase): @ddt.data({'resource': None, 'query_url': None}, {'resource': 'volume', 'query_url': '?resource=volume'}, {'resource': 'group', 'query_url': '?resource=group'}) @ddt.unpack def test_list_resource_filters(self, resource, query_url): cs.resource_filters.list(resource) url = '/resource_filters' if resource is not None: url += query_url cs.assert_called('GET', url) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_services.py0000664000175000017500000001060500000000000026101 0ustar00zuulzuul00000000000000# Copyright (c) 2016 Red Hat Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3 import services class ServicesTest(utils.TestCase): def test_list_services_with_cluster_info(self): cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.7')) services_list = cs.services.list() cs.assert_called('GET', '/os-services') self.assertEqual(3, len(services_list)) for service in services_list: self.assertIsInstance(service, services.Service) # Make sure cluster fields from v3.7 is present and not None self.assertIsNotNone(getattr(service, 'cluster')) self._assert_request_id(services_list) def test_api_version(self): client = fakes.FakeClient(version_header='3.0') svs = client.services.server_api_version() [self.assertIsInstance(s, services.Service) for s in svs] def test_set_log_levels(self): expected = {'level': 'debug', 'binary': 'cinder-api', 'server': 'host1', 'prefix': 'sqlalchemy.'} cs = fakes.FakeClient(version_header='3.32') cs.services.set_log_levels(expected['level'], expected['binary'], expected['server'], expected['prefix']) cs.assert_called('PUT', '/os-services/set-log', body=expected) def test_get_log_levels(self): expected = {'binary': 'cinder-api', 'server': 'host1', 'prefix': 'sqlalchemy.'} cs = fakes.FakeClient(version_header='3.32') result = cs.services.get_log_levels(expected['binary'], expected['server'], expected['prefix']) cs.assert_called('PUT', '/os-services/get-log', body=expected) expected = [services.LogLevel(cs.services, {'binary': 'cinder-api', 'host': 'host1', 'prefix': 'prefix1', 'level': 'DEBUG'}, loaded=True), services.LogLevel(cs.services, {'binary': 'cinder-api', 'host': 'host1', 'prefix': 'prefix2', 'level': 'INFO'}, loaded=True), services.LogLevel(cs.services, {'binary': 'cinder-volume', 'host': 'host@backend#pool', 'prefix': 'prefix3', 'level': 'WARNING'}, loaded=True), services.LogLevel(cs.services, {'binary': 'cinder-volume', 'host': 'host@backend#pool', 'prefix': 'prefix4', 'level': 'ERROR'}, loaded=True)] # Since it will be sorted by the prefix we can compare them directly self.assertListEqual(expected, result) def test_list_services_with_backend_state(self): cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.49')) services_list = cs.services.list() cs.assert_called('GET', '/os-services') self.assertEqual(3, len(services_list)) for service in services_list: self.assertIsInstance(service, services.Service) # Make sure backend_state fields from v3.49 is present and not # None if service.binary == 'cinder-volume': self.assertIsNotNone(getattr(service, 'backend_state', None)) self._assert_request_id(services_list) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_services_base.py0000664000175000017500000000717700000000000027105 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3 import services cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.0')) class ServicesTest(utils.TestCase): """Tests for v3.0 behavior""" def test_list_services(self): svs = cs.services.list() cs.assert_called('GET', '/os-services') self.assertEqual(3, len(svs)) for service in svs: self.assertIsInstance(service, services.Service) # Make sure cluster fields from v3.7 are not there self.assertFalse(hasattr(service, 'cluster')) self._assert_request_id(svs) def test_list_services_with_hostname(self): svs = cs.services.list(host='host2') cs.assert_called('GET', '/os-services?host=host2') self.assertEqual(2, len(svs)) [self.assertIsInstance(s, services.Service) for s in svs] [self.assertEqual('host2', s.host) for s in svs] self._assert_request_id(svs) def test_list_services_with_binary(self): svs = cs.services.list(binary='cinder-volume') cs.assert_called('GET', '/os-services?binary=cinder-volume') self.assertEqual(2, len(svs)) [self.assertIsInstance(s, services.Service) for s in svs] [self.assertEqual('cinder-volume', s.binary) for s in svs] self._assert_request_id(svs) def test_list_services_with_host_binary(self): svs = cs.services.list('host2', 'cinder-volume') cs.assert_called('GET', '/os-services?host=host2&binary=cinder-volume') self.assertEqual(1, len(svs)) [self.assertIsInstance(s, services.Service) for s in svs] [self.assertEqual('host2', s.host) for s in svs] [self.assertEqual('cinder-volume', s.binary) for s in svs] self._assert_request_id(svs) def test_services_enable(self): s = cs.services.enable('host1', 'cinder-volume') values = {"host": "host1", 'binary': 'cinder-volume'} cs.assert_called('PUT', '/os-services/enable', values) self.assertIsInstance(s, services.Service) self.assertEqual('enabled', s.status) self._assert_request_id(s) def test_services_disable(self): s = cs.services.disable('host1', 'cinder-volume') values = {"host": "host1", 'binary': 'cinder-volume'} cs.assert_called('PUT', '/os-services/disable', values) self.assertIsInstance(s, services.Service) self.assertEqual('disabled', s.status) self._assert_request_id(s) def test_services_disable_log_reason(self): s = cs.services.disable_log_reason( 'host1', 'cinder-volume', 'disable bad host') values = {"host": "host1", 'binary': 'cinder-volume', "disabled_reason": "disable bad host"} cs.assert_called('PUT', '/os-services/disable-log-reason', values) self.assertIsInstance(s, services.Service) self.assertEqual('disabled', s.status) self._assert_request_id(s) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_shell.py0000664000175000017500000025130000000000000025364 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # NOTE(geguileo): For v3 we cannot mock any of the following methods # - utils.find_volume # - shell_utils.find_backup # - shell_utils.find_volume_snapshot # - shell_utils.find_group # - shell_utils.find_group_snapshot # because we are caching them in cinderclient.v3.shell:RESET_STATE_RESOURCES # which means that our tests could fail depending on the mocking and loading # order. # # Alternatives are: # - Mock utils.find_resource when we have only 1 call to that method # - Use an auxiliary method that will call original method for irrelevant # calls. Example from test_revert_to_snapshot: # original = client_utils.find_resource # # def find_resource(manager, name_or_id, **kwargs): # if isinstance(manager, volume_snapshots.SnapshotManager): # return volume_snapshots.Snapshot(self, # {'id': '5678', # 'volume_id': '1234'}) # return original(manager, name_or_id, **kwargs) from unittest import mock from urllib import parse import ddt import fixtures from requests_mock.contrib import fixture as requests_mock_fixture import cinderclient from cinderclient import api_versions from cinderclient import base from cinderclient import client from cinderclient import exceptions from cinderclient import shell from cinderclient.tests.unit.fixture_data import keystone_client from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient import utils as cinderclient_utils from cinderclient.v3 import attachments from cinderclient.v3 import volume_snapshots from cinderclient.v3 import volumes @ddt.ddt @mock.patch.object(client, 'Client', fakes.FakeClient) class ShellTest(utils.TestCase): FAKE_ENV = { 'CINDER_USERNAME': 'username', 'CINDER_PASSWORD': 'password', 'CINDER_PROJECT_ID': 'project_id', 'OS_VOLUME_API_VERSION': '3', 'CINDER_URL': keystone_client.BASE_URL, } # Patch os.environ to avoid required auth info. def setUp(self): """Run before each test.""" super(ShellTest, self).setUp() for var in self.FAKE_ENV: self.useFixture(fixtures.EnvironmentVariable(var, self.FAKE_ENV[var])) self.mock_completion() self.shell = shell.OpenStackCinderShell() self.requests = self.useFixture(requests_mock_fixture.Fixture()) self.requests.register_uri( 'GET', keystone_client.BASE_URL, text=keystone_client.keystone_request_callback) self.cs = mock.Mock() def run_command(self, cmd): # Ensure the version negotiation indicates that # all versions are supported with mock.patch('cinderclient.api_versions._get_server_version_range', return_value=(api_versions.APIVersion('3.0'), api_versions.APIVersion('3.99'))): self.shell.main(cmd.split()) def assert_called(self, method, url, body=None, partial_body=None, **kwargs): return self.shell.cs.assert_called(method, url, body, partial_body, **kwargs) def assert_call_contained(self, url_part): self.shell.cs.assert_in_call(url_part) @ddt.data({'resource': None, 'query_url': None}, {'resource': 'volume', 'query_url': '?resource=volume'}, {'resource': 'group', 'query_url': '?resource=group'}) @ddt.unpack def test_list_filters(self, resource, query_url): url = '/resource_filters' if resource is not None: url += query_url self.run_command('--os-volume-api-version 3.33 ' 'list-filters --resource=%s' % resource) else: self.run_command('--os-volume-api-version 3.33 list-filters') self.assert_called('GET', url) @ddt.data( # testcases for list volume {'command': 'list --name=123 --filters name=456', 'expected': '/volumes/detail?name=456'}, {'command': 'list --filters name=123', 'expected': '/volumes/detail?name=123'}, {'command': 'list --filters metadata={key1:value1}', 'expected': '/volumes/detail?metadata=%7B%27key1%27%3A+%27value1%27%7D'}, {'command': 'list --filters name~=456', 'expected': '/volumes/detail?name~=456'}, {'command': u'list --filters name~=Σ', 'expected': '/volumes/detail?name~=%CE%A3'}, {'command': u'list --filters name=abc --filters size=1', 'expected': '/volumes/detail?name=abc&size=1'}, {'command': u'list --filters created_at=lt:2020-01-15T00:00:00', 'expected': '/volumes/detail?created_at=lt%3A2020-01-15T00%3A00%3A00'}, {'command': u'list --filters updated_at=gte:2020-02-01T00:00:00,' u'lt:2020-03-01T00:00:00', 'expected': '/volumes/detail?updated_at=gte%3A2020-02-01T00%3A00%3A00%2C' 'lt%3A2020-03-01T00%3A00%3A00'}, {'command': u'list --filters updated_at=gte:2020-02-01T00:00:00,' u'lt:2020-03-01T00:00:00 --filters created_at=' u'lt:2020-01-15T00:00:00', 'expected': '/volumes/detail?created_at=lt%3A2020-01-15T00%3A00%3A00' '&updated_at=gte%3A2020-02-01T00%3A00%3A00%2C' 'lt%3A2020-03-01T00%3A00%3A00'}, # testcases for list group {'command': 'group-list --filters name=456', 'expected': '/groups/detail?name=456'}, {'command': 'group-list --filters status=available', 'expected': '/groups/detail?status=available'}, {'command': 'group-list --filters name~=456', 'expected': '/groups/detail?name~=456'}, {'command': 'group-list --filters name=abc --filters status=available', 'expected': '/groups/detail?name=abc&status=available'}, # testcases for list group-snapshot {'command': 'group-snapshot-list --status=error --filters status=available', 'expected': '/group_snapshots/detail?status=available'}, {'command': 'group-snapshot-list --filters availability_zone=123', 'expected': '/group_snapshots/detail?availability_zone=123'}, {'command': 'group-snapshot-list --filters status~=available', 'expected': '/group_snapshots/detail?status~=available'}, {'command': 'group-snapshot-list --filters status=available ' '--filters availability_zone=123', 'expected': '/group_snapshots/detail?availability_zone=123&status=available'}, # testcases for list message {'command': 'message-list --event_id=123 --filters event_id=456', 'expected': '/messages?event_id=456'}, {'command': 'message-list --filters request_id=123', 'expected': '/messages?request_id=123'}, {'command': 'message-list --filters request_id~=123', 'expected': '/messages?request_id~=123'}, {'command': 'message-list --filters request_id=123 --filters event_id=456', 'expected': '/messages?event_id=456&request_id=123'}, # testcases for list attachment {'command': 'attachment-list --volume-id=123 --filters volume_id=456', 'expected': '/attachments?volume_id=456'}, {'command': 'attachment-list --filters mountpoint=123', 'expected': '/attachments?mountpoint=123'}, {'command': 'attachment-list --filters volume_id~=456', 'expected': '/attachments?volume_id~=456'}, {'command': 'attachment-list --filters volume_id=123 ' '--filters mountpoint=456', 'expected': '/attachments?mountpoint=456&volume_id=123'}, # testcases for list backup {'command': 'backup-list --volume-id=123 --filters volume_id=456', 'expected': '/backups/detail?volume_id=456'}, {'command': 'backup-list --filters name=123', 'expected': '/backups/detail?name=123'}, {'command': 'backup-list --filters volume_id~=456', 'expected': '/backups/detail?volume_id~=456'}, {'command': 'backup-list --filters volume_id=123 --filters name=456', 'expected': '/backups/detail?name=456&volume_id=123'}, # testcases for list snapshot {'command': 'snapshot-list --volume-id=123 --filters volume_id=456', 'expected': '/snapshots/detail?volume_id=456'}, {'command': 'snapshot-list --filters name=123', 'expected': '/snapshots/detail?name=123'}, {'command': 'snapshot-list --filters volume_id~=456', 'expected': '/snapshots/detail?volume_id~=456'}, {'command': 'snapshot-list --filters volume_id=123 --filters name=456', 'expected': '/snapshots/detail?name=456&volume_id=123'}, # testcases for get pools {'command': 'get-pools --filters name=456 --detail', 'expected': '/scheduler-stats/get_pools?detail=True&name=456'}, {'command': 'get-pools --filters name=456', 'expected': '/scheduler-stats/get_pools?name=456'}, {'command': 'get-pools --filters name=456 --filters detail=True', 'expected': '/scheduler-stats/get_pools?detail=True&name=456'} ) @ddt.unpack def test_list_with_filters_mixed(self, command, expected): self.run_command('--os-volume-api-version 3.33 %s' % command) self.assert_called('GET', expected) def test_list(self): self.run_command('list') # NOTE(jdg): we default to detail currently self.assert_called('GET', '/volumes/detail') def test_list_with_with_count(self): self.run_command('--os-volume-api-version 3.45 list --with-count') self.assert_called('GET', '/volumes/detail?with_count=True') def test_summary(self): self.run_command('--os-volume-api-version 3.12 summary') self.assert_called('GET', '/volumes/summary') def test_list_with_group_id_before_3_10(self): self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, 'list --group_id fake_id') def test_type_list_with_filters_invalid(self): self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, '--os-volume-api-version 3.51 type-list ' '--filters key=value') def test_type_list_with_filters(self): self.run_command('--os-volume-api-version 3.52 type-list ' '--filters extra_specs={key:value}') self.assert_called('GET', mock.ANY) self.assert_call_contained( parse.urlencode( {'extra_specs': {'key': 'value'}})) self.assert_call_contained(parse.urlencode({'is_public': None})) def test_type_list_public(self): self.run_command('--os-volume-api-version 3.52 type-list ' '--filters is_public=True') self.assert_called('GET', '/types?is_public=True') def test_type_list_private(self): self.run_command('--os-volume-api-version 3.52 type-list ' '--filters is_public=False') self.assert_called('GET', '/types?is_public=False') def test_type_list_public_private(self): self.run_command('--os-volume-api-version 3.52 type-list') self.assert_called('GET', '/types?is_public=None') @ddt.data("3.10", "3.11") def test_list_with_group_id_after_3_10(self, version): command = ('--os-volume-api-version %s list --group_id fake_id' % version) self.run_command(command) self.assert_called('GET', '/volumes/detail?group_id=fake_id') @mock.patch("cinderclient.utils.print_list") def test_list_duplicate_fields(self, mock_print): self.run_command('list --field Status,id,Size,status') self.assert_called('GET', '/volumes/detail') key_list = ['ID', 'Status', 'Size'] mock_print.assert_called_once_with(mock.ANY, key_list, exclude_unavailable=True, sortby_index=0) @mock.patch("cinderclient.shell.OpenStackCinderShell.downgrade_warning") def test_list_version_downgrade(self, mock_warning): self.run_command('--os-volume-api-version 3.998 list') mock_warning.assert_called_once_with( api_versions.APIVersion('3.998'), api_versions.APIVersion(api_versions.MAX_VERSION) ) def test_list_availability_zone(self): self.run_command('availability-zone-list') self.assert_called('GET', '/os-availability-zone') @ddt.data({'cmd': '1234 1233', 'body': {'instance_uuid': '1233', 'connector': {}, 'volume_uuid': '1234'}}, {'cmd': '1234 1233 ' '--connect True ' '--ip 10.23.12.23 --host server01 ' '--platform x86_xx ' '--ostype 123 ' '--multipath true ' '--mountpoint /123 ' '--initiator aabbccdd', 'body': {'instance_uuid': '1233', 'connector': {'ip': '10.23.12.23', 'host': 'server01', 'os_type': '123', 'multipath': 'true', 'mountpoint': '/123', 'initiator': 'aabbccdd', 'platform': 'x86_xx'}, 'volume_uuid': '1234'}}, {'cmd': 'abc 1233', 'body': {'instance_uuid': '1233', 'connector': {}, 'volume_uuid': '1234'}}, {'cmd': '1234', 'body': {'connector': {}, 'volume_uuid': '1234'}}, {'cmd': '1234 ' '--connect True ' '--ip 10.23.12.23 --host server01 ' '--platform x86_xx ' '--ostype 123 ' '--multipath true ' '--mountpoint /123 ' '--initiator aabbccdd', 'body': {'connector': {'ip': '10.23.12.23', 'host': 'server01', 'os_type': '123', 'multipath': 'true', 'mountpoint': '/123', 'initiator': 'aabbccdd', 'platform': 'x86_xx'}, 'volume_uuid': '1234'}}) @mock.patch('cinderclient.utils.find_resource') @ddt.unpack def test_attachment_create(self, mock_find_volume, cmd, body): mock_find_volume.return_value = volumes.Volume(self, {'id': '1234'}, loaded=True) command = '--os-volume-api-version 3.27 attachment-create ' command += cmd self.run_command(command) expected = {'attachment': body} self.assertTrue(mock_find_volume.called) self.assert_called('POST', '/attachments', body=expected) @ddt.data({'cmd': '1234 1233', 'body': {'instance_uuid': '1233', 'connector': {}, 'volume_uuid': '1234', 'mode': 'ro'}}, {'cmd': '1234 1233 ' '--connect True ' '--ip 10.23.12.23 --host server01 ' '--platform x86_xx ' '--ostype 123 ' '--multipath true ' '--mountpoint /123 ' '--initiator aabbccdd', 'body': {'instance_uuid': '1233', 'connector': {'ip': '10.23.12.23', 'host': 'server01', 'os_type': '123', 'multipath': 'true', 'mountpoint': '/123', 'initiator': 'aabbccdd', 'platform': 'x86_xx'}, 'volume_uuid': '1234', 'mode': 'ro'}}, {'cmd': 'abc 1233', 'body': {'instance_uuid': '1233', 'connector': {}, 'volume_uuid': '1234', 'mode': 'ro'}}, {'cmd': '1234', 'body': {'connector': {}, 'volume_uuid': '1234', 'mode': 'ro'}}, {'cmd': '1234 ' '--connect True ' '--ip 10.23.12.23 --host server01 ' '--platform x86_xx ' '--ostype 123 ' '--multipath true ' '--mountpoint /123 ' '--initiator aabbccdd', 'body': {'connector': {'ip': '10.23.12.23', 'host': 'server01', 'os_type': '123', 'multipath': 'true', 'mountpoint': '/123', 'initiator': 'aabbccdd', 'platform': 'x86_xx'}, 'volume_uuid': '1234', 'mode': 'ro'}}) @mock.patch('cinderclient.utils.find_resource') @ddt.unpack def test_attachment_create_with_mode(self, mock_find_volume, cmd, body): mock_find_volume.return_value = volumes.Volume(self, {'id': '1234'}, loaded=True) command = ('--os-volume-api-version 3.54 ' 'attachment-create ' '--mode ro ') command += cmd self.run_command(command) expected = {'attachment': body} self.assertTrue(mock_find_volume.called) self.assert_called('POST', '/attachments', body=expected) @mock.patch.object(volumes.VolumeManager, 'findall') def test_attachment_create_duplicate_name_vol(self, mock_findall): found = [volumes.Volume(self, {'id': '7654', 'name': 'abc'}, loaded=True), volumes.Volume(self, {'id': '9876', 'name': 'abc'}, loaded=True)] mock_findall.return_value = found self.assertRaises(exceptions.CommandError, self.run_command, '--os-volume-api-version 3.27 ' 'attachment-create abc 789') @ddt.data({'cmd': '', 'expected': ''}, {'cmd': '--volume-id 1234', 'expected': '?volume_id=1234'}, {'cmd': '--status error', 'expected': '?status=error'}, {'cmd': '--all-tenants 1', 'expected': '?all_tenants=1'}, {'cmd': '--all-tenants 1 --volume-id 12345', 'expected': '?all_tenants=1&volume_id=12345'}, {'cmd': '--all-tenants 1 --tenant 12345', 'expected': '?all_tenants=1&project_id=12345'}, {'cmd': '--tenant 12345', 'expected': '?all_tenants=1&project_id=12345'} ) @ddt.unpack def test_attachment_list(self, cmd, expected): command = '--os-volume-api-version 3.27 attachment-list ' command += cmd self.run_command(command) self.assert_called('GET', '/attachments%s' % expected) @mock.patch('cinderclient.utils.print_list') @mock.patch.object(cinderclient.v3.attachments.VolumeAttachmentManager, 'list') def test_attachment_list_setattr(self, mock_list, mock_print): command = '--os-volume-api-version 3.27 attachment-list ' fake_attachment = [attachments.VolumeAttachment(mock.ANY, attachment) for attachment in fakes.fake_attachment_list['attachments']] mock_list.return_value = fake_attachment self.run_command(command) for attach in fake_attachment: setattr(attach, 'server_id', getattr(attach, 'instance')) columns = ['ID', 'Volume ID', 'Status', 'Server ID'] mock_print.assert_called_once_with(fake_attachment, columns, sortby_index=0) def test_revert_to_snapshot(self): original = cinderclient_utils.find_resource def find_resource(manager, name_or_id, **kwargs): if isinstance(manager, volume_snapshots.SnapshotManager): return volume_snapshots.Snapshot(self, {'id': '5678', 'volume_id': '1234'}) return original(manager, name_or_id, **kwargs) with mock.patch('cinderclient.utils.find_resource', side_effect=find_resource): self.run_command( '--os-volume-api-version 3.40 revert-to-snapshot 5678') self.assert_called('POST', '/volumes/1234/action', body={'revert': {'snapshot_id': '5678'}}) def test_attachment_show(self): self.run_command('--os-volume-api-version 3.27 attachment-show 1234') self.assert_called('GET', '/attachments/1234') @ddt.data({'cmd': '1234 ' '--ip 10.23.12.23 --host server01 ' '--platform x86_xx ' '--ostype 123 ' '--multipath true ' '--mountpoint /123 ' '--initiator aabbccdd', 'body': {'connector': {'ip': '10.23.12.23', 'host': 'server01', 'os_type': '123', 'multipath': 'true', 'mountpoint': '/123', 'initiator': 'aabbccdd', 'platform': 'x86_xx'}}}) @ddt.unpack def test_attachment_update(self, cmd, body): command = '--os-volume-api-version 3.27 attachment-update ' command += cmd self.run_command(command) self.assert_called('PUT', '/attachments/1234', body={'attachment': body}) @ddt.unpack def test_attachment_complete(self): command = '--os-volume-api-version 3.44 attachment-complete 1234' self.run_command(command) self.assert_called('POST', '/attachments/1234/action', body=None) def test_attachment_delete(self): self.run_command('--os-volume-api-version 3.27 ' 'attachment-delete 1234') self.assert_called('DELETE', '/attachments/1234') def test_upload_to_image(self): expected = {'os-volume_upload_image': {'force': False, 'container_format': 'bare', 'disk_format': 'raw', 'image_name': 'test-image'}} self.run_command('upload-to-image 1234 test-image') self.assert_called_anytime('GET', '/volumes/1234') self.assert_called_anytime('POST', '/volumes/1234/action', body=expected) def test_upload_to_image_private_not_protected(self): expected = {'os-volume_upload_image': {'force': False, 'container_format': 'bare', 'disk_format': 'raw', 'image_name': 'test-image', 'protected': False, 'visibility': 'private'}} self.run_command('--os-volume-api-version 3.1 ' 'upload-to-image 1234 test-image') self.assert_called_anytime('GET', '/volumes/1234') self.assert_called_anytime('POST', '/volumes/1234/action', body=expected) def test_upload_to_image_public_protected(self): expected = {'os-volume_upload_image': {'force': False, 'container_format': 'bare', 'disk_format': 'raw', 'image_name': 'test-image', 'protected': 'True', 'visibility': 'public'}} self.run_command('--os-volume-api-version 3.1 ' 'upload-to-image --visibility=public ' '--protected=True 1234 test-image') self.assert_called_anytime('GET', '/volumes/1234') self.assert_called_anytime('POST', '/volumes/1234/action', body=expected) def test_backup_update(self): self.run_command('--os-volume-api-version 3.9 ' 'backup-update --name new_name 1234') expected = {'backup': {'name': 'new_name'}} self.assert_called('PUT', '/backups/1234', body=expected) def test_backup_list_with_with_count(self): self.run_command( '--os-volume-api-version 3.45 backup-list --with-count') self.assert_called('GET', '/backups/detail?with_count=True') def test_backup_update_with_description(self): self.run_command('--os-volume-api-version 3.9 ' 'backup-update 1234 --description=new-description') expected = {'backup': {'description': 'new-description'}} self.assert_called('PUT', '/backups/1234', body=expected) def test_backup_update_with_metadata(self): cmd = '--os-volume-api-version 3.43 ' cmd += 'backup-update ' cmd += '--metadata foo=bar ' cmd += '1234' self.run_command(cmd) expected = {'backup': {'metadata': {'foo': 'bar'}}} self.assert_called('PUT', '/backups/1234', body=expected) def test_backup_update_all(self): # rename and change description self.run_command('--os-volume-api-version 3.43 ' 'backup-update --name new-name ' '--description=new-description ' '--metadata foo=bar 1234') expected = {'backup': { 'name': 'new-name', 'description': 'new-description', 'metadata': {'foo': 'bar'} }} self.assert_called('PUT', '/backups/1234', body=expected) def test_backup_update_without_arguments(self): # Call rename with no arguments self.assertRaises(SystemExit, self.run_command, '--os-volume-api-version 3.9 backup-update') def test_backup_update_bad_request(self): self.assertRaises(exceptions.ClientException, self.run_command, '--os-volume-api-version 3.9 backup-update 1234') def test_backup_update_wrong_version(self): self.assertRaises(SystemExit, self.run_command, '--os-volume-api-version 3.8 ' 'backup-update --name new-name 1234') def test_group_type_list(self): self.run_command('--os-volume-api-version 3.11 group-type-list') self.assert_called_anytime('GET', '/group_types?is_public=None') def test_group_type_list_public(self): self.run_command('--os-volume-api-version 3.52 group-type-list ' '--filters is_public=True') self.assert_called('GET', '/group_types?is_public=True') def test_group_type_list_private(self): self.run_command('--os-volume-api-version 3.52 group-type-list ' '--filters is_public=False') self.assert_called('GET', '/group_types?is_public=False') def test_group_type_list_public_private(self): self.run_command('--os-volume-api-version 3.52 group-type-list') self.assert_called('GET', '/group_types?is_public=None') def test_group_type_show(self): self.run_command('--os-volume-api-version 3.11 ' 'group-type-show 1') self.assert_called('GET', '/group_types/1') def test_group_type_create(self): self.run_command('--os-volume-api-version 3.11 ' 'group-type-create test-type-1') self.assert_called('POST', '/group_types') def test_group_type_create_public(self): expected = {'group_type': {'name': 'test-type-1', 'description': 'test_type-1-desc', 'is_public': True}} self.run_command('--os-volume-api-version 3.11 ' 'group-type-create test-type-1 ' '--description=test_type-1-desc ' '--is-public=True') self.assert_called('POST', '/group_types', body=expected) def test_group_type_create_private(self): expected = {'group_type': {'name': 'test-type-3', 'description': 'test_type-3-desc', 'is_public': False}} self.run_command('--os-volume-api-version 3.11 ' 'group-type-create test-type-3 ' '--description=test_type-3-desc ' '--is-public=False') self.assert_called('POST', '/group_types', body=expected) def test_group_specs_list(self): self.run_command('--os-volume-api-version 3.11 group-specs-list') self.assert_called('GET', '/group_types?is_public=None') def test_create_volume_with_group(self): self.run_command('--os-volume-api-version 3.13 create --group-id 5678 ' '--volume-type 4321 1') self.assert_called('GET', '/volumes/1234') expected = {'volume': {'imageRef': None, 'size': 1, 'availability_zone': None, 'source_volid': None, 'consistencygroup_id': None, 'group_id': '5678', 'name': None, 'snapshot_id': None, 'metadata': {}, 'volume_type': '4321', 'description': None, 'backup_id': None}} self.assert_called_anytime('POST', '/volumes', expected) @ddt.data({'cmd': '--os-volume-api-version 3.47 create --backup-id 1234', 'update': {'backup_id': '1234'}}, {'cmd': '--os-volume-api-version 3.47 create 2', 'update': {'size': 2}} ) @ddt.unpack def test_create_volume_with_backup(self, cmd, update): self.run_command(cmd) self.assert_called('GET', '/volumes/1234') expected = {'volume': {'imageRef': None, 'size': None, 'availability_zone': None, 'source_volid': None, 'consistencygroup_id': None, 'name': None, 'snapshot_id': None, 'metadata': {}, 'volume_type': None, 'description': None, 'backup_id': None}} expected['volume'].update(update) self.assert_called_anytime('POST', '/volumes', body=expected) def test_group_list(self): self.run_command('--os-volume-api-version 3.13 group-list') self.assert_called_anytime('GET', '/groups/detail') def test_group_list__with_all_tenant(self): self.run_command( '--os-volume-api-version 3.13 group-list --all-tenants') self.assert_called_anytime('GET', '/groups/detail?all_tenants=1') def test_group_show(self): self.run_command('--os-volume-api-version 3.13 ' 'group-show 1234') self.assert_called('GET', '/groups/1234') def test_group_show_with_list_volume(self): self.run_command('--os-volume-api-version 3.25 ' 'group-show 1234 --list-volume') self.assert_called('GET', '/groups/1234?list_volume=True') @ddt.data(True, False) def test_group_delete(self, delete_vol): cmd = '--os-volume-api-version 3.13 group-delete 1234' if delete_vol: cmd += ' --delete-volumes' self.run_command(cmd) expected = {'delete': {'delete-volumes': delete_vol}} self.assert_called('POST', '/groups/1234/action', expected) def test_group_create(self): expected = {'group': {'name': 'test-1', 'description': 'test-1-desc', 'group_type': 'my_group_type', 'volume_types': ['type1', 'type2'], 'availability_zone': 'zone1'}} self.run_command('--os-volume-api-version 3.13 ' 'group-create --name test-1 ' '--description test-1-desc ' '--availability-zone zone1 ' 'my_group_type type1,type2') self.assert_called_anytime('POST', '/groups', body=expected) def test_group_update(self): self.run_command('--os-volume-api-version 3.13 group-update ' '--name group2 --description desc2 ' '--add-volumes uuid1,uuid2 ' '--remove-volumes uuid3,uuid4 ' '1234') expected = {'group': {'name': 'group2', 'description': 'desc2', 'add_volumes': 'uuid1,uuid2', 'remove_volumes': 'uuid3,uuid4'}} self.assert_called('PUT', '/groups/1234', body=expected) def test_group_update_invalid_args(self): self.assertRaises(exceptions.ClientException, self.run_command, '--os-volume-api-version 3.13 group-update 1234') def test_group_snapshot_list(self): self.run_command('--os-volume-api-version 3.14 group-snapshot-list') self.assert_called_anytime('GET', '/group_snapshots/detail') def test_group_snapshot_show(self): self.run_command('--os-volume-api-version 3.14 ' 'group-snapshot-show 1234') self.assert_called('GET', '/group_snapshots/1234') def test_group_snapshot_delete(self): cmd = '--os-volume-api-version 3.14 group-snapshot-delete 1234' self.run_command(cmd) self.assert_called('DELETE', '/group_snapshots/1234') def test_group_snapshot_create(self): expected = {'group_snapshot': {'name': 'test-1', 'description': 'test-1-desc', 'group_id': '1234'}} self.run_command('--os-volume-api-version 3.14 ' 'group-snapshot-create --name test-1 ' '--description test-1-desc 1234') self.assert_called_anytime('POST', '/group_snapshots', body=expected) @ddt.data( {'grp_snap_id': '1234', 'src_grp_id': None, 'src': '--group-snapshot 1234'}, {'grp_snap_id': None, 'src_grp_id': '1234', 'src': '--source-group 1234'}, ) @ddt.unpack def test_group_create_from_src(self, grp_snap_id, src_grp_id, src): expected = {'create-from-src': {'name': 'test-1', 'description': 'test-1-desc'}} if grp_snap_id: expected['create-from-src']['group_snapshot_id'] = grp_snap_id elif src_grp_id: expected['create-from-src']['source_group_id'] = src_grp_id cmd = ('--os-volume-api-version 3.14 ' 'group-create-from-src --name test-1 ' '--description test-1-desc ') cmd += src self.run_command(cmd) self.assert_called_anytime('POST', '/groups/action', body=expected) def test_volume_manageable_list(self): self.run_command('--os-volume-api-version 3.8 ' 'manageable-list fakehost') self.assert_called('GET', '/manageable_volumes/detail?host=fakehost') def test_volume_manageable_list_details(self): self.run_command('--os-volume-api-version 3.8 ' 'manageable-list fakehost --detailed True') self.assert_called('GET', '/manageable_volumes/detail?host=fakehost') def test_volume_manageable_list_no_details(self): self.run_command('--os-volume-api-version 3.8 ' 'manageable-list fakehost --detailed False') self.assert_called('GET', '/manageable_volumes?host=fakehost') def test_volume_manageable_list_cluster(self): self.run_command('--os-volume-api-version 3.17 ' 'manageable-list --cluster dest') self.assert_called('GET', '/manageable_volumes/detail?cluster=dest') @ddt.data(True, False, 'Nonboolean') @mock.patch('cinderclient.utils.find_resource') def test_snapshot_create_pre_3_66(self, force_value, mock_find_vol): mock_find_vol.return_value = volumes.Volume( self, {'id': '123456'}, loaded=True) snap_body_3_65 = { 'snapshot': { 'volume_id': '123456', 'force': f'{force_value}', 'name': None, 'description': None, 'metadata': {} } } self.run_command('--os-volume-api-version 3.65 ' f'snapshot-create --force {force_value} 123456') self.assert_called_anytime('POST', '/snapshots', body=snap_body_3_65) SNAP_BODY_3_66 = { 'snapshot': { 'volume_id': '123456', 'name': None, 'description': None, 'metadata': {} } } @ddt.data(True, 'true', 'on', '1') @mock.patch('cinderclient.utils.find_resource') def test_snapshot_create_3_66_with_force_true(self, f_val, mock_find_vol): mock_find_vol.return_value = volumes.Volume( self, {'id': '123456'}, loaded=True) mock_find_vol.return_value = volumes.Volume(self, {'id': '123456'}, loaded=True) self.run_command('--os-volume-api-version 3.66 ' f'snapshot-create --force {f_val} 123456') self.assert_called_anytime('POST', '/snapshots', body=self.SNAP_BODY_3_66) @ddt.data(False, 'false', 'no', '0', 'whatever') @mock.patch('cinderclient.utils.find_resource') def test_snapshot_create_3_66_with_force_not_true( self, f_val, mock_find_vol): mock_find_vol.return_value = volumes.Volume( self, {'id': '123456'}, loaded=True) uae = self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, '--os-volume-api-version 3.66 ' f'snapshot-create --force {f_val} 123456') self.assertIn('not allowed after microversion 3.65', str(uae)) @mock.patch('cinderclient.utils.find_resource') def test_snapshot_create_3_66(self, mock_find_vol): mock_find_vol.return_value = volumes.Volume( self, {'id': '123456'}, loaded=True) self.run_command('--os-volume-api-version 3.66 ' 'snapshot-create 123456') self.assert_called_anytime('POST', '/snapshots', body=self.SNAP_BODY_3_66) def test_snapshot_manageable_list(self): self.run_command('--os-volume-api-version 3.8 ' 'snapshot-manageable-list fakehost') self.assert_called('GET', '/manageable_snapshots/detail?host=fakehost') def test_snapshot_manageable_list_details(self): self.run_command('--os-volume-api-version 3.8 ' 'snapshot-manageable-list fakehost --detailed True') self.assert_called('GET', '/manageable_snapshots/detail?host=fakehost') def test_snapshot_manageable_list_no_details(self): self.run_command('--os-volume-api-version 3.8 ' 'snapshot-manageable-list fakehost --detailed False') self.assert_called('GET', '/manageable_snapshots?host=fakehost') def test_snapshot_manageable_list_cluster(self): self.run_command('--os-volume-api-version 3.17 ' 'snapshot-manageable-list --cluster dest') self.assert_called('GET', '/manageable_snapshots/detail?cluster=dest') @ddt.data('', 'snapshot-') def test_manageable_list_cluster_before_3_17(self, prefix): self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, '--os-volume-api-version 3.16 ' '%smanageable-list --cluster dest' % prefix) @mock.patch('cinderclient.shell.CinderClientArgumentParser.error') @ddt.data('', 'snapshot-') def test_manageable_list_mutual_exclusion(self, prefix, error_mock): error_mock.side_effect = SystemExit self.assertRaises(SystemExit, self.run_command, '--os-volume-api-version 3.17 ' '%smanageable-list fakehost --cluster dest' % prefix) @mock.patch('cinderclient.shell.CinderClientArgumentParser.error') @ddt.data('', 'snapshot-') def test_manageable_list_missing_required(self, prefix, error_mock): error_mock.side_effect = SystemExit self.assertRaises(SystemExit, self.run_command, '--os-volume-api-version 3.17 ' '%smanageable-list' % prefix) def test_list_messages(self): self.run_command('--os-volume-api-version 3.3 message-list') self.assert_called('GET', '/messages') @ddt.data('volume', 'backup', 'snapshot', None) def test_reset_state_entity_not_found(self, entity_type): cmd = 'reset-state 999999' if entity_type is not None: cmd += ' --type %s' % entity_type self.assertRaises(exceptions.CommandError, self.run_command, cmd) @ddt.data({'entity_types': [{'name': 'volume', 'version': '3.0', 'command': 'os-reset_status'}, {'name': 'backup', 'version': '3.0', 'command': 'os-reset_status'}, {'name': 'snapshot', 'version': '3.0', 'command': 'os-reset_status'}, {'name': None, 'version': '3.0', 'command': 'os-reset_status'}, {'name': 'group', 'version': '3.20', 'command': 'reset_status'}, {'name': 'group-snapshot', 'version': '3.19', 'command': 'reset_status'}], 'r_id': ['1234'], 'states': ['available', 'error', None]}, {'entity_types': [{'name': 'volume', 'version': '3.0', 'command': 'os-reset_status'}, {'name': 'backup', 'version': '3.0', 'command': 'os-reset_status'}, {'name': 'snapshot', 'version': '3.0', 'command': 'os-reset_status'}, {'name': None, 'version': '3.0', 'command': 'os-reset_status'}, {'name': 'group', 'version': '3.20', 'command': 'reset_status'}, {'name': 'group-snapshot', 'version': '3.19', 'command': 'reset_status'}], 'r_id': ['1234', '5678'], 'states': ['available', 'error', None]}) @ddt.unpack def test_reset_state_normal(self, entity_types, r_id, states): for state in states: for t in entity_types: if state is None: expected = {t['command']: {}} cmd = ('--os-volume-api-version ' '%s reset-state %s') % (t['version'], ' '.join(r_id)) else: expected = {t['command']: {'status': state}} cmd = ('--os-volume-api-version ' '%s reset-state ' '--state %s %s') % (t['version'], state, ' '.join(r_id)) if t['name'] is not None: cmd += ' --type %s' % t['name'] self.run_command(cmd) name = t['name'] if t['name'] else 'volume' for re in r_id: self.assert_called_anytime('POST', '/%ss/%s/action' % (name.replace('-', '_'), re), body=expected) @ddt.data({'command': '--attach-status detached', 'expected': {'attach_status': 'detached'}}, {'command': '--state in-use --attach-status attached', 'expected': {'status': 'in-use', 'attach_status': 'attached'}}, {'command': '--reset-migration-status', 'expected': {'migration_status': 'none'}}) @ddt.unpack def test_reset_state_volume_additional_status(self, command, expected): self.run_command('reset-state %s 1234' % command) expected = {'os-reset_status': expected} self.assert_called('POST', '/volumes/1234/action', body=expected) def test_snapshot_list_with_with_count(self): self.run_command( '--os-volume-api-version 3.45 snapshot-list --with-count') self.assert_called('GET', '/snapshots/detail?with_count=True') def test_snapshot_list_with_metadata(self): self.run_command('--os-volume-api-version 3.22 ' 'snapshot-list --metadata key1=val1') expected = ("/snapshots/detail?metadata=%s" % parse.quote_plus("{'key1': 'val1'}")) self.assert_called('GET', expected) @ddt.data(('resource_type',), ('event_id',), ('resource_uuid',), ('level', 'message_level'), ('request_id',)) def test_list_messages_with_filters(self, filter): self.run_command('--os-volume-api-version 3.5 message-list --%s=TEST' % filter[0]) self.assert_called('GET', '/messages?%s=TEST' % filter[-1]) def test_list_messages_with_sort(self): self.run_command('--os-volume-api-version 3.5 ' 'message-list --sort=id:asc') self.assert_called('GET', '/messages?sort=id%3Aasc') def test_list_messages_with_limit(self): self.run_command('--os-volume-api-version 3.5 message-list --limit=1') self.assert_called('GET', '/messages?limit=1') def test_list_messages_with_marker(self): self.run_command('--os-volume-api-version 3.5 message-list --marker=1') self.assert_called('GET', '/messages?marker=1') def test_list_with_image_metadata_before_3_4(self): self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, 'list --image_metadata image_name=1234') def test_list_filter_image_metadata(self): self.run_command('--os-volume-api-version 3.4 ' 'list --image_metadata image_name=1234') url = ('/volumes/detail?%s' % parse.urlencode([('glance_metadata', {"image_name": "1234"})])) self.assert_called('GET', url) def test_show_message(self): self.run_command('--os-volume-api-version 3.5 message-show 1234') self.assert_called('GET', '/messages/1234') def test_delete_message(self): self.run_command('--os-volume-api-version 3.5 message-delete 1234') self.assert_called('DELETE', '/messages/1234') def test_delete_messages(self): self.run_command( '--os-volume-api-version 3.3 message-delete 1234 12345') self.assert_called_anytime('DELETE', '/messages/1234') self.assert_called_anytime('DELETE', '/messages/12345') @mock.patch('cinderclient.utils.find_resource') def test_delete_metadata(self, mock_find_volume): mock_find_volume.return_value = volumes.Volume(self, {'id': '1234', 'metadata': {'k1': 'v1', 'k2': 'v2', 'k3': 'v3'}}, loaded = True) expected = {'metadata': {'k2': 'v2'}} self.run_command('--os-volume-api-version 3.15 ' 'metadata 1234 unset k1 k3') self.assert_called('PUT', '/volumes/1234/metadata', body=expected) @ddt.data(("3.0", None), ("3.6", None), ("3.7", True), ("3.7", False), ("3.7", "")) @ddt.unpack def test_service_list_withreplication(self, version, replication): command = ('--os-volume-api-version %s service-list' % version) if replication is not None: command += ' --withreplication %s' % replication self.run_command(command) self.assert_called('GET', '/os-services') def test_group_enable_replication(self): cmd = '--os-volume-api-version 3.38 group-enable-replication 1234' self.run_command(cmd) expected = {'enable_replication': {}} self.assert_called('POST', '/groups/1234/action', body=expected) def test_group_disable_replication(self): cmd = '--os-volume-api-version 3.38 group-disable-replication 1234' self.run_command(cmd) expected = {'disable_replication': {}} self.assert_called('POST', '/groups/1234/action', body=expected) @ddt.data((False, None), (True, None), (False, "backend1"), (True, "backend1"), (False, "default"), (True, "default")) @ddt.unpack def test_group_failover_replication(self, attach_vol, backend): attach = '--allow-attached-volume ' if attach_vol else '' backend_id = ('--secondary-backend-id ' + backend) if backend else '' cmd = ('--os-volume-api-version 3.38 ' 'group-failover-replication 1234 ' + attach + backend_id) self.run_command(cmd) expected = {'failover_replication': {'allow_attached_volume': attach_vol, 'secondary_backend_id': backend if backend else None}} self.assert_called('POST', '/groups/1234/action', body=expected) def test_group_list_replication_targets(self): cmd = ('--os-volume-api-version 3.38 group-list-replication-targets' ' 1234') self.run_command(cmd) expected = {'list_replication_targets': {}} self.assert_called('POST', '/groups/1234/action', body=expected) @mock.patch('cinderclient.v3.services.ServiceManager.get_log_levels') def test_service_get_log_before_3_32(self, get_levels_mock): self.assertRaises(SystemExit, self.run_command, '--os-volume-api-version 3.28 ' 'service-get-log') get_levels_mock.assert_not_called() @mock.patch('cinderclient.v3.services.ServiceManager.get_log_levels') @mock.patch('cinderclient.utils.print_list') def test_service_get_log_no_params(self, print_mock, get_levels_mock): self.run_command('--os-volume-api-version 3.32 service-get-log') get_levels_mock.assert_called_once_with('', '', '') print_mock.assert_called_once_with(get_levels_mock.return_value, ('Binary', 'Host', 'Prefix', 'Level')) @ddt.data('*', 'cinder-api', 'cinder-volume', 'cinder-scheduler', 'cinder-backup') @mock.patch('cinderclient.v3.services.ServiceManager.get_log_levels') @mock.patch('cinderclient.utils.print_list') def test_service_get_log(self, binary, print_mock, get_levels_mock): server = 'host1' prefix = 'sqlalchemy' self.run_command('--os-volume-api-version 3.32 service-get-log ' '--binary %s --server %s --prefix %s' % ( binary, server, prefix)) get_levels_mock.assert_called_once_with(binary, server, prefix) print_mock.assert_called_once_with(get_levels_mock.return_value, ('Binary', 'Host', 'Prefix', 'Level')) @mock.patch('cinderclient.v3.services.ServiceManager.set_log_levels') def test_service_set_log_before_3_32(self, set_levels_mock): self.assertRaises(SystemExit, self.run_command, '--os-volume-api-version 3.28 ' 'service-set-log debug') set_levels_mock.assert_not_called() @mock.patch('cinderclient.v3.services.ServiceManager.set_log_levels') @mock.patch('cinderclient.shell.CinderClientArgumentParser.error') def test_service_set_log_missing_required(self, error_mock, set_levels_mock): error_mock.side_effect = SystemExit self.assertRaises(SystemExit, self.run_command, '--os-volume-api-version 3.32 ' 'service-set-log') set_levels_mock.assert_not_called() msg = 'the following arguments are required: ' error_mock.assert_called_once_with(msg) @ddt.data('debug', 'DEBUG', 'info', 'INFO', 'warning', 'WARNING', 'error', 'ERROR') @mock.patch('cinderclient.v3.services.ServiceManager.set_log_levels') def test_service_set_log_min_params(self, level, set_levels_mock): self.run_command('--os-volume-api-version 3.32 ' 'service-set-log %s' % level) set_levels_mock.assert_called_once_with(level, '', '', '') @ddt.data('*', 'cinder-api', 'cinder-volume', 'cinder-scheduler', 'cinder-backup') @mock.patch('cinderclient.v3.services.ServiceManager.set_log_levels') def test_service_set_log_levels(self, binary, set_levels_mock): level = 'debug' server = 'host1' prefix = 'sqlalchemy.' self.run_command('--os-volume-api-version 3.32 ' 'service-set-log %s --binary %s --server %s ' '--prefix %s' % (level, binary, server, prefix)) set_levels_mock.assert_called_once_with(level, binary, server, prefix) @mock.patch('cinderclient.shell_utils._poll_for_status') def test_create_with_poll(self, poll_method): self.run_command('create --poll 1') self.assert_called_anytime('GET', '/volumes/1234') volume = self.shell.cs.volumes.get('1234') info = dict() info.update(volume._info) self.assertEqual(1, poll_method.call_count) timeout_period = 3600 poll_method.assert_has_calls([mock.call(self.shell.cs.volumes.get, 1234, info, 'creating', ['available'], timeout_period, self.shell.cs.client.global_request_id, self.shell.cs.messages)]) @mock.patch('cinderclient.shell_utils.time') def test_poll_for_status(self, mock_time): poll_period = 2 some_id = "some-id" global_request_id = "req-someid" action = "some" updated_objects = ( base.Resource(None, info={"not_default_field": "creating"}), base.Resource(None, info={"not_default_field": "available"})) poll_fn = mock.MagicMock(side_effect=updated_objects) cinderclient.shell_utils._poll_for_status( poll_fn = poll_fn, obj_id = some_id, global_request_id = global_request_id, messages = base.Resource(None, {}), info = {}, action = action, status_field = "not_default_field", final_ok_states = ['available'], timeout_period=3600) self.assertEqual([mock.call(poll_period)] * 2, mock_time.sleep.call_args_list) self.assertEqual([mock.call(some_id)] * 2, poll_fn.call_args_list) @mock.patch('cinderclient.v3.messages.MessageManager.list') @mock.patch('cinderclient.shell_utils.time') def test_poll_for_status_error(self, mock_time, mock_message_list): poll_period = 2 some_id = "some_id" global_request_id = "req-someid" action = "some" updated_objects = ( base.Resource(None, info={"not_default_field": "creating"}), base.Resource(None, info={"not_default_field": "error"})) poll_fn = mock.MagicMock(side_effect=updated_objects) msg_object = base.Resource(cinderclient.v3.messages.MessageManager, info = {"user_message": "ERROR!"}) mock_message_list.return_value = (msg_object,) self.assertRaises(exceptions.ResourceInErrorState, cinderclient.shell_utils._poll_for_status, poll_fn=poll_fn, obj_id=some_id, global_request_id=global_request_id, messages=cinderclient.v3.messages.MessageManager(api=3.34), info=dict(), action=action, final_ok_states=['available'], status_field="not_default_field", timeout_period=3600) self.assertEqual([mock.call(poll_period)] * 2, mock_time.sleep.call_args_list) self.assertEqual([mock.call(some_id)] * 2, poll_fn.call_args_list) def test_backup(self): self.run_command('--os-volume-api-version 3.42 backup-create ' '--name 1234 1234') expected = {'backup': {'volume_id': 1234, 'container': None, 'name': '1234', 'description': None, 'incremental': False, 'force': False, 'snapshot_id': None, }} self.assert_called('POST', '/backups', body=expected) def test_backup_with_metadata(self): self.run_command('--os-volume-api-version 3.43 backup-create ' '--metadata foo=bar --name 1234 1234') expected = {'backup': {'volume_id': 1234, 'container': None, 'name': '1234', 'description': None, 'incremental': False, 'force': False, 'snapshot_id': None, 'metadata': {'foo': 'bar'}, }} self.assert_called('POST', '/backups', body=expected) def test_backup_with_az(self): self.run_command('--os-volume-api-version 3.51 backup-create ' '--availability-zone AZ2 --name 1234 1234') expected = {'backup': {'volume_id': 1234, 'container': None, 'name': '1234', 'description': None, 'incremental': False, 'force': False, 'snapshot_id': None, 'availability_zone': 'AZ2'}} self.assert_called('POST', '/backups', body=expected) @mock.patch("cinderclient.utils.print_list") def test_snapshot_list(self, mock_print_list): """Ensure we always present all existing fields when listing snaps.""" self.run_command('--os-volume-api-version 3.65 snapshot-list') self.assert_called('GET', '/snapshots/detail') columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Consumes Quota', 'User ID'] mock_print_list.assert_called_once_with(mock.ANY, columns, exclude_unavailable=True, sortby_index=0) @mock.patch('cinderclient.v3.volumes.Volume.migrate_volume') def test_migrate_volume_before_3_16(self, v3_migrate_mock): self.run_command('--os-volume-api-version 3.15 ' 'migrate 1234 fakehost') v3_migrate_mock.assert_called_once_with( 'fakehost', False, False, None) @mock.patch('cinderclient.v3.volumes.Volume.migrate_volume') def test_migrate_volume_3_16(self, v3_migrate_mock): self.run_command('--os-volume-api-version 3.16 ' 'migrate 1234 fakehost') self.assertEqual(4, len(v3_migrate_mock.call_args[0])) def test_migrate_volume_with_cluster_before_3_16(self): self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, '--os-volume-api-version 3.15 ' 'migrate 1234 fakehost --cluster fakecluster') @mock.patch('cinderclient.shell.CinderClientArgumentParser.error') def test_migrate_volume_mutual_exclusion(self, error_mock): error_mock.side_effect = SystemExit self.assertRaises(SystemExit, self.run_command, '--os-volume-api-version 3.16 ' 'migrate 1234 fakehost --cluster fakecluster') msg = 'argument --cluster: not allowed with argument ' error_mock.assert_called_once_with(msg) @mock.patch('cinderclient.shell.CinderClientArgumentParser.error') def test_migrate_volume_missing_required(self, error_mock): error_mock.side_effect = SystemExit self.assertRaises(SystemExit, self.run_command, '--os-volume-api-version 3.16 ' 'migrate 1234') msg = 'one of the arguments --cluster is required' error_mock.assert_called_once_with(msg) def test_migrate_volume_host(self): self.run_command('--os-volume-api-version 3.16 ' 'migrate 1234 fakehost') expected = {'os-migrate_volume': {'force_host_copy': False, 'lock_volume': False, 'host': 'fakehost'}} self.assert_called('POST', '/volumes/1234/action', body=expected) def test_migrate_volume_cluster(self): self.run_command('--os-volume-api-version 3.16 ' 'migrate 1234 --cluster mycluster') expected = {'os-migrate_volume': {'force_host_copy': False, 'lock_volume': False, 'cluster': 'mycluster'}} self.assert_called('POST', '/volumes/1234/action', body=expected) def test_migrate_volume_bool_force(self): self.run_command('--os-volume-api-version 3.16 ' 'migrate 1234 fakehost --force-host-copy ' '--lock-volume') expected = {'os-migrate_volume': {'force_host_copy': True, 'lock_volume': True, 'host': 'fakehost'}} self.assert_called('POST', '/volumes/1234/action', body=expected) def test_migrate_volume_bool_force_false(self): # Set both --force-host-copy and --lock-volume to False. self.run_command('--os-volume-api-version 3.16 ' 'migrate 1234 fakehost --force-host-copy=False ' '--lock-volume=False') expected = {'os-migrate_volume': {'force_host_copy': 'False', 'lock_volume': 'False', 'host': 'fakehost'}} self.assert_called('POST', '/volumes/1234/action', body=expected) # Do not set the values to --force-host-copy and --lock-volume. self.run_command('--os-volume-api-version 3.16 ' 'migrate 1234 fakehost') expected = {'os-migrate_volume': {'force_host_copy': False, 'lock_volume': False, 'host': 'fakehost'}} self.assert_called('POST', '/volumes/1234/action', body=expected) @ddt.data({'bootable': False, 'by_id': False, 'cluster': None}, {'bootable': True, 'by_id': False, 'cluster': None}, {'bootable': False, 'by_id': True, 'cluster': None}, {'bootable': True, 'by_id': True, 'cluster': None}, {'bootable': True, 'by_id': True, 'cluster': 'clustername'}) @ddt.unpack def test_volume_manage(self, bootable, by_id, cluster): cmd = ('--os-volume-api-version 3.16 ' 'manage host1 some_fake_name --name foo --description bar ' '--volume-type baz --availability-zone az ' '--metadata k1=v1 k2=v2') if by_id: cmd += ' --id-type source-id' if bootable: cmd += ' --bootable' if cluster: cmd += ' --cluster ' + cluster self.run_command(cmd) ref = 'source-id' if by_id else 'source-name' expected = {'volume': {'host': 'host1', 'ref': {ref: 'some_fake_name'}, 'name': 'foo', 'description': 'bar', 'volume_type': 'baz', 'availability_zone': 'az', 'metadata': {'k1': 'v1', 'k2': 'v2'}, 'bootable': bootable}} if cluster: expected['volume']['cluster'] = cluster self.assert_called_anytime('POST', '/os-volume-manage', body=expected) def test_volume_manage_before_3_16(self): """Cluster optional argument was not acceptable.""" self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, 'manage host1 some_fake_name ' '--cluster clustername' '--name foo --description bar --bootable ' '--volume-type baz --availability-zone az ' '--metadata k1=v1 k2=v2') def test_worker_cleanup_before_3_24(self): self.assertRaises(SystemExit, self.run_command, 'work-cleanup fakehost') def test_worker_cleanup(self): self.run_command('--os-volume-api-version 3.24 ' 'work-cleanup --cluster clustername --host hostname ' '--binary binaryname --is-up false --disabled true ' '--resource-id uuid --resource-type Volume ' '--service-id 1') expected = {'cluster_name': 'clustername', 'host': 'hostname', 'binary': 'binaryname', 'is_up': 'false', 'disabled': 'true', 'resource_id': 'uuid', 'resource_type': 'Volume', 'service_id': 1} self.assert_called('POST', '/workers/cleanup', body=expected) def test_create_transfer(self): self.run_command('transfer-create 1234') expected = {'transfer': {'volume_id': 1234, 'name': None, }} self.assert_called('POST', '/os-volume-transfer', body=expected) def test_create_transfer_no_snaps(self): self.run_command('--os-volume-api-version 3.55 transfer-create ' '--no-snapshots 1234') expected = {'transfer': {'volume_id': 1234, 'name': None, 'no_snapshots': True }} self.assert_called('POST', '/volume-transfers', body=expected) def test_list_transfer_sorty_not_sorty(self): self.run_command( '--os-volume-api-version 3.59 transfer-list') url = ('/volume-transfers/detail') self.assert_called('GET', url) def test_subcommand_parser(self): """Ensure that all the expected commands show up. This test ensures that refactoring code does not somehow result in a command accidentally ceasing to exist. TODO: add a similar test for 3.59 or so """ p = self.shell.get_subcommand_parser(api_versions.APIVersion("3.0"), input_args=['help'], do_help=True) help_text = p.format_help() # These are v3.0 commands only expected_commands = ('absolute-limits', 'api-version', 'availability-zone-list', 'backup-create', 'backup-delete', 'backup-export', 'backup-import', 'backup-list', 'backup-reset-state', 'backup-restore', 'backup-show', 'cgsnapshot-create', 'cgsnapshot-delete', 'cgsnapshot-list', 'cgsnapshot-show', 'consisgroup-create', 'consisgroup-create-from-src', 'consisgroup-delete', 'consisgroup-list', 'consisgroup-show', 'consisgroup-update', 'create', 'delete', 'encryption-type-create', 'encryption-type-delete', 'encryption-type-list', 'encryption-type-show', 'encryption-type-update', 'extend', 'extra-specs-list', 'failover-host', 'force-delete', 'freeze-host', 'get-capabilities', 'get-pools', 'image-metadata', 'image-metadata-show', 'list', 'manage', 'metadata', 'metadata-show', 'metadata-update-all', 'migrate', 'qos-associate', 'qos-create', 'qos-delete', 'qos-disassociate', 'qos-disassociate-all', 'qos-get-association', 'qos-key', 'qos-list', 'qos-show', 'quota-class-show', 'quota-class-update', 'quota-defaults', 'quota-delete', 'quota-show', 'quota-update', 'quota-usage', 'rate-limits', 'readonly-mode-update', 'rename', 'reset-state', 'retype', 'service-disable', 'service-enable', 'service-list', 'set-bootable', 'show', 'snapshot-create', 'snapshot-delete', 'snapshot-list', 'snapshot-manage', 'snapshot-metadata', 'snapshot-metadata-show', 'snapshot-metadata-update-all', 'snapshot-rename', 'snapshot-reset-state', 'snapshot-show', 'snapshot-unmanage', 'thaw-host', 'transfer-accept', 'transfer-create', 'transfer-delete', 'transfer-list', 'transfer-show', 'type-access-add', 'type-access-list', 'type-access-remove', 'type-create', 'type-default', 'type-delete', 'type-key', 'type-list', 'type-show', 'type-update', 'unmanage', 'upload-to-image', 'version-list', 'bash-completion', 'help',) for e in expected_commands: self.assertIn(' ' + e, help_text) @ddt.data( # testcases for list transfers {'command': 'transfer-list --filters volume_id=456', 'expected': '/os-volume-transfer/detail?volume_id=456'}, {'command': 'transfer-list --filters id=123', 'expected': '/os-volume-transfer/detail?id=123'}, {'command': 'transfer-list --filters name=abc', 'expected': '/os-volume-transfer/detail?name=abc'}, {'command': 'transfer-list --filters name=abc --filters volume_id=456', 'expected': '/os-volume-transfer/detail?name=abc&volume_id=456'}, {'command': 'transfer-list --filters id=123 --filters volume_id=456', 'expected': '/os-volume-transfer/detail?id=123&volume_id=456'}, {'command': 'transfer-list --filters id=123 --filters name=abc', 'expected': '/os-volume-transfer/detail?id=123&name=abc'}, ) @ddt.unpack def test_transfer_list_with_filters(self, command, expected): self.run_command('--os-volume-api-version 3.52 %s' % command) self.assert_called('GET', expected) def test_default_type_set(self): self.run_command('--os-volume-api-version 3.62 default-type-set ' '4c298f16-e339-4c80-b934-6cbfcb7525a0 ' '629632e7-99d2-4c40-9ae3-106fa3b1c9b7') body = { 'default_type': { 'volume_type': '4c298f16-e339-4c80-b934-6cbfcb7525a0' } } self.assert_called( 'PUT', 'v3/default-types/629632e7-99d2-4c40-9ae3-106fa3b1c9b7', body=body) def test_default_type_list_project(self): self.run_command('--os-volume-api-version 3.62 default-type-list ' '--project-id 629632e7-99d2-4c40-9ae3-106fa3b1c9b7') self.assert_called( 'GET', 'v3/default-types/629632e7-99d2-4c40-9ae3-106fa3b1c9b7') def test_default_type_list(self): self.run_command('--os-volume-api-version 3.62 default-type-list') self.assert_called('GET', 'v3/default-types') def test_default_type_delete(self): self.run_command('--os-volume-api-version 3.62 default-type-unset ' '629632e7-99d2-4c40-9ae3-106fa3b1c9b7') self.assert_called( 'DELETE', 'v3/default-types/629632e7-99d2-4c40-9ae3-106fa3b1c9b7') def test_restore(self): self.run_command('backup-restore 1234') self.assert_called('POST', '/backups/1234/restore') def test_restore_with_name(self): self.run_command('backup-restore 1234 --name restore_vol') expected = {'restore': {'volume_id': None, 'name': 'restore_vol'}} self.assert_called('POST', '/backups/1234/restore', body=expected) def test_restore_with_name_error(self): self.assertRaises(exceptions.CommandError, self.run_command, 'backup-restore 1234 --volume fake_vol --name ' 'restore_vol') def test_restore_with_az(self): self.run_command('--os-volume-api-version 3.47 backup-restore 1234 ' '--name restore_vol --availability-zone restore_az') expected = {'volume': {'size': 10, 'name': 'restore_vol', 'availability_zone': 'restore_az', 'backup_id': '1234', 'metadata': {}, 'imageRef': None, 'source_volid': None, 'consistencygroup_id': None, 'snapshot_id': None, 'volume_type': None, 'description': None}} self.assert_called('POST', '/volumes', body=expected) def test_restore_with_az_microversion_error(self): self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, '--os-volume-api-version 3.46 backup-restore 1234 ' '--name restore_vol --availability-zone restore_az') def test_restore_with_volume_type(self): self.run_command('--os-volume-api-version 3.47 backup-restore 1234 ' '--name restore_vol --volume-type restore_type') expected = {'volume': {'size': 10, 'name': 'restore_vol', 'volume_type': 'restore_type', 'backup_id': '1234', 'metadata': {}, 'imageRef': None, 'source_volid': None, 'consistencygroup_id': None, 'snapshot_id': None, 'availability_zone': None, 'description': None}} self.assert_called('POST', '/volumes', body=expected) def test_restore_with_volume_type_microversion_error(self): self.assertRaises(exceptions.UnsupportedAttribute, self.run_command, '--os-volume-api-version 3.46 backup-restore 1234 ' '--name restore_vol --volume-type restore_type') def test_restore_with_volume_type_and_az_no_name(self): self.run_command('--os-volume-api-version 3.47 backup-restore 1234 ' '--volume-type restore_type ' '--availability-zone restore_az') expected = {'volume': {'size': 10, 'name': 'restore_backup_1234', 'volume_type': 'restore_type', 'availability_zone': 'restore_az', 'backup_id': '1234', 'metadata': {}, 'imageRef': None, 'source_volid': None, 'consistencygroup_id': None, 'snapshot_id': None, 'description': None}} self.assert_called('POST', '/volumes', body=expected) @ddt.data( { 'volume': '1234', 'name': None, 'volume_type': None, 'availability_zone': None, }, { 'volume': '1234', 'name': 'ignored', 'volume_type': None, 'availability_zone': None, }, { 'volume': None, 'name': 'sample-volume', 'volume_type': 'sample-type', 'availability_zone': None, }, { 'volume': None, 'name': 'sample-volume', 'volume_type': None, 'availability_zone': 'az1', }, { 'volume': None, 'name': 'sample-volume', 'volume_type': None, 'availability_zone': 'different-az', }, { 'volume': None, 'name': None, 'volume_type': None, 'availability_zone': 'different-az', }, ) @ddt.unpack @mock.patch('cinderclient.utils.print_dict') @mock.patch('cinderclient.tests.unit.v3.fakes_base._stub_restore') def test_do_backup_restore(self, mock_stub_restore, mock_print_dict, volume, name, volume_type, availability_zone): # Restore from the fake '1234' backup. cmd = '--os-volume-api-version 3.47 backup-restore 1234' if volume: cmd += ' --volume %s' % volume if name: cmd += ' --name %s' % name if volume_type: cmd += ' --volume-type %s' % volume_type if availability_zone: cmd += ' --availability-zone %s' % availability_zone if name or volume: volume_name = 'sample-volume' else: volume_name = 'restore_backup_1234' mock_stub_restore.return_value = {'volume_id': '1234', 'volume_name': volume_name} self.run_command(cmd) # Check whether mock_stub_restore was called in order to determine # whether the restore command invoked the backup-restore API. If # mock_stub_restore was not called then this indicates the command # invoked the volume-create API to restore the backup to a new volume # of a specific volume type, or in a different AZ (the fake '1234' # backup is in az1). if volume_type or availability_zone == 'different-az': mock_stub_restore.assert_not_called() else: mock_stub_restore.assert_called_once() mock_print_dict.assert_called_once_with({ 'backup_id': '1234', 'volume_id': '1234', 'volume_name': volume_name, }) def test_reimage(self): self.run_command('--os-volume-api-version 3.68 reimage 1234 1') expected = {'os-reimage': {'image_id': '1', 'reimage_reserved': False}} self.assert_called('POST', '/volumes/1234/action', body=expected) @ddt.data('False', 'True') def test_reimage_reserved(self, reimage_reserved): self.run_command( '--os-volume-api-version 3.68 reimage --reimage-reserved %s 1234 1' % reimage_reserved) expected = {'os-reimage': {'image_id': '1', 'reimage_reserved': reimage_reserved}} self.assert_called('POST', '/volumes/1234/action', body=expected) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_snapshot_actions.py0000664000175000017500000000453700000000000027644 0ustar00zuulzuul00000000000000# Copyright 2013 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient.tests.unit.fixture_data import client from cinderclient.tests.unit.fixture_data import snapshots from cinderclient.tests.unit import utils class SnapshotActionsTest(utils.FixturedTestCase): client_fixture_class = client.V3 data_fixture_class = snapshots.Fixture def test_update_snapshot_status(self): snap = self.cs.volume_snapshots.get('1234') self._assert_request_id(snap) stat = {'status': 'available'} stats = self.cs.volume_snapshots.update_snapshot_status(snap, stat) self.assert_called('POST', '/snapshots/1234/action') self._assert_request_id(stats) def test_update_snapshot_status_with_progress(self): s = self.cs.volume_snapshots.get('1234') self._assert_request_id(s) stat = {'status': 'available', 'progress': '73%'} stats = self.cs.volume_snapshots.update_snapshot_status(s, stat) self.assert_called('POST', '/snapshots/1234/action') self._assert_request_id(stats) def test_list_snapshots_with_marker_limit(self): lst = self.cs.volume_snapshots.list(marker=1234, limit=2) self.assert_called('GET', '/snapshots/detail?limit=2&marker=1234') self._assert_request_id(lst) def test_list_snapshots_with_sort(self): lst = self.cs.volume_snapshots.list(sort="id") self.assert_called('GET', '/snapshots/detail?sort=id') self._assert_request_id(lst) def test_snapshot_unmanage(self): s = self.cs.volume_snapshots.get('1234') self._assert_request_id(s) snap = self.cs.volume_snapshots.unmanage(s) self.assert_called('POST', '/snapshots/1234/action', {'os-unmanage': None}) self._assert_request_id(snap) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_type_access.py0000664000175000017500000000333600000000000026563 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3 import volume_type_access cs = fakes.FakeClient() PROJECT_UUID = '11111111-1111-1111-111111111111' class TypeAccessTest(utils.TestCase): def test_list(self): access = cs.volume_type_access.list(volume_type='3') cs.assert_called('GET', '/types/3/os-volume-type-access') self._assert_request_id(access) for a in access: self.assertIsInstance(a, volume_type_access.VolumeTypeAccess) def test_add_project_access(self): access = cs.volume_type_access.add_project_access('3', PROJECT_UUID) cs.assert_called('POST', '/types/3/action', {'addProjectAccess': {'project': PROJECT_UUID}}) self._assert_request_id(access) def test_remove_project_access(self): access = cs.volume_type_access.remove_project_access('3', PROJECT_UUID) cs.assert_called('POST', '/types/3/action', {'removeProjectAccess': {'project': PROJECT_UUID}}) self._assert_request_id(access) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_types.py0000664000175000017500000001151300000000000025421 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3 import volume_types cs = fakes.FakeClient() class TypesTest(utils.TestCase): def test_list_types(self): tl = cs.volume_types.list() cs.assert_called('GET', '/types?is_public=None') self._assert_request_id(tl) for t in tl: self.assertIsInstance(t, volume_types.VolumeType) def test_list_types_not_public(self): t1 = cs.volume_types.list(is_public=None) cs.assert_called('GET', '/types?is_public=None') self._assert_request_id(t1) def test_create(self): t = cs.volume_types.create('test-type-3', 'test-type-3-desc') cs.assert_called('POST', '/types', {'volume_type': { 'name': 'test-type-3', 'description': 'test-type-3-desc', 'os-volume-type-access:is_public': True }}) self.assertIsInstance(t, volume_types.VolumeType) self._assert_request_id(t) def test_create_non_public(self): t = cs.volume_types.create('test-type-3', 'test-type-3-desc', False) cs.assert_called('POST', '/types', {'volume_type': { 'name': 'test-type-3', 'description': 'test-type-3-desc', 'os-volume-type-access:is_public': False }}) self.assertIsInstance(t, volume_types.VolumeType) self._assert_request_id(t) def test_update(self): t = cs.volume_types.update('1', 'test_type_1', 'test_desc_1', False) cs.assert_called('PUT', '/types/1', {'volume_type': {'name': 'test_type_1', 'description': 'test_desc_1', 'is_public': False}}) self.assertIsInstance(t, volume_types.VolumeType) self._assert_request_id(t) def test_update_name(self): """Test volume_type update shell command Verify that only name is updated and the description and is_public properties remains unchanged. """ # create volume_type with is_public True t = cs.volume_types.create('test-type-3', 'test_type-3-desc', True) self.assertTrue(t.is_public) # update name only t1 = cs.volume_types.update(t.id, 'test-type-2') cs.assert_called('PUT', '/types/3', {'volume_type': {'name': 'test-type-2', 'description': None}}) # verify that name is updated and the description # and is_public are the same. self.assertEqual('test-type-2', t1.name) self.assertEqual('test_type-3-desc', t1.description) self.assertTrue(t1.is_public) def test_get(self): t = cs.volume_types.get('1') cs.assert_called('GET', '/types/1') self.assertIsInstance(t, volume_types.VolumeType) self._assert_request_id(t) def test_default(self): t = cs.volume_types.default() cs.assert_called('GET', '/types/default') self.assertIsInstance(t, volume_types.VolumeType) self._assert_request_id(t) def test_set_key(self): t = cs.volume_types.get(1) res = t.set_keys({'k': 'v'}) cs.assert_called('POST', '/types/1/extra_specs', {'extra_specs': {'k': 'v'}}) self._assert_request_id(res) def test_unset_keys(self): t = cs.volume_types.get(1) res = t.unset_keys(['k']) cs.assert_called('DELETE', '/types/1/extra_specs/k') self._assert_request_id(res) def test_unset_multiple_keys(self): t = cs.volume_types.get(1) res = t.unset_keys(['k', 'm']) cs.assert_called_anytime('DELETE', '/types/1/extra_specs/k') cs.assert_called_anytime('DELETE', '/types/1/extra_specs/m') self._assert_request_id(res, count=2) def test_delete(self): t = cs.volume_types.delete(1) cs.assert_called('DELETE', '/types/1') self._assert_request_id(t) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_volume_backups.py0000664000175000017500000000461700000000000027303 0ustar00zuulzuul00000000000000# Copyright (c) 2016 Intel, 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 cinderclient import api_versions from cinderclient import exceptions as exc from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3 import volume_backups_restore class VolumesTest(utils.TestCase): def test_update(self): cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.9')) b = cs.backups.get('1234') backup = b.update(name='new-name') cs.assert_called( 'PUT', '/backups/1234', {'backup': {'name': 'new-name'}}) self._assert_request_id(backup) def test_pre_version(self): cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.8')) b = cs.backups.get('1234') self.assertRaises(exc.VersionNotFoundForAPIMethod, b.update, name='new-name') def test_restore(self): cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.0')) backup_id = '76a17945-3c6f-435c-975b-b5685db10b62' info = cs.restores.restore(backup_id) cs.assert_called('POST', '/backups/%s/restore' % backup_id) self.assertIsInstance(info, volume_backups_restore.VolumeBackupsRestore) self._assert_request_id(info) def test_restore_with_name(self): cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.0')) backup_id = '76a17945-3c6f-435c-975b-b5685db10b62' name = 'restore_vol' info = cs.restores.restore(backup_id, name=name) expected_body = {'restore': {'volume_id': None, 'name': name}} cs.assert_called('POST', '/backups/%s/restore' % backup_id, body=expected_body) self.assertIsInstance(info, volume_backups_restore.VolumeBackupsRestore) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_volume_backups_30.py0000664000175000017500000001366300000000000027606 0ustar00zuulzuul00000000000000# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. # 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 cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes cs = fakes.FakeClient() class VolumeBackupsTest(utils.TestCase): def test_create(self): vol = cs.backups.create('2b695faf-b963-40c8-8464-274008fbcef4') cs.assert_called('POST', '/backups') self._assert_request_id(vol) def test_create_full(self): vol = cs.backups.create('2b695faf-b963-40c8-8464-274008fbcef4', None, None, False) cs.assert_called('POST', '/backups') self._assert_request_id(vol) def test_create_incremental(self): vol = cs.backups.create('2b695faf-b963-40c8-8464-274008fbcef4', None, None, True) cs.assert_called('POST', '/backups') self._assert_request_id(vol) def test_create_force(self): vol = cs.backups.create('2b695faf-b963-40c8-8464-274008fbcef4', None, None, False, True) cs.assert_called('POST', '/backups') self._assert_request_id(vol) def test_create_snapshot(self): cs.backups.create('2b695faf-b963-40c8-8464-274008fbcef4', None, None, False, False, '3c706gbg-c074-51d9-9575-385119gcdfg5') cs.assert_called('POST', '/backups') def test_get(self): backup_id = '76a17945-3c6f-435c-975b-b5685db10b62' back = cs.backups.get(backup_id) cs.assert_called('GET', '/backups/%s' % backup_id) self._assert_request_id(back) def test_list(self): lst = cs.backups.list() cs.assert_called('GET', '/backups/detail') self._assert_request_id(lst) def test_list_with_pagination(self): lst = cs.backups.list(limit=2, marker=100) cs.assert_called('GET', '/backups/detail?limit=2&marker=100') self._assert_request_id(lst) def test_sorted_list(self): lst = cs.backups.list(sort="id") cs.assert_called('GET', '/backups/detail?sort=id') self._assert_request_id(lst) def test_sorted_list_by_data_timestamp(self): cs.backups.list(sort="data_timestamp") cs.assert_called('GET', '/backups/detail?sort=data_timestamp') def test_delete(self): b = cs.backups.list()[0] del_back = b.delete() cs.assert_called('DELETE', '/backups/76a17945-3c6f-435c-975b-b5685db10b62') self._assert_request_id(del_back) del_back = cs.backups.delete('76a17945-3c6f-435c-975b-b5685db10b62') cs.assert_called('DELETE', '/backups/76a17945-3c6f-435c-975b-b5685db10b62') self._assert_request_id(del_back) del_back = cs.backups.delete(b) cs.assert_called('DELETE', '/backups/76a17945-3c6f-435c-975b-b5685db10b62') self._assert_request_id(del_back) def test_force_delete_with_True_force_param_value(self): """Tests delete backup with force parameter set to True""" b = cs.backups.list()[0] del_back = b.delete(force=True) expected_body = {'os-force_delete': None} cs.assert_called('POST', '/backups/76a17945-3c6f-435c-975b-b5685db10b62/action', expected_body) self._assert_request_id(del_back) def test_force_delete_with_false_force_param_vaule(self): """To delete backup with force parameter set to False""" b = cs.backups.list()[0] del_back = b.delete(force=False) cs.assert_called('DELETE', '/backups/76a17945-3c6f-435c-975b-b5685db10b62') self._assert_request_id(del_back) del_back = cs.backups.delete('76a17945-3c6f-435c-975b-b5685db10b62') cs.assert_called('DELETE', '/backups/76a17945-3c6f-435c-975b-b5685db10b62') self._assert_request_id(del_back) del_back = cs.backups.delete(b) cs.assert_called('DELETE', '/backups/76a17945-3c6f-435c-975b-b5685db10b62') self._assert_request_id(del_back) def test_reset_state(self): b = cs.backups.list()[0] api = '/backups/76a17945-3c6f-435c-975b-b5685db10b62/action' st = b.reset_state(state='error') cs.assert_called('POST', api) self._assert_request_id(st) st = cs.backups.reset_state('76a17945-3c6f-435c-975b-b5685db10b62', state='error') cs.assert_called('POST', api) self._assert_request_id(st) st = cs.backups.reset_state(b, state='error') cs.assert_called('POST', api) self._assert_request_id(st) def test_record_export(self): backup_id = '76a17945-3c6f-435c-975b-b5685db10b62' export = cs.backups.export_record(backup_id) cs.assert_called('GET', '/backups/%s/export_record' % backup_id) self._assert_request_id(export) def test_record_import(self): backup_service = 'fake-backup-service' backup_url = 'fake-backup-url' expected_body = {'backup-record': {'backup_service': backup_service, 'backup_url': backup_url}} impt = cs.backups.import_record(backup_service, backup_url) cs.assert_called('POST', '/backups/import_record', expected_body) self._assert_request_id(impt) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_volume_encryption_types.py0000664000175000017500000001266600000000000031274 0ustar00zuulzuul00000000000000# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory # 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 cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3.volume_encryption_types import VolumeEncryptionType cs = fakes.FakeClient() FAKE_ENCRY_TYPE = {'provider': 'Test', 'key_size': None, 'cipher': None, 'control_location': None, 'volume_type_id': '65922555-7bc0-47e9-8d88-c7fdbcac4781', 'encryption_id': '62daf814-cf9b-401c-8fc8-f84d7850fb7c'} class VolumeEncryptionTypesTest(utils.TestCase): """ Test suite for the Volume Encryption Types Resource and Manager. """ def test_list(self): """ Unit test for VolumeEncryptionTypesManager.list Verify that a series of GET requests are made: - one GET request for the list of volume types - one GET request per volume type for encryption type information Verify that all returned information is :class: VolumeEncryptionType """ encryption_types = cs.volume_encryption_types.list() cs.assert_called_anytime('GET', '/types?is_public=None') cs.assert_called_anytime('GET', '/types/2/encryption') cs.assert_called_anytime('GET', '/types/1/encryption') for encryption_type in encryption_types: self.assertIsInstance(encryption_type, VolumeEncryptionType) self._assert_request_id(encryption_type) def test_get(self): """ Unit test for VolumeEncryptionTypesManager.get Verify that one GET request is made for the volume type encryption type information. Verify that returned information is :class: VolumeEncryptionType """ encryption_type = cs.volume_encryption_types.get(1) cs.assert_called('GET', '/types/1/encryption') self.assertIsInstance(encryption_type, VolumeEncryptionType) self._assert_request_id(encryption_type) def test_get_no_encryption(self): """ Unit test for VolumeEncryptionTypesManager.get Verify that a request on a volume type with no associated encryption type information returns a VolumeEncryptionType with no attributes. """ encryption_type = cs.volume_encryption_types.get(2) self.assertIsInstance(encryption_type, VolumeEncryptionType) self.assertFalse(hasattr(encryption_type, 'id'), 'encryption type has an id') self._assert_request_id(encryption_type) def test_create(self): """ Unit test for VolumeEncryptionTypesManager.create Verify that one POST request is made for the encryption type creation. Verify that encryption type creation returns a VolumeEncryptionType. """ result = cs.volume_encryption_types.create(2, {'provider': 'Test', 'key_size': None, 'cipher': None, 'control_location': None}) cs.assert_called('POST', '/types/2/encryption') self.assertIsInstance(result, VolumeEncryptionType) self._assert_request_id(result) def test_update(self): """ Unit test for VolumeEncryptionTypesManager.update Verify that one PUT request is made for encryption type update Verify that an empty encryption-type update returns the original encryption-type information. """ expected = {'id': 1, 'volume_type_id': 1, 'provider': 'test', 'cipher': 'test', 'key_size': 1, 'control_location': 'front-end'} result = cs.volume_encryption_types.update(1, {}) cs.assert_called('PUT', '/types/1/encryption/provider') self.assertEqual(expected, result, "empty update must yield original data") self._assert_request_id(result) def test_delete(self): """ Unit test for VolumeEncryptionTypesManager.delete Verify that one DELETE request is made for encryption type deletion Verify that encryption type deletion returns None """ result = cs.volume_encryption_types.delete(1) cs.assert_called('DELETE', '/types/1/encryption/provider') self.assertIsInstance(result, tuple) self.assertEqual(202, result[0].status_code) self._assert_request_id(result) def test___repr__(self): """ Unit test for VolumeEncryptionTypes.__repr__ Verify that one encryption type can be printed """ encry_type = VolumeEncryptionType(None, FAKE_ENCRY_TYPE) self.assertEqual( "" % FAKE_ENCRY_TYPE['encryption_id'], repr(encry_type)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_volume_transfers.py0000664000175000017500000000772600000000000027666 0ustar00zuulzuul00000000000000# Copyright 2018 FiberHome Telecommunication Technologies CO.,LTD # 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 cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes TRANSFER_URL = 'os-volume-transfer' TRANSFER_355_URL = 'volume-transfers' # Create calls need the right version of faked client v355cs = fakes.FakeClient(api_versions.APIVersion('3.55')) # Other calls fall back to API extension behavior v3cs = fakes.FakeClient(api_versions.APIVersion('3.0')) class VolumeTransfersTest(utils.TestCase): def test_create(self): vol = v3cs.transfers.create('1234') v3cs.assert_called('POST', '/%s' % TRANSFER_URL, body={'transfer': {'volume_id': '1234', 'name': None}}) self._assert_request_id(vol) def test_create_355(self): vol = v355cs.transfers.create('1234') v355cs.assert_called('POST', '/%s' % TRANSFER_355_URL, body={'transfer': {'volume_id': '1234', 'name': None, 'no_snapshots': False}}) self._assert_request_id(vol) def test_create_without_snapshots(self): vol = v355cs.transfers.create('1234', no_snapshots=True) v355cs.assert_called('POST', '/%s' % TRANSFER_355_URL, body={'transfer': {'volume_id': '1234', 'name': None, 'no_snapshots': True}}) self._assert_request_id(vol) def _test_get(self, client, expected_url): transfer_id = '5678' vol = client.transfers.get(transfer_id) client.assert_called('GET', '/%s/%s' % (expected_url, transfer_id)) self._assert_request_id(vol) def test_get(self): self._test_get(v3cs, TRANSFER_URL) def test_get_355(self): self._test_get(v355cs, TRANSFER_355_URL) def _test_list(self, client, expected_url): lst = client.transfers.list() client.assert_called('GET', '/%s/detail' % expected_url) self._assert_request_id(lst) def test_list(self): self._test_list(v3cs, TRANSFER_URL) def test_list_355(self): self._test_list(v355cs, TRANSFER_355_URL) def _test_delete(self, client, expected_url): url = '/%s/5678' % expected_url b = client.transfers.list()[0] vol = b.delete() client.assert_called('DELETE', url) self._assert_request_id(vol) vol = client.transfers.delete('5678') self._assert_request_id(vol) client.assert_called('DELETE', url) vol = client.transfers.delete(b) client.assert_called('DELETE', url) self._assert_request_id(vol) def test_delete(self): self._test_delete(v3cs, TRANSFER_URL) def test_delete_355(self): self._test_delete(v355cs, TRANSFER_355_URL) def _test_accept(self, client, expected_url): transfer_id = '5678' auth_key = '12345' vol = client.transfers.accept(transfer_id, auth_key) client.assert_called( 'POST', '/%s/%s/accept' % (expected_url, transfer_id)) self._assert_request_id(vol) def test_accept(self): self._test_accept(v3cs, TRANSFER_URL) def test_accept_355(self): self._test_accept(v355cs, TRANSFER_355_URL) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_volumes.py0000664000175000017500000002324300000000000025752 0ustar00zuulzuul00000000000000# Copyright 2016 FUJITSU LIMITED # Copyright (c) 2016 EMC Corporation # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from urllib import parse import ddt from cinderclient import api_versions from cinderclient import exceptions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3 import volume_snapshots from cinderclient.v3 import volumes @ddt.ddt class VolumesTest(utils.TestCase): def test_volume_manager_upload_to_image(self): expected = {'os-volume_upload_image': {'force': False, 'container_format': 'bare', 'disk_format': 'raw', 'image_name': 'name', 'visibility': 'public', 'protected': True}} api_version = api_versions.APIVersion('3.1') cs = fakes.FakeClient(api_version) manager = volumes.VolumeManager(cs) fake_volume = volumes.Volume(manager, {'id': 1234, 'name': 'sample-volume'}, loaded=True) fake_volume.upload_to_image(False, 'name', 'bare', 'raw', visibility='public', protected=True) cs.assert_called_anytime('POST', '/volumes/1234/action', body=expected) @ddt.data('3.39', '3.40') def test_revert_to_snapshot(self, version): api_version = api_versions.APIVersion(version) cs = fakes.FakeClient(api_version) manager = volumes.VolumeManager(cs) fake_snapshot = volume_snapshots.Snapshot( manager, {'id': 12345, 'name': 'fake-snapshot'}, loaded=True) fake_volume = volumes.Volume(manager, {'id': 1234, 'name': 'sample-volume'}, loaded=True) expected = {'revert': {'snapshot_id': 12345}} if version == '3.40': fake_volume.revert_to_snapshot(fake_snapshot) cs.assert_called_anytime('POST', '/volumes/1234/action', body=expected) else: self.assertRaises(exceptions.VersionNotFoundForAPIMethod, fake_volume.revert_to_snapshot, fake_snapshot) def test_create_volume(self): cs = fakes.FakeClient(api_versions.APIVersion('3.13')) vol = cs.volumes.create(1, group_id='1234', volume_type='5678') expected = {'volume': {'description': None, 'availability_zone': None, 'source_volid': None, 'snapshot_id': None, 'size': 1, 'name': None, 'imageRef': None, 'volume_type': '5678', 'metadata': {}, 'consistencygroup_id': None, 'group_id': '1234', 'backup_id': None}} cs.assert_called('POST', '/volumes', body=expected) self._assert_request_id(vol) def test_create_volume_with_hint(self): cs = fakes.FakeClient(api_versions.APIVersion('3.0')) vol = cs.volumes.create(1, scheduler_hints='uuid') expected = {'volume': {'description': None, 'availability_zone': None, 'source_volid': None, 'snapshot_id': None, 'size': 1, 'name': None, 'imageRef': None, 'volume_type': None, 'metadata': {}, 'consistencygroup_id': None, 'backup_id': None, }, 'OS-SCH-HNT:scheduler_hints': 'uuid'} cs.assert_called('POST', '/volumes', body=expected) self._assert_request_id(vol) @ddt.data((False, '/volumes/summary'), (True, '/volumes/summary?all_tenants=True')) def test_volume_summary(self, all_tenants_input): all_tenants, url = all_tenants_input cs = fakes.FakeClient(api_versions.APIVersion('3.12')) cs.volumes.summary(all_tenants=all_tenants) cs.assert_called('GET', url) def test_volume_manage_cluster(self): cs = fakes.FakeClient(api_versions.APIVersion('3.16')) vol = cs.volumes.manage(None, {'k': 'v'}, cluster='cluster1') expected = {'host': None, 'name': None, 'availability_zone': None, 'description': None, 'metadata': None, 'ref': {'k': 'v'}, 'volume_type': None, 'bootable': False, 'cluster': 'cluster1'} cs.assert_called('POST', '/os-volume-manage', {'volume': expected}) self._assert_request_id(vol) def test_volume_list_manageable(self): cs = fakes.FakeClient(api_versions.APIVersion('3.8')) cs.volumes.list_manageable('host1', detailed=False) cs.assert_called('GET', '/manageable_volumes?host=host1') def test_volume_list_manageable_detailed(self): cs = fakes.FakeClient(api_versions.APIVersion('3.8')) cs.volumes.list_manageable('host1', detailed=True) cs.assert_called('GET', '/manageable_volumes/detail?host=host1') def test_snapshot_list_manageable(self): cs = fakes.FakeClient(api_versions.APIVersion('3.8')) cs.volume_snapshots.list_manageable('host1', detailed=False) cs.assert_called('GET', '/manageable_snapshots?host=host1') def test_snapshot_list_manageable_detailed(self): cs = fakes.FakeClient(api_versions.APIVersion('3.8')) cs.volume_snapshots.list_manageable('host1', detailed=True) cs.assert_called('GET', '/manageable_snapshots/detail?host=host1') def test_snapshot_list_with_metadata(self): cs = fakes.FakeClient(api_versions.APIVersion('3.22')) cs.volume_snapshots.list(search_opts={'metadata': {'key1': 'val1'}}) expected = ("/snapshots/detail?metadata=%s" % parse.quote_plus("{'key1': 'val1'}")) cs.assert_called('GET', expected) def test_list_with_image_metadata(self): cs = fakes.FakeClient(api_versions.APIVersion('3.0')) cs.volumes.list(search_opts={'glance_metadata': {'key1': 'val1'}}) expected = ("/volumes/detail?glance_metadata=%s" % parse.quote_plus("{'key1': 'val1'}")) cs.assert_called('GET', expected) @ddt.data(True, False) def test_get_pools_filter_by_name(self, detail): cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.33')) vol = cs.volumes.get_pools(detail, {'name': 'pool1'}) request_url = '/scheduler-stats/get_pools?name=pool1' if detail: request_url = '/scheduler-stats/get_pools?detail=True&name=pool1' cs.assert_called('GET', request_url) self._assert_request_id(vol) def test_migrate_host(self): cs = fakes.FakeClient(api_versions.APIVersion('3.0')) v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.migrate_volume(v, 'host_dest', False, False) cs.assert_called('POST', '/volumes/1234/action', {'os-migrate_volume': {'host': 'host_dest', 'force_host_copy': False, 'lock_volume': False}}) self._assert_request_id(vol) def test_migrate_with_lock_volume(self): cs = fakes.FakeClient(api_versions.APIVersion('3.0')) v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.migrate_volume(v, 'dest', False, True) cs.assert_called('POST', '/volumes/1234/action', {'os-migrate_volume': {'host': 'dest', 'force_host_copy': False, 'lock_volume': True}}) self._assert_request_id(vol) def test_migrate_cluster(self): cs = fakes.FakeClient(api_versions.APIVersion('3.16')) v = cs.volumes.get('fake') self._assert_request_id(v) vol = cs.volumes.migrate_volume(v, 'host_dest', False, False, 'cluster_dest') cs.assert_called('POST', '/volumes/fake/action', {'os-migrate_volume': {'cluster': 'cluster_dest', 'force_host_copy': False, 'lock_volume': False}}) self._assert_request_id(vol) @ddt.data(False, True) def test_reimage(self, reimage_reserved): cs = fakes.FakeClient(api_versions.APIVersion('3.68')) v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.reimage(v, '1', reimage_reserved) cs.assert_called('POST', '/volumes/1234/action', {'os-reimage': {'image_id': '1', 'reimage_reserved': reimage_reserved}}) self._assert_request_id(vol) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/tests/unit/v3/test_volumes_base.py0000664000175000017500000003155500000000000026751 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import api_versions from cinderclient.tests.unit import utils from cinderclient.tests.unit.v3 import fakes from cinderclient.v3.volumes import Volume cs = fakes.FakeClient(api_version=api_versions.APIVersion('3.0')) class VolumesTest(utils.TestCase): """Block Storage API v3.0""" def test_list_volumes_with_marker_limit(self): lst = cs.volumes.list(marker=1234, limit=2) cs.assert_called('GET', '/volumes/detail?limit=2&marker=1234') self._assert_request_id(lst) def test__list(self): # There only 2 volumes available for our tests, so we set limit to 2. limit = 2 url = "/volumes?limit=%s" % limit response_key = "volumes" fake_volume1234 = Volume(self, {'id': 1234, 'name': 'sample-volume'}, loaded=True) fake_volume5678 = Volume(self, {'id': 5678, 'name': 'sample-volume2'}, loaded=True) fake_volumes = [fake_volume1234, fake_volume5678] # osapi_max_limit is 1000 by default. If limit is less than # osapi_max_limit, we can get 2 volumes back. volumes = cs.volumes._list(url, response_key, limit=limit) self._assert_request_id(volumes) cs.assert_called('GET', url) self.assertEqual(fake_volumes, volumes) # When we change the osapi_max_limit to 1, the next link should be # generated. If limit equals 2 and id passed as an argument, we can # still get 2 volumes back, because the method _list will fetch the # volume from the next link. cs.client.osapi_max_limit = 1 volumes = cs.volumes._list(url, response_key, limit=limit) self.assertEqual(fake_volumes, volumes) self._assert_request_id(volumes) cs.client.osapi_max_limit = 1000 def test_create_volume(self): vol = cs.volumes.create(1) cs.assert_called('POST', '/volumes') self._assert_request_id(vol) def test_delete_volume(self): v = cs.volumes.list()[0] del_v = v.delete() cs.assert_called('DELETE', '/volumes/1234') self._assert_request_id(del_v) del_v = cs.volumes.delete('1234') cs.assert_called('DELETE', '/volumes/1234') self._assert_request_id(del_v) del_v = cs.volumes.delete(v) cs.assert_called('DELETE', '/volumes/1234') self._assert_request_id(del_v) def test_attach(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.attach(v, 1, '/dev/vdc', mode='ro') cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_attach_to_host(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.attach(v, None, None, host_name='test', mode='rw') cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_detach(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.detach(v, 'abc123') cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_reserve(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.reserve(v) cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_unreserve(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.unreserve(v) cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_begin_detaching(self): v = cs.volumes.get('1234') cs.volumes.begin_detaching(v) cs.assert_called('POST', '/volumes/1234/action') def test_roll_detaching(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.roll_detaching(v) cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_initialize_connection(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.initialize_connection(v, {}) cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_terminate_connection(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.terminate_connection(v, {}) cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_set_metadata(self): vol = cs.volumes.set_metadata(1234, {'k1': 'v2', 'тест': 'тест'}) cs.assert_called('POST', '/volumes/1234/metadata', {'metadata': {'k1': 'v2', 'тест': 'тест'}}) self._assert_request_id(vol) def test_delete_metadata(self): keys = ['key1'] vol = cs.volumes.delete_metadata(1234, keys) cs.assert_called('DELETE', '/volumes/1234/metadata/key1') self._assert_request_id(vol) def test_extend(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.extend(v, 2) cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_reset_state(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.reset_state(v, 'in-use', attach_status='detached') cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_reset_state_migration_status(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.reset_state(v, 'in-use', attach_status='detached', migration_status='none') cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_get_encryption_metadata(self): vol = cs.volumes.get_encryption_metadata('1234') cs.assert_called('GET', '/volumes/1234/encryption') self._assert_request_id(vol) def test_migrate(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.migrate_volume(v, 'dest', False, False) cs.assert_called('POST', '/volumes/1234/action', {'os-migrate_volume': {'host': 'dest', 'force_host_copy': False, 'lock_volume': False}}) self._assert_request_id(vol) def test_migrate_with_lock_volume(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.migrate_volume(v, 'dest', False, True) cs.assert_called('POST', '/volumes/1234/action', {'os-migrate_volume': {'host': 'dest', 'force_host_copy': False, 'lock_volume': True}}) self._assert_request_id(vol) def test_metadata_update_all(self): vol = cs.volumes.update_all_metadata(1234, {'k1': 'v1'}) cs.assert_called('PUT', '/volumes/1234/metadata', {'metadata': {'k1': 'v1'}}) self._assert_request_id(vol) def test_readonly_mode_update(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.update_readonly_flag(v, True) cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_retype(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.retype(v, 'foo', 'on-demand') cs.assert_called('POST', '/volumes/1234/action', {'os-retype': {'new_type': 'foo', 'migration_policy': 'on-demand'}}) self._assert_request_id(vol) def test_set_bootable(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.set_bootable(v, True) cs.assert_called('POST', '/volumes/1234/action') self._assert_request_id(vol) def test_volume_manage(self): vol = cs.volumes.manage('host1', {'k': 'v'}) expected = {'host': 'host1', 'name': None, 'availability_zone': None, 'description': None, 'metadata': None, 'ref': {'k': 'v'}, 'volume_type': None, 'bootable': False} cs.assert_called('POST', '/os-volume-manage', {'volume': expected}) self._assert_request_id(vol) def test_volume_manage_bootable(self): vol = cs.volumes.manage('host1', {'k': 'v'}, bootable=True) expected = {'host': 'host1', 'name': None, 'availability_zone': None, 'description': None, 'metadata': None, 'ref': {'k': 'v'}, 'volume_type': None, 'bootable': True} cs.assert_called('POST', '/os-volume-manage', {'volume': expected}) self._assert_request_id(vol) def test_volume_list_manageable(self): cs.volumes.list_manageable('host1', detailed=False) cs.assert_called('GET', '/os-volume-manage?host=host1') def test_volume_list_manageable_detailed(self): cs.volumes.list_manageable('host1', detailed=True) cs.assert_called('GET', '/os-volume-manage/detail?host=host1') def test_volume_unmanage(self): v = cs.volumes.get('1234') self._assert_request_id(v) vol = cs.volumes.unmanage(v) cs.assert_called('POST', '/volumes/1234/action', {'os-unmanage': None}) self._assert_request_id(vol) def test_snapshot_manage(self): vol = cs.volume_snapshots.manage('volume_id1', {'k': 'v'}) expected = {'volume_id': 'volume_id1', 'name': None, 'description': None, 'metadata': None, 'ref': {'k': 'v'}} cs.assert_called('POST', '/os-snapshot-manage', {'snapshot': expected}) self._assert_request_id(vol) def test_snapshot_list_manageable(self): cs.volume_snapshots.list_manageable('host1', detailed=False) cs.assert_called('GET', '/os-snapshot-manage?host=host1') def test_snapshot_list_manageable_detailed(self): cs.volume_snapshots.list_manageable('host1', detailed=True) cs.assert_called('GET', '/os-snapshot-manage/detail?host=host1') def test_get_pools(self): vol = cs.volumes.get_pools('') cs.assert_called('GET', '/scheduler-stats/get_pools') self._assert_request_id(vol) def test_get_pools_detail(self): vol = cs.volumes.get_pools('--detail') cs.assert_called('GET', '/scheduler-stats/get_pools?detail=True') self._assert_request_id(vol) class FormatSortParamTestCase(utils.TestCase): def test_format_sort_empty_input(self): for s in [None, '', []]: self.assertIsNone(cs.volumes._format_sort_param(s)) def test_format_sort_string_single_key(self): s = 'id' self.assertEqual('id', cs.volumes._format_sort_param(s)) def test_format_sort_string_single_key_and_dir(self): s = 'id:asc' self.assertEqual('id:asc', cs.volumes._format_sort_param(s)) def test_format_sort_string_multiple(self): s = 'id:asc,status,size:desc' self.assertEqual('id:asc,status,size:desc', cs.volumes._format_sort_param(s)) def test_format_sort_string_mappings(self): s = 'id:asc,name,size:desc' self.assertEqual('id:asc,display_name,size:desc', cs.volumes._format_sort_param(s)) def test_format_sort_whitespace_trailing_comma(self): s = ' id : asc ,status, size:desc,' self.assertEqual('id:asc,status,size:desc', cs.volumes._format_sort_param(s)) def test_format_sort_list_of_strings(self): s = ['id:asc', 'status', 'size:desc'] self.assertEqual('id:asc,status,size:desc', cs.volumes._format_sort_param(s)) def test_format_sort_invalid_direction(self): for s in ['id:foo', 'id:asc,status,size:foo', ['id', 'status', 'size:foo']]: self.assertRaises(ValueError, cs.volumes._format_sort_param, s) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/utils.py0000664000175000017500000002331300000000000021706 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import os from urllib import parse import uuid import prettytable import stevedore from cinderclient import exceptions def arg(*args, **kwargs): """Decorator for CLI args.""" def _decorator(func): add_arg(func, *args, **kwargs) return func return _decorator def exclusive_arg(group_name, *args, **kwargs): """Decorator for CLI mutually exclusive args.""" def _decorator(func): required = kwargs.pop('required', None) add_exclusive_arg(func, group_name, required, *args, **kwargs) return func return _decorator def env(*vars, **kwargs): """ returns the first environment variable set if none are non-empty, defaults to '' or keyword arg default """ for v in vars: value = os.environ.get(v, None) if value: return value return kwargs.get('default', '') def add_arg(f, *args, **kwargs): """Bind CLI arguments to a shell.py `do_foo` function.""" if not hasattr(f, 'arguments'): f.arguments = [] # NOTE(sirp): avoid dups that can occur when the module is shared across # tests. if (args, kwargs) not in f.arguments: # Because of the semantics of decorator composition if we just append # to the options list positional options will appear to be backwards. f.arguments.insert(0, (args, kwargs)) def add_exclusive_arg(f, group_name, required, *args, **kwargs): """Bind CLI mutally exclusive arguments to a shell.py `do_foo` function.""" if not hasattr(f, 'exclusive_args'): f.exclusive_args = collections.defaultdict(list) # Default required to False f.exclusive_args['__required__'] = collections.defaultdict(bool) # NOTE(sirp): avoid dups that can occur when the module is shared across # tests. if (args, kwargs) not in f.exclusive_args[group_name]: # Because of the semantics of decorator composition if we just append # to the options list positional options will appear to be backwards. f.exclusive_args[group_name].insert(0, (args, kwargs)) if required is not None: f.exclusive_args['__required__'][group_name] = required def unauthenticated(f): """ Adds 'unauthenticated' attribute to decorated function. Usage: @unauthenticated def mymethod(f): ... """ f.unauthenticated = True return f def isunauthenticated(f): """ Checks to see if the function is marked as not requiring authentication with the @unauthenticated decorator. Returns True if decorator is set to True, False otherwise. """ return getattr(f, 'unauthenticated', False) def _print(pt, order): print(pt.get_string(sortby=order)) def print_list(objs, fields, exclude_unavailable=False, formatters=None, sortby_index=0): '''Prints a list of objects. @param objs: Objects to print @param fields: Fields on each object to be printed @param exclude_unavailable: Boolean to decide if unavailable fields are removed @param formatters: Custom field formatters @param sortby_index: Results sorted against the key in the fields list at this index; if None then the object order is not altered ''' formatters = formatters or {} mixed_case_fields = ['serverId'] removed_fields = [] rows = [] for o in objs: row = [] for field in fields: if field in removed_fields: continue if field in formatters: row.append(formatters[field](o)) else: if field in mixed_case_fields: field_name = field.replace(' ', '_') else: field_name = field.lower().replace(' ', '_') if isinstance(o, dict) and field in o: data = o[field] else: if not hasattr(o, field_name) and exclude_unavailable: removed_fields.append(field) continue else: data = getattr(o, field_name, '') if data is None: data = '-' if isinstance(data, str) and "\r" in data: data = data.replace("\r", " ") row.append(data) rows.append(row) for f in removed_fields: fields.remove(f) pt = prettytable.PrettyTable((f for f in fields), caching=False) pt.align = 'l' for row in rows: count = 0 # Converts unicode values in dictionary to string for part in row: count = count + 1 if isinstance(part, dict): row[count - 1] = part pt.add_row(row) if sortby_index is None: order_by = None else: order_by = fields[sortby_index] _print(pt, order_by) def build_query_param(params, sort=False): """parse list to url query parameters""" if not params: return "" if not sort: param_list = list(params.items()) else: param_list = list(sorted(params.items())) query_string = parse.urlencode( [(k, v) for (k, v) in param_list if v not in (None, '')]) # urllib's parse library used to adhere to RFC 2396 until # python 3.7. The library moved from RFC 2396 to RFC 3986 # for quoting URL strings in python 3.7 and '~' is now # included in the set of reserved characters. [1] # # Below ensures "~" is never encoded. See LP 1784728 [2] for more details. # [1] https://docs.python.org/3/library/urllib.parse.html#url-quoting # [2] https://bugs.launchpad.net/python-cinderclient/+bug/1784728 query_string = query_string.replace("%7E=", "~=") if query_string: query_string = "?%s" % (query_string,) return query_string def _pretty_format_dict(data_dict): formatted_data = [] for k in sorted(data_dict): formatted_data.append("%s : %s" % (k, data_dict[k])) return "\n".join(formatted_data) def print_dict(d, property="Property", formatters=None): pt = prettytable.PrettyTable([property, 'Value'], caching=False) pt.align = 'l' formatters = formatters or {} for r in d.items(): r = list(r) if r[0] in formatters: if isinstance(r[1], dict): r[1] = _pretty_format_dict(r[1]) if isinstance(r[1], str) and "\r" in r[1]: r[1] = r[1].replace("\r", " ") pt.add_row(r) _print(pt, property) def find_resource(manager, name_or_id, **kwargs): """Helper for the _find_* methods.""" is_group = kwargs.pop('is_group', False) # first try to get entity as integer id try: if isinstance(name_or_id, int) or name_or_id.isdigit(): if is_group: return manager.get(int(name_or_id), **kwargs) return manager.get(int(name_or_id)) except exceptions.NotFound: pass else: # now try to get entity as uuid try: uuid.UUID(name_or_id) if is_group: return manager.get(name_or_id, **kwargs) return manager.get(name_or_id) except (ValueError, exceptions.NotFound): pass try: try: resource = getattr(manager, 'resource_class', None) name_attr = resource.NAME_ATTR if resource else 'name' if is_group: kwargs[name_attr] = name_or_id return manager.find(**kwargs) return manager.find(**{name_attr: name_or_id}) except exceptions.NotFound: pass # finally try to find entity by human_id try: if is_group: kwargs['human_id'] = name_or_id return manager.find(**kwargs) return manager.find(human_id=name_or_id) except exceptions.NotFound: msg = "No %s with a name or ID of '%s' exists." % \ (manager.resource_class.__name__.lower(), name_or_id) raise exceptions.CommandError(msg) except exceptions.NoUniqueMatch: msg = ("Multiple %s matches found for '%s', use an ID to be more" " specific." % (manager.resource_class.__name__.lower(), name_or_id)) raise exceptions.CommandError(msg) def find_volume(cs, volume): """Get a volume by name or ID.""" return find_resource(cs.volumes, volume) def safe_issubclass(*args): """Like issubclass, but will just return False if not a class.""" try: if issubclass(*args): return True except TypeError: pass return False def _load_entry_point(ep_name, name=None): """Try to load the entry point ep_name that matches name.""" mgr = stevedore.NamedExtensionManager( namespace=ep_name, names=[name], # Ignore errors on load on_load_failure_callback=lambda mgr, entry_point, error: None, ) try: return mgr[name].plugin except KeyError: pass def get_function_name(func): return "%s.%s" % (func.__module__, func.__qualname__) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1645793255.281896 python-cinderclient-8.3.0/cinderclient/v3/0000775000175000017500000000000000000000000020522 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/__init__.py0000664000175000017500000000126400000000000022636 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient.v3.client import Client # noqa ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/attachments.py0000664000175000017500000000742700000000000023421 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Attachment interface.""" from cinderclient import api_versions from cinderclient import base class VolumeAttachment(base.Resource): """An attachment is a connected volume.""" def __repr__(self): """Obj to Str method.""" return "" % self.id class VolumeAttachmentManager(base.ManagerWithFind): resource_class = VolumeAttachment @api_versions.wraps('3.27') def create(self, volume_id, connector, instance_id=None, mode='null'): """Create a attachment for specified volume.""" body = {'attachment': {'volume_uuid': volume_id, 'connector': connector}} if instance_id: body['attachment']['instance_uuid'] = instance_id if self.api_version >= api_versions.APIVersion("3.54"): if mode and mode != 'null': body['attachment']['mode'] = mode retval = self._create('/attachments', body, 'attachment') return retval.to_dict() @api_versions.wraps('3.27') def delete(self, attachment): """Delete an attachment by ID.""" return self._delete("/attachments/%s" % base.getid(attachment)) @api_versions.wraps('3.27') def list(self, detailed=False, search_opts=None, marker=None, limit=None, sort=None): """List all attachments.""" resource_type = "attachments" url = self._build_list_url(resource_type, detailed=detailed, search_opts=search_opts, marker=marker, limit=limit, sort=sort) return self._list(url, resource_type, limit=limit) @api_versions.wraps('3.27') def show(self, id): """Attachment show. :param id: Attachment ID. """ url = '/attachments/%s' % id resp, body = self.api.client.get(url) return self.resource_class(self, body['attachment'], loaded=True, resp=resp) @api_versions.wraps('3.27') def update(self, id, connector): """Attachment update.""" body = {'attachment': {'connector': connector}} resp = self._update('/attachments/%s' % id, body) # NOTE(jdg): This kinda sucks, # create returns a dict, but update returns an object :( return self.resource_class(self, resp['attachment'], loaded=True, resp=resp) @api_versions.wraps('3.44') def complete(self, attachment): """Mark the attachment as completed.""" resp, body = self._action_return_resp_and_body('os-complete', attachment, None) return resp def _action_return_resp_and_body(self, action, attachment, info=None, **kwargs): """Perform a attachments "action" and return response headers and body. """ body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/attachments/%s/action' % base.getid(attachment) return self.api.client.post(url, body=body) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/availability_zones.py0000664000175000017500000000262300000000000024767 0ustar00zuulzuul00000000000000# Copyright 2011-2013 OpenStack Foundation # Copyright 2013 IBM Corp. # 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. """Availability Zone interface (v3 extension)""" from cinderclient import base class AvailabilityZone(base.Resource): NAME_ATTR = 'display_name' def __repr__(self): return "" % self.zoneName class AvailabilityZoneManager(base.ManagerWithFind): """Manage :class:`AvailabilityZone` resources.""" resource_class = AvailabilityZone def list(self, detailed=False): """Lists all availability zones. :rtype: list of :class:`AvailabilityZone` """ if detailed is True: return self._list("/os-availability-zone/detail", "availabilityZoneInfo") else: return self._list("/os-availability-zone", "availabilityZoneInfo") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/capabilities.py0000664000175000017500000000236200000000000023530 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Hitachi Data Systems, 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. """Capabilities interface (v3 extension)""" from cinderclient import base class Capabilities(base.Resource): NAME_ATTR = 'name' def __repr__(self): return "" % self._info.get('namespace') class CapabilitiesManager(base.Manager): """Manage :class:`Capabilities` resources.""" resource_class = Capabilities def get(self, host): """Show backend volume stats and properties. :param host: Specified backend to obtain volume stats and properties. :rtype: :class:`Capabilities` """ return self._get('/capabilities/%s' % host, None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/cgsnapshots.py0000664000175000017500000000744400000000000023441 0ustar00zuulzuul00000000000000# Copyright (C) 2012 - 2014 EMC Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """cgsnapshot interface (v3 extension).""" from cinderclient.apiclient import base as common_base from cinderclient import base from cinderclient import utils class Cgsnapshot(base.Resource): """A cgsnapshot is snapshot of a consistency group.""" def __repr__(self): return "" % self.id def delete(self): """Delete this cgsnapshot.""" return self.manager.delete(self) def update(self, **kwargs): """Update the name or description for this cgsnapshot.""" return self.manager.update(self, **kwargs) class CgsnapshotManager(base.ManagerWithFind): """Manage :class:`Cgsnapshot` resources.""" resource_class = Cgsnapshot def create(self, consistencygroup_id, name=None, description=None, user_id=None, project_id=None): """Creates a cgsnapshot. :param consistencygroup: Name or uuid of a consistency group :param name: Name of the cgsnapshot :param description: Description of the cgsnapshot :param user_id: User id derived from context :param project_id: Project id derived from context :rtype: :class:`Cgsnapshot` """ body = {'cgsnapshot': {'consistencygroup_id': consistencygroup_id, 'name': name, 'description': description, 'user_id': user_id, 'project_id': project_id, 'status': "creating", }} return self._create('/cgsnapshots', body, 'cgsnapshot') def get(self, cgsnapshot_id): """Get a cgsnapshot. :param cgsnapshot_id: The ID of the cgsnapshot to get. :rtype: :class:`Cgsnapshot` """ return self._get("/cgsnapshots/%s" % cgsnapshot_id, "cgsnapshot") def list(self, detailed=True, search_opts=None): """Lists all cgsnapshots. :rtype: list of :class:`Cgsnapshot` """ query_string = utils.build_query_param(search_opts) detail = "" if detailed: detail = "/detail" return self._list("/cgsnapshots%s%s" % (detail, query_string), "cgsnapshots") def delete(self, cgsnapshot): """Delete a cgsnapshot. :param cgsnapshot: The :class:`Cgsnapshot` to delete. """ return self._delete("/cgsnapshots/%s" % base.getid(cgsnapshot)) def update(self, cgsnapshot, **kwargs): """Update the name or description for a cgsnapshot. :param cgsnapshot: The :class:`Cgsnapshot` to update. """ if not kwargs: return body = {"cgsnapshot": kwargs} return self._update("/cgsnapshots/%s" % base.getid(cgsnapshot), body) def _action(self, action, cgsnapshot, info=None, **kwargs): """Perform a cgsnapshot "action." """ body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/cgsnapshots/%s/action' % base.getid(cgsnapshot) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/client.py0000664000175000017500000001451600000000000022361 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import api_versions from cinderclient import client from cinderclient.v3 import attachments from cinderclient.v3 import availability_zones from cinderclient.v3 import capabilities from cinderclient.v3 import cgsnapshots from cinderclient.v3 import clusters from cinderclient.v3 import consistencygroups from cinderclient.v3 import default_types from cinderclient.v3 import group_snapshots from cinderclient.v3 import group_types from cinderclient.v3 import groups from cinderclient.v3 import limits from cinderclient.v3 import messages from cinderclient.v3 import pools from cinderclient.v3 import qos_specs from cinderclient.v3 import quota_classes from cinderclient.v3 import quotas from cinderclient.v3 import resource_filters from cinderclient.v3 import services from cinderclient.v3 import volume_backups from cinderclient.v3 import volume_backups_restore from cinderclient.v3 import volume_encryption_types from cinderclient.v3 import volume_snapshots from cinderclient.v3 import volume_transfers from cinderclient.v3 import volume_type_access from cinderclient.v3 import volume_types from cinderclient.v3 import volumes from cinderclient.v3 import workers class Client(object): """Top-level object to access the OpenStack Volume API. Create an instance with your creds:: >>> client = Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) Then call methods on its managers:: >>> client.volumes.list() ... """ def __init__(self, username=None, api_key=None, project_id=None, auth_url='', insecure=False, timeout=None, tenant_id=None, proxy_tenant_id=None, proxy_token=None, region_name=None, endpoint_type='publicURL', extensions=None, service_type='volumev3', service_name=None, volume_service_name=None, os_endpoint=None, retries=0, http_log_debug=False, cacert=None, cert=None, auth_system='keystone', auth_plugin=None, session=None, api_version=None, logger=None, **kwargs): # FIXME(comstud): Rename the api_key argument above when we # know it's not being used as keyword argument password = api_key self.version = '3.0' self.limits = limits.LimitsManager(self) self.api_version = api_version or api_versions.APIVersion(self.version) self.volumes = volumes.VolumeManager(self) self.volume_snapshots = volume_snapshots.SnapshotManager(self) self.volume_types = volume_types.VolumeTypeManager(self) self.group_types = group_types.GroupTypeManager(self) self.volume_type_access = \ volume_type_access.VolumeTypeAccessManager(self) self.volume_encryption_types = \ volume_encryption_types.VolumeEncryptionTypeManager(self) self.default_types = default_types.DefaultVolumeTypeManager(self) self.qos_specs = qos_specs.QoSSpecsManager(self) self.quota_classes = quota_classes.QuotaClassSetManager(self) self.quotas = quotas.QuotaSetManager(self) self.backups = volume_backups.VolumeBackupManager(self) self.messages = messages.MessageManager(self) self.resource_filters = resource_filters.ResourceFilterManager(self) self.restores = volume_backups_restore.VolumeBackupRestoreManager(self) self.transfers = volume_transfers.VolumeTransferManager(self) self.services = services.ServiceManager(self) self.clusters = clusters.ClusterManager(self) self.workers = workers.WorkerManager(self) self.consistencygroups = consistencygroups.\ ConsistencygroupManager(self) self.groups = groups.GroupManager(self) self.cgsnapshots = cgsnapshots.CgsnapshotManager(self) self.group_snapshots = group_snapshots.GroupSnapshotManager(self) self.availability_zones = \ availability_zones.AvailabilityZoneManager(self) self.pools = pools.PoolManager(self) self.capabilities = capabilities.CapabilitiesManager(self) self.attachments = \ attachments.VolumeAttachmentManager(self) # Add in any extensions... if extensions: for extension in extensions: if extension.manager_class: setattr(self, extension.name, extension.manager_class(self)) self.client = client._construct_http_client( username=username, password=password, project_id=project_id, auth_url=auth_url, insecure=insecure, timeout=timeout, tenant_id=tenant_id, proxy_tenant_id=tenant_id, proxy_token=proxy_token, region_name=region_name, endpoint_type=endpoint_type, service_type=service_type, service_name=service_name, volume_service_name=volume_service_name, os_endpoint=os_endpoint, retries=retries, http_log_debug=http_log_debug, cacert=cacert, cert=cert, auth_system=auth_system, auth_plugin=auth_plugin, session=session, api_version=self.api_version, logger=logger, **kwargs) def authenticate(self): """Authenticate against the server. Normally this is called automatically when you first access the API, but you can call this method to force authentication right now. Returns on success; raises :exc:`exceptions.Unauthorized` if the credentials are wrong. """ self.client.authenticate() def get_volume_api_version_from_endpoint(self): return self.client.get_volume_api_version_from_endpoint() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/clusters.py0000664000175000017500000000643700000000000022752 0ustar00zuulzuul00000000000000# Copyright (c) 2016 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Interface to clusters API """ from cinderclient import api_versions from cinderclient import base class Cluster(base.Resource): def __repr__(self): return "" % (self.name, self.id) class ClusterManager(base.ManagerWithFind): resource_class = Cluster base_url = '/clusters' def _build_url(self, url_path=None, **kwargs): url = self.base_url + ('/' + url_path if url_path else '') filters = {'%s=%s' % (k, v) for k, v in kwargs.items() if v} if filters: url = "%s?%s" % (url, "&".join(filters)) return url @api_versions.wraps("3.7") def list(self, name=None, binary=None, is_up=None, disabled=None, num_hosts=None, num_down_hosts=None, detailed=False): """Clustered Service list. :param name: filter by cluster name. :param binary: filter by cluster binary. :param is_up: filtering by up/down status. :param disabled: filtering by disabled status. :param num_hosts: filtering by number of hosts. :param num_down_hosts: filtering by number of hosts that are down. :param detailed: retrieve simple or detailed list. """ url_path = 'detail' if detailed else None url = self._build_url(url_path, name=name, binary=binary, is_up=is_up, disabled=disabled, num_hosts=num_hosts, num_down_hosts=num_down_hosts) return self._list(url, 'clusters') @api_versions.wraps("3.7") def show(self, name, binary=None): """Clustered Service show. :param name: Cluster name. :param binary: Clustered service binary. """ url = self._build_url(name, binary=binary) resp, body = self.api.client.get(url) return self.resource_class(self, body['cluster'], loaded=True, resp=resp) @api_versions.wraps("3.7") def update(self, name, binary, disabled, disabled_reason=None): """Enable or disable a clustered service. :param name: Cluster name. :param binary: Clustered service binary. :param disabled: Boolean determining desired disabled status. :param disabled_reason: Value to pass as disabled reason. """ url_path = 'disable' if disabled else 'enable' url = self._build_url(url_path) body = {'name': name, 'binary': binary} if disabled and disabled_reason: body['disabled_reason'] = disabled_reason result = self._update(url, body) return self.resource_class(self, result['cluster'], loaded=True, resp=result.request_ids) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/consistencygroups.py0000664000175000017500000001364000000000000024701 0ustar00zuulzuul00000000000000# Copyright (C) 2012 - 2014 EMC Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Consistencygroup interface (v3 extension).""" from cinderclient.apiclient import base as common_base from cinderclient import base from cinderclient import utils class Consistencygroup(base.Resource): """A Consistencygroup of volumes.""" def __repr__(self): return "" % self.id def delete(self, force='False'): """Delete this consistency group.""" return self.manager.delete(self, force) def update(self, **kwargs): """Update the name or description for this consistency group.""" return self.manager.update(self, **kwargs) class ConsistencygroupManager(base.ManagerWithFind): """Manage :class:`Consistencygroup` resources.""" resource_class = Consistencygroup def create(self, volume_types, name=None, description=None, user_id=None, project_id=None, availability_zone=None): """Creates a consistency group. :param name: Name of the ConsistencyGroup :param description: Description of the ConsistencyGroup :param volume_types: Types of volume :param user_id: User id derived from context :param project_id: Project id derived from context :param availability_zone: Availability Zone to use :rtype: :class:`Consistencygroup` """ body = {'consistencygroup': {'name': name, 'description': description, 'volume_types': volume_types, 'user_id': user_id, 'project_id': project_id, 'availability_zone': availability_zone, 'status': "creating", }} return self._create('/consistencygroups', body, 'consistencygroup') def create_from_src(self, cgsnapshot_id, source_cgid, name=None, description=None, user_id=None, project_id=None): """Creates a consistency group from a cgsnapshot or a source CG. :param cgsnapshot_id: UUID of a CGSnapshot :param source_cgid: UUID of a source CG :param name: Name of the ConsistencyGroup :param description: Description of the ConsistencyGroup :param user_id: User id derived from context :param project_id: Project id derived from context :rtype: A dictionary containing Consistencygroup metadata """ body = {'consistencygroup-from-src': {'name': name, 'description': description, 'cgsnapshot_id': cgsnapshot_id, 'source_cgid': source_cgid, 'user_id': user_id, 'project_id': project_id, 'status': "creating", }} self.run_hooks('modify_body_for_update', body, 'consistencygroup-from-src') resp, body = self.api.client.post( "/consistencygroups/create_from_src", body=body) return common_base.DictWithMeta(body['consistencygroup'], resp) def get(self, group_id): """Get a consistency group. :param group_id: The ID of the consistency group to get. :rtype: :class:`Consistencygroup` """ return self._get("/consistencygroups/%s" % group_id, "consistencygroup") def list(self, detailed=True, search_opts=None): """Lists all consistency groups. :rtype: list of :class:`Consistencygroup` """ query_string = utils.build_query_param(search_opts) detail = "" if detailed: detail = "/detail" return self._list("/consistencygroups%s%s" % (detail, query_string), "consistencygroups") def delete(self, consistencygroup, force=False): """Delete a consistency group. :param Consistencygroup: The :class:`Consistencygroup` to delete. """ body = {'consistencygroup': {'force': force}} self.run_hooks('modify_body_for_action', body, 'consistencygroup') url = '/consistencygroups/%s/delete' % base.getid(consistencygroup) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) def update(self, consistencygroup, **kwargs): """Update the name or description for a consistency group. :param Consistencygroup: The :class:`Consistencygroup` to update. """ if not kwargs: return body = {"consistencygroup": kwargs} return self._update("/consistencygroups/%s" % base.getid(consistencygroup), body) def _action(self, action, consistencygroup, info=None, **kwargs): """Perform a consistency group "action." """ body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/consistencygroups/%s/action' % base.getid(consistencygroup) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1645793255.285896 python-cinderclient-8.3.0/cinderclient/v3/contrib/0000775000175000017500000000000000000000000022162 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/contrib/__init__.py0000664000175000017500000000000000000000000024261 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/contrib/list_extensions.py0000664000175000017500000000256300000000000025774 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import base from cinderclient import utils class ListExtResource(base.Resource): @property def summary(self): descr = self.description.strip() if not descr: return '??' lines = descr.split("\n") if len(lines) == 1: return lines[0] else: return lines[0] + "..." class ListExtManager(base.Manager): resource_class = ListExtResource def show_all(self): return self._list("/extensions", 'extensions') def do_list_extensions(client, _args): """Lists all available os-api extensions.""" extensions = client.list_extensions.show_all() fields = ["Name", "Summary", "Alias", "Updated"] utils.print_list(extensions, fields) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/default_types.py0000664000175000017500000000371600000000000023753 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """Default Volume Type interface.""" from cinderclient import base class DefaultVolumeType(base.Resource): """Default volume types for projects.""" def __repr__(self): return "" % self.project_id class DefaultVolumeTypeManager(base.ManagerWithFind): """Manage :class:`DefaultVolumeType` resources.""" resource_class = DefaultVolumeType def create(self, volume_type, project_id): """Creates a default volume type for a project :param volume_type: Name or ID of the volume type :param project_id: Project to set default type for """ body = { "default_type": { "volume_type": volume_type } } return self._create_update_with_base_url( 'v3/default-types/%s' % project_id, body, response_key='default_type') def list(self, project_id=None): """List the default types.""" url = 'v3/default-types' response_key = "default_types" if project_id: url += '/' + project_id response_key = "default_type" return self._get_all_with_base_url(url, response_key) def delete(self, project_id): """Removes the default volume type for a project :param project_id: The ID of the project to unset default for. """ return self._delete_with_base_url('v3/default-types/%s' % project_id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/group_snapshots.py0000664000175000017500000001116100000000000024332 0ustar00zuulzuul00000000000000# Copyright (C) 2016 EMC Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """group snapshot interface (v3).""" from cinderclient import api_versions from cinderclient.apiclient import base as common_base from cinderclient import base from cinderclient import utils class GroupSnapshot(base.Resource): """A group snapshot is a snapshot of a group.""" def __repr__(self): return "" % self.id def delete(self): """Delete this group snapshot.""" return self.manager.delete(self) def update(self, **kwargs): """Update the name or description for this group snapshot.""" return self.manager.update(self, **kwargs) def reset_state(self, state): """Reset the group snapshot's state with specified one.""" return self.manager.reset_state(self, state) class GroupSnapshotManager(base.ManagerWithFind): """Manage :class:`GroupSnapshot` resources.""" resource_class = GroupSnapshot @api_versions.wraps('3.14') def create(self, group_id, name=None, description=None, user_id=None, project_id=None): """Creates a group snapshot. :param group_id: Name or uuid of a group :param name: Name of the group snapshot :param description: Description of the group snapshot :param user_id: User id derived from context :param project_id: Project id derived from context :rtype: :class:`GroupSnapshot` """ body = { 'group_snapshot': { 'group_id': group_id, 'name': name, 'description': description, } } return self._create('/group_snapshots', body, 'group_snapshot') @api_versions.wraps('3.14') def get(self, group_snapshot_id): """Get a group snapshot. :param group_snapshot_id: The ID of the group snapshot to get. :rtype: :class:`GroupSnapshot` """ return self._get("/group_snapshots/%s" % group_snapshot_id, "group_snapshot") @api_versions.wraps('3.19') def reset_state(self, group_snapshot, state): """Update the provided group snapshot with the provided state. :param group_snapshot: The :class:`GroupSnapshot` to set the state. :param state: The state of the group snapshot to be set. """ body = {'status': state} if state else {} return self._action('reset_status', group_snapshot, body) @api_versions.wraps('3.14') def list(self, detailed=True, search_opts=None): """Lists all group snapshots. :param detailed: list detailed info or not :param search_opts: search options :rtype: list of :class:`GroupSnapshot` """ query_string = utils.build_query_param(search_opts, sort=True) detail = "" if detailed: detail = "/detail" return self._list("/group_snapshots%s%s" % (detail, query_string), "group_snapshots") @api_versions.wraps('3.14') def delete(self, group_snapshot): """Delete a group_snapshot. :param group_snapshot: The :class:`GroupSnapshot` to delete. """ return self._delete("/group_snapshots/%s" % base.getid(group_snapshot)) @api_versions.wraps('3.14') def update(self, group_snapshot, **kwargs): """Update the name or description for a group_snapshot. :param group_snapshot: The :class:`GroupSnapshot` to update. """ if not kwargs: return body = {"group_snapshot": kwargs} return self._update("/group_snapshots/%s" % base.getid(group_snapshot), body) def _action(self, action, group_snapshot, info=None, **kwargs): """Perform a group_snapshot action.""" body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/group_snapshots/%s/action' % base.getid(group_snapshot) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/group_types.py0000664000175000017500000001177600000000000023470 0ustar00zuulzuul00000000000000# Copyright (c) 2016 EMC Corporation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. """Group Type interface.""" from urllib import parse from cinderclient import api_versions from cinderclient import base class GroupType(base.Resource): """A Group Type is the type of group to be created.""" def __repr__(self): return "" % self.name @property def is_public(self): """ Provide a user-friendly accessor to is_public """ return self._info.get("is_public", self._info.get("is_public", 'N/A')) @api_versions.wraps("3.11") def get_keys(self): """Get group specs from a group type. :param type: The :class:`GroupType` to get specs from """ _resp, body = self.manager.api.client.get( "/group_types/%s/group_specs" % base.getid(self)) return body["group_specs"] @api_versions.wraps("3.11") def set_keys(self, metadata): """Set group specs on a group type. :param type : The :class:`GroupType` to set spec on :param metadata: A dict of key/value pairs to be set """ body = {'group_specs': metadata} return self.manager._create( "/group_types/%s/group_specs" % base.getid(self), body, "group_specs", return_raw=True) @api_versions.wraps("3.11") def unset_keys(self, keys): """Unset specs on a group type. :param type_id: The :class:`GroupType` to unset spec on :param keys: A list of keys to be unset """ for k in keys: resp = self.manager._delete( "/group_types/%s/group_specs/%s" % ( base.getid(self), k)) if resp: return resp class GroupTypeManager(base.ManagerWithFind): """Manage :class:`GroupType` resources.""" resource_class = GroupType @api_versions.wraps("3.11") def list(self, search_opts=None, is_public=None): """Lists all group types. :rtype: list of :class:`GroupType`. """ if not search_opts: search_opts = dict() query_string = '' if 'is_public' not in search_opts: search_opts['is_public'] = is_public query_string = "?%s" % parse.urlencode(search_opts) return self._list("/group_types%s" % (query_string), "group_types") @api_versions.wraps("3.11") def get(self, group_type): """Get a specific group type. :param group_type: The ID of the :class:`GroupType` to get. :rtype: :class:`GroupType` """ return self._get("/group_types/%s" % base.getid(group_type), "group_type") @api_versions.wraps("3.11") def default(self): """Get the default group type. :rtype: :class:`GroupType` """ return self._get("/group_types/default", "group_type") @api_versions.wraps("3.11") def delete(self, group_type): """Deletes a specific group_type. :param group_type: The name or ID of the :class:`GroupType` to get. """ return self._delete("/group_types/%s" % base.getid(group_type)) @api_versions.wraps("3.11") def create(self, name, description=None, is_public=True): """Creates a group type. :param name: Descriptive name of the group type :param description: Description of the group type :param is_public: Group type visibility :rtype: :class:`GroupType` """ body = { "group_type": { "name": name, "description": description, "is_public": is_public, } } return self._create("/group_types", body, "group_type") @api_versions.wraps("3.11") def update(self, group_type, name=None, description=None, is_public=None): """Update the name and/or description for a group type. :param group_type: The ID of the :class:`GroupType` to update. :param name: Descriptive name of the group type. :param description: Description of the group type. :rtype: :class:`GroupType` """ body = { "group_type": { "name": name, "description": description } } if is_public is not None: body["group_type"]["is_public"] = is_public return self._update("/group_types/%s" % base.getid(group_type), body, response_key="group_type") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/groups.py0000664000175000017500000002315300000000000022417 0ustar00zuulzuul00000000000000# Copyright (C) 2016 EMC Corporation. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Group interface (v3 extension).""" from cinderclient import api_versions from cinderclient.apiclient import base as common_base from cinderclient import base from cinderclient import utils class Group(base.Resource): """A Group of volumes.""" def __repr__(self): return "" % self.id def delete(self, delete_volumes=False): """Delete this group.""" return self.manager.delete(self, delete_volumes) def update(self, **kwargs): """Update the name or description for this group.""" return self.manager.update(self, **kwargs) def reset_state(self, state): """Reset the group's state with specified one""" return self.manager.reset_state(self, state) def enable_replication(self): """Enables replication for this group.""" return self.manager.enable_replication(self) def disable_replication(self): """Disables replication for this group.""" return self.manager.disable_replication(self) def failover_replication(self, allow_attached_volume=False, secondary_backend_id=None): """Fails over replication for this group.""" return self.manager.failover_replication(self, allow_attached_volume, secondary_backend_id) def list_replication_targets(self): """Lists replication targets for this group.""" return self.manager.list_replication_targets(self) class GroupManager(base.ManagerWithFind): """Manage :class:`Group` resources.""" resource_class = Group @api_versions.wraps('3.13') def create(self, group_type, volume_types, name=None, description=None, user_id=None, project_id=None, availability_zone=None): """Creates a group. :param group_type: Type of the Group :param volume_types: Types of volume :param name: Name of the Group :param description: Description of the Group :param user_id: User id derived from context :param project_id: Project id derived from context :param availability_zone: Availability Zone to use :rtype: :class:`Group` """ body = {'group': {'name': name, 'description': description, 'group_type': group_type, 'volume_types': volume_types.split(','), 'availability_zone': availability_zone, }} return self._create('/groups', body, 'group') @api_versions.wraps('3.20') def reset_state(self, group, state): """Update the provided group with the provided state. :param group: The :class:`Group` to set the state. :param state: The state of the group to be set. """ body = {'status': state} if state else {} return self._action('reset_status', group, body) @api_versions.wraps('3.14') def create_from_src(self, group_snapshot_id, source_group_id, name=None, description=None, user_id=None, project_id=None): """Creates a group from a group snapshot or a source group. :param group_snapshot_id: UUID of a GroupSnapshot :param source_group_id: UUID of a source Group :param name: Name of the Group :param description: Description of the Group :param user_id: User id derived from context :param project_id: Project id derived from context :rtype: A dictionary containing Group metadata """ # NOTE(wanghao): According the API schema in cinder side, client # should NOT specify the group_snapshot_id and source_group_id at # same time, even one of them is None. if group_snapshot_id: create_key = 'group_snapshot_id' create_value = group_snapshot_id elif source_group_id: create_key = 'source_group_id' create_value = source_group_id body = {'create-from-src': {'name': name, 'description': description, create_key: create_value}} self.run_hooks('modify_body_for_action', body, 'create-from-src') resp, body = self.api.client.post( "/groups/action", body=body) return common_base.DictWithMeta(body['group'], resp) @api_versions.wraps('3.13') def get(self, group_id, **kwargs): """Get a group. :param group_id: The ID of the group to get. :rtype: :class:`Group` """ query_params = kwargs query_string = utils.build_query_param(query_params, sort=True) return self._get("/groups/%s" % group_id + query_string, "group") @api_versions.wraps('3.13') def list(self, detailed=True, search_opts=None, list_volume=False): """Lists all groups. :rtype: list of :class:`Group` """ if list_volume: if not search_opts: search_opts = {} search_opts['list_volume'] = True query_string = utils.build_query_param(search_opts, sort=True) detail = "" if detailed: detail = "/detail" return self._list("/groups%s%s" % (detail, query_string), "groups") @api_versions.wraps('3.13') def delete(self, group, delete_volumes=False): """Delete a group. :param group: the :class:`Group` to delete. :param delete_volumes: delete volumes in the group. """ body = {'delete': {'delete-volumes': delete_volumes}} self.run_hooks('modify_body_for_action', body, 'group') url = '/groups/%s/action' % base.getid(group) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) @api_versions.wraps('3.13') def update(self, group, **kwargs): """Update the name or description for a group. :param Group: The :class:`Group` to update. """ if not kwargs: return body = {"group": kwargs} return self._update("/groups/%s" % base.getid(group), body) def _action(self, action, group, info=None, **kwargs): """Perform a group "action." :param action: an action to be performed on the group :param group: a group to perform the action on :param info: details of the action :param **kwargs: other parameters """ body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/groups/%s/action' % base.getid(group) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) @api_versions.wraps('3.38') def enable_replication(self, group): """Enables replication for a group. :param group: the :class:`Group` to enable replication. """ body = {'enable_replication': {}} self.run_hooks('modify_body_for_action', body, 'group') url = '/groups/%s/action' % base.getid(group) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) @api_versions.wraps('3.38') def disable_replication(self, group): """disables replication for a group. :param group: the :class:`Group` to disable replication. """ body = {'disable_replication': {}} self.run_hooks('modify_body_for_action', body, 'group') url = '/groups/%s/action' % base.getid(group) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) @api_versions.wraps('3.38') def failover_replication(self, group, allow_attached_volume=False, secondary_backend_id=None): """fails over replication for a group. :param group: the :class:`Group` to failover. :param allow attached volumes: allow attached volumes in the group. :param secondary_backend_id: secondary backend id. """ body = { 'failover_replication': { 'allow_attached_volume': allow_attached_volume, 'secondary_backend_id': secondary_backend_id } } self.run_hooks('modify_body_for_action', body, 'group') url = '/groups/%s/action' % base.getid(group) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) @api_versions.wraps('3.38') def list_replication_targets(self, group): """List replication targets for a group. :param group: the :class:`Group` to list replication targets. """ body = {'list_replication_targets': {}} self.run_hooks('modify_body_for_action', body, 'group') url = '/groups/%s/action' % base.getid(group) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/limits.py0000664000175000017500000000564000000000000022402 0ustar00zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from cinderclient import base from cinderclient import utils class Limits(base.Resource): """A collection of RateLimit and AbsoluteLimit objects.""" def __repr__(self): return "" @property def absolute(self): for (name, value) in list(self._info['absolute'].items()): yield AbsoluteLimit(name, value) @property def rate(self): for group in self._info['rate']: uri = group['uri'] regex = group['regex'] for rate in group['limit']: yield RateLimit(rate['verb'], uri, regex, rate['value'], rate['remaining'], rate['unit'], rate['next-available']) class RateLimit(object): """Data model that represents a flattened view of a single rate limit.""" def __init__(self, verb, uri, regex, value, remain, unit, next_available): self.verb = verb self.uri = uri self.regex = regex self.value = value self.remain = remain self.unit = unit self.next_available = next_available def __eq__(self, other): return self.uri == other.uri \ and self.regex == other.regex \ and self.value == other.value \ and self.verb == other.verb \ and self.remain == other.remain \ and self.unit == other.unit \ and self.next_available == other.next_available def __repr__(self): return "" % (self.verb, self.uri) class AbsoluteLimit(object): """Data model that represents a single absolute limit.""" def __init__(self, name, value): self.name = name self.value = value def __eq__(self, other): return self.value == other.value and self.name == other.name def __repr__(self): return "" % (self.name) class LimitsManager(base.Manager): """Manager object used to interact with limits resource.""" resource_class = Limits def get(self, tenant_id=None): """Get a specific extension. :rtype: :class:`Limits` """ opts = {} if tenant_id: opts['tenant_id'] = tenant_id query_string = utils.build_query_param(opts) return self._get("/limits%s" % query_string, "limits") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/messages.py0000664000175000017500000000502700000000000022707 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Message interface (v3 extension).""" from cinderclient import api_versions from cinderclient import base class Message(base.Resource): NAME_ATTR = 'id' def __repr__(self): return "" % self.id def delete(self): """Delete this message.""" return self.manager.delete(self) class MessageManager(base.ManagerWithFind): """Manage :class:`Message` resources.""" resource_class = Message @api_versions.wraps('3.3') def get(self, message_id): """Get a message. :param message_id: The ID of the message to get. :rtype: :class:`Message` """ return self._get("/messages/%s" % message_id, "message") @api_versions.wraps('3.3', '3.4') def list(self, **kwargs): """Lists all messages. :rtype: list of :class:`Message` """ resource_type = "messages" url = self._build_list_url(resource_type, detailed=False) return self._list(url, resource_type) @api_versions.wraps('3.5') def list(self, search_opts=None, marker=None, limit=None, # noqa: F811 sort=None): """Lists all messages. :param search_opts: Search options to filter out volumes. :param marker: Begin returning volumes that appear later in the volume list than that represented by this volume id. :param limit: Maximum number of volumes to return. :param sort: Sort information :rtype: list of :class:`Message` """ resource_type = "messages" url = self._build_list_url(resource_type, detailed=False, search_opts=search_opts, marker=marker, limit=limit, sort=sort) return self._list(url, resource_type, limit=limit) @api_versions.wraps('3.3') def delete(self, message): """Delete a message.""" loc = "/messages/%s" % base.getid(message) return self._delete(loc) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/pools.py0000664000175000017500000000421100000000000022226 0ustar00zuulzuul00000000000000# Copyright (C) 2015 Hewlett-Packard Development Company, L.P. # 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. """Pools interface (v3 extension)""" from cinderclient import base class Pool(base.Resource): NAME_ATTR = 'name' def __repr__(self): return "" % self.name class PoolManager(base.Manager): """Manage :class:`Pool` resources.""" resource_class = Pool def list(self, detailed=False): """Lists all :rtype: list of :class:`Pool` """ if detailed is True: pools = self._list("/scheduler-stats/get_pools?detail=True", "pools") # Other than the name, all of the pool data is buried below in # a 'capabilities' dictionary. In order to be consistent with the # get-pools command line, these elements are moved up a level to # be attributes of the pool itself. for pool in pools: if hasattr(pool, 'capabilities'): for k, v in pool.capabilities.items(): setattr(pool, k, v) # Remove the capabilities dictionary since all of its # elements have been copied up to the containing pool del pool.capabilities return pools else: pools = self._list("/scheduler-stats/get_pools", "pools") # avoid cluttering the basic pool list with capabilities dict for pool in pools: if hasattr(pool, 'capabilities'): del pool.capabilities return pools ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/qos_specs.py0000664000175000017500000001172500000000000023101 0ustar00zuulzuul00000000000000# Copyright (c) 2013 eBay Inc. # Copyright (c) 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. """ QoS Specs interface. """ from cinderclient.apiclient import base as common_base from cinderclient import base class QoSSpecs(base.Resource): """QoS specs entity represents quality-of-service parameters/requirements. A QoS specs is a set of parameters or requirements for quality-of-service purpose, which can be associated with volume types (for now). In future, QoS specs may be extended to be associated other entities, such as single volume. """ def __repr__(self): return "" % self.name def delete(self): return self.manager.delete(self) class QoSSpecsManager(base.ManagerWithFind): """ Manage :class:`QoSSpecs` resources. """ resource_class = QoSSpecs def list(self, search_opts=None): """Get a list of all qos specs. :rtype: list of :class:`QoSSpecs`. """ return self._list("/qos-specs", "qos_specs") def get(self, qos_specs): """Get a specific qos specs. :param qos_specs: The ID of the :class:`QoSSpecs` to get. :rtype: :class:`QoSSpecs` """ return self._get("/qos-specs/%s" % base.getid(qos_specs), "qos_specs") def delete(self, qos_specs, force=False): """Delete a specific qos specs. :param qos_specs: The ID of the :class:`QoSSpecs` to be removed. :param force: Flag that indicates whether to delete target qos specs if it was in-use. """ return self._delete("/qos-specs/%s?force=%s" % (base.getid(qos_specs), force)) def create(self, name, specs): """Create a qos specs. :param name: Descriptive name of the qos specs, must be unique :param specs: A dict of key/value pairs to be set :rtype: :class:`QoSSpecs` """ body = { "qos_specs": { "name": name, } } body["qos_specs"].update(specs) return self._create("/qos-specs", body, "qos_specs") def set_keys(self, qos_specs, specs): """Add/Update keys in qos specs. :param qos_specs: The ID of qos specs :param specs: A dict of key/value pairs to be set :rtype: :class:`QoSSpecs` """ body = { "qos_specs": {} } body["qos_specs"].update(specs) return self._update("/qos-specs/%s" % qos_specs, body) def unset_keys(self, qos_specs, specs): """Remove keys from a qos specs. :param qos_specs: The ID of qos specs :param specs: A list of key to be unset :rtype: :class:`QoSSpecs` """ body = {'keys': specs} return self._update("/qos-specs/%s/delete_keys" % qos_specs, body) def get_associations(self, qos_specs): """Get associated entities of a qos specs. :param qos_specs: The id of the :class: `QoSSpecs` :return: a list of entities that associated with specific qos specs. """ return self._list("/qos-specs/%s/associations" % base.getid(qos_specs), "qos_associations") def associate(self, qos_specs, vol_type_id): """Associate a volume type with specific qos specs. :param qos_specs: The qos specs to be associated with :param vol_type_id: The volume type id to be associated with """ resp, body = self.api.client.get( "/qos-specs/%s/associate?vol_type_id=%s" % (base.getid(qos_specs), vol_type_id)) return common_base.TupleWithMeta((resp, body), resp) def disassociate(self, qos_specs, vol_type_id): """Disassociate qos specs from volume type. :param qos_specs: The qos specs to be associated with :param vol_type_id: The volume type id to be associated with """ resp, body = self.api.client.get( "/qos-specs/%s/disassociate?vol_type_id=%s" % (base.getid(qos_specs), vol_type_id)) return common_base.TupleWithMeta((resp, body), resp) def disassociate_all(self, qos_specs): """Disassociate all entities from specific qos specs. :param qos_specs: The qos specs to be associated with """ resp, body = self.api.client.get( "/qos-specs/%s/disassociate_all" % base.getid(qos_specs)) return common_base.TupleWithMeta((resp, body), resp) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/quota_classes.py0000664000175000017500000000316300000000000023745 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import base class QuotaClassSet(base.Resource): @property def id(self): """Needed by base.Resource to self-refresh and be indexed.""" return self.class_name def update(self, *args, **kwargs): return self.manager.update(self.class_name, *args, **kwargs) class QuotaClassSetManager(base.Manager): resource_class = QuotaClassSet def get(self, class_name): return self._get("/os-quota-class-sets/%s" % (class_name), "quota_class_set") def update(self, class_name, **updates): quota_class_set = {} for update in updates: quota_class_set[update] = updates[update] result = self._update('/os-quota-class-sets/%s' % (class_name), {'quota_class_set': quota_class_set}) return self.resource_class(self, result['quota_class_set'], loaded=True, resp=result.request_ids) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/quotas.py0000664000175000017500000000415600000000000022416 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from cinderclient import base class QuotaSet(base.Resource): @property def id(self): """Needed by base.Resource to self-refresh and be indexed.""" return self.tenant_id def update(self, *args, **kwargs): return self.manager.update(self.tenant_id, *args, **kwargs) class QuotaSetManager(base.Manager): resource_class = QuotaSet def get(self, tenant_id, usage=False): if hasattr(tenant_id, 'tenant_id'): tenant_id = tenant_id.tenant_id return self._get("/os-quota-sets/%s?usage=%s" % (tenant_id, usage), "quota_set") def update(self, tenant_id, **updates): skip_validation = updates.pop('skip_validation', True) body = {'quota_set': {'tenant_id': tenant_id}} for update in updates: body['quota_set'][update] = updates[update] request_url = '/os-quota-sets/%s' % tenant_id if not skip_validation: request_url += '?skip_validation=False' result = self._update(request_url, body) return self.resource_class(self, result['quota_set'], loaded=True, resp=result.request_ids) def defaults(self, tenant_id): return self._get('/os-quota-sets/%s/defaults' % tenant_id, 'quota_set') def delete(self, tenant_id): if hasattr(tenant_id, 'tenant_id'): tenant_id = tenant_id.tenant_id return self._delete("/os-quota-sets/%s" % tenant_id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/resource_filters.py0000664000175000017500000000232300000000000024453 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Resource filters interface.""" from cinderclient import api_versions from cinderclient import base class ResourceFilter(base.Resource): NAME_ATTR = 'resource' def __repr__(self): return "" % self.resource class ResourceFilterManager(base.ManagerWithFind): """Manage :class:`ResourceFilter` resources.""" resource_class = ResourceFilter @api_versions.wraps('3.33') def list(self, resource=None): """List all resource filters.""" url = '/resource_filters' if resource is not None: url += '?resource=%s' % resource return self._list(url, "resource_filters") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/services.py0000664000175000017500000001072000000000000022717 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ service interface """ from cinderclient import api_versions from cinderclient import base class Service(base.Resource): def __repr__(self): return "" % (self.binary, self.host) class LogLevel(base.Resource): def __repr__(self): return '' % ( self.binary, self.host, self.prefix, self.level) class ServiceManagerBase(base.ManagerWithFind): resource_class = Service def list(self, host=None, binary=None): """ Describes service list for host. :param host: destination host name. :param binary: service binary. """ url = "/os-services" filters = [] if host: filters.append("host=%s" % host) if binary: filters.append("binary=%s" % binary) if filters: url = "%s?%s" % (url, "&".join(filters)) return self._list(url, "services") def enable(self, host, binary): """Enable the service specified by hostname and binary.""" body = {"host": host, "binary": binary} result = self._update("/os-services/enable", body) return self.resource_class(self, result, resp=result.request_ids) def disable(self, host, binary): """Disable the service specified by hostname and binary.""" body = {"host": host, "binary": binary} result = self._update("/os-services/disable", body) return self.resource_class(self, result, resp=result.request_ids) def disable_log_reason(self, host, binary, reason): """Disable the service with reason.""" body = {"host": host, "binary": binary, "disabled_reason": reason} result = self._update("/os-services/disable-log-reason", body) return self.resource_class(self, result, resp=result.request_ids) def freeze_host(self, host): """Freeze the service specified by hostname.""" body = {"host": host} return self._update("/os-services/freeze", body) def thaw_host(self, host): """Thaw the service specified by hostname.""" body = {"host": host} return self._update("/os-services/thaw", body) def failover_host(self, host, backend_id): """Failover a replicated backend by hostname.""" body = {"host": host, "backend_id": backend_id} return self._update("/os-services/failover_host", body) class ServiceManager(ServiceManagerBase): @api_versions.wraps("3.0") def server_api_version(self): """Returns the API Version supported by the server. :return: Returns response obj for a server that supports microversions. Returns an empty list for Liberty and prior Cinder servers. """ try: return self._get_with_base_url("", response_key='versions') except LookupError: return [] @api_versions.wraps("3.32") def set_log_levels(self, level, binary, server, prefix): """Set log level for services.""" body = {'level': level, 'binary': binary, 'server': server, 'prefix': prefix} return self._update("/os-services/set-log", body) @api_versions.wraps("3.32") def get_log_levels(self, binary, server, prefix): """Get log levels for services.""" body = {'binary': binary, 'server': server, 'prefix': prefix} response = self._update("/os-services/get-log", body) log_levels = [] for entry in response['log_levels']: entry_levels = sorted(entry['levels'].items(), key=lambda x: x[0]) for prefix, level in entry_levels: log_dict = {'binary': entry['binary'], 'host': entry['host'], 'prefix': prefix, 'level': level} log_levels.append(LogLevel(self, log_dict, loaded=True)) return log_levels ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/shell.py0000664000175000017500000032371500000000000022216 0ustar00zuulzuul00000000000000# Copyright (c) 2013-2014 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import collections import os from oslo_utils import strutils import cinderclient from cinderclient import api_versions from cinderclient import base from cinderclient import exceptions from cinderclient import shell_utils from cinderclient import utils from cinderclient.v3.shell_base import * # noqa from cinderclient.v3.shell_base import CheckSizeArgForCreate FILTER_DEPRECATED = ("This option is deprecated and will be removed in " "newer release. Please use '--filters' option which " "is introduced since 3.33 instead.") class AppendFilters(argparse.Action): filters = [] def __call__(self, parser, namespace, values, option_string): AppendFilters.filters.append(values[0]) @api_versions.wraps('3.33') @utils.arg('--resource', metavar='', default=None, help='Show enabled filters for specified resource. Default=None.') def do_list_filters(cs, args): """List enabled filters. Symbol '~' after filter key means it supports inexact filtering. """ filters = cs.resource_filters.list(resource=args.resource) shell_utils.print_resource_filter_list(filters) @utils.arg('--filters', action=AppendFilters, type=str, nargs='*', start_version='3.52', metavar='', default=None, help="Filter key and value pairs. Admin only.") def do_type_list(cs, args): """Lists available 'volume types'. (Only admin and tenant users will see private types) """ # pylint: disable=function-redefined search_opts = {} # Update search option with `filters` if AppendFilters.filters: search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) vtypes = cs.volume_types.list(search_opts=search_opts) shell_utils.print_volume_type_list(vtypes) with cs.volume_types.completion_cache( 'uuid', cinderclient.v3.volume_types.VolumeType, mode="w"): for vtype in vtypes: cs.volume_types.write_to_completion_cache('uuid', vtype.id) with cs.volume_types.completion_cache( 'name', cinderclient.v3.volume_types.VolumeType, mode="w"): for vtype in vtypes: cs.volume_types.write_to_completion_cache('name', vtype.name) AppendFilters.filters = [] @utils.arg('--all-tenants', metavar='', nargs='?', type=int, const=1, default=0, help='Shows details for all tenants. Admin only.') @utils.arg('--all_tenants', nargs='?', type=int, const=1, help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help="Filters results by a name. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--status', metavar='', default=None, help="Filters results by a status. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--volume-id', metavar='', default=None, help="Filters results by a volume ID. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--volume_id', help=argparse.SUPPRESS) @utils.arg('--marker', metavar='', default=None, help='Begin returning backups that appear later in the backup ' 'list than that represented by this id. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, help='Maximum number of backups to return. Default=None.') @utils.arg('--sort', metavar='[:]', default=None, help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) @utils.arg('--filters', action=AppendFilters, type=str, nargs='*', start_version='3.33', metavar='', default=None, help="Filter key and value pairs. Please use 'cinder list-filters' " "to check enabled filters from server. Use 'key~=value' for " "inexact filtering if the key supports. Default=None.") @utils.arg('--with-count', type=bool, default=False, const=True, nargs='?', start_version='3.45', metavar='', help="Show total number of backup entities. This is useful when " "pagination is applied in the request.") def do_backup_list(cs, args): """Lists all backups.""" # pylint: disable=function-redefined show_count = True if hasattr( args, 'with_count') and args.with_count else False search_opts = { 'all_tenants': args.all_tenants, 'name': args.name, 'status': args.status, 'volume_id': args.volume_id, } # Update search option with `filters` if AppendFilters.filters: search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) total_count = 0 if show_count: search_opts['with_count'] = args.with_count backups, total_count = cs.backups.list(search_opts=search_opts, marker=args.marker, limit=args.limit, sort=args.sort) else: backups = cs.backups.list(search_opts=search_opts, marker=args.marker, limit=args.limit, sort=args.sort) shell_utils.translate_volume_snapshot_keys(backups) columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Object Count', 'Container'] if cs.api_version >= api_versions.APIVersion('3.56'): columns.append('User ID') if args.sort: sortby_index = None else: sortby_index = 0 utils.print_list(backups, columns, sortby_index=sortby_index) if show_count: print("Backup in total: %s" % total_count) with cs.backups.completion_cache( 'uuid', cinderclient.v3.volume_backups.VolumeBackup, mode="w"): for backup in backups: cs.backups.write_to_completion_cache('uuid', backup.id) with cs.backups.completion_cache( 'name', cinderclient.v3.volume_backups.VolumeBackup, mode='w'): for backup in backups: if backup.name is not None: cs.backups.write_to_completion_cache('name', backup.name) AppendFilters.filters = [] @utils.arg('backup', metavar='', help='Name or ID of backup to restore.') @utils.arg('--volume', metavar='', default=None, help='Name or ID of existing volume to which to restore. ' 'This is mutually exclusive with --name and takes priority. ' 'Default=None.') @utils.arg('--name', metavar='', default=None, help='Use the name for new volume creation to restore. ' 'This is mutually exclusive with --volume and --volume ' 'takes priority. ' 'Default=None.') @utils.arg('--volume-type', metavar='', default=None, start_version='3.47', help='Volume type for the new volume creation to restore. This ' 'option is not valid when used with the "volume" option. ' 'Default=None.') @utils.arg('--availability-zone', metavar='', default=None, start_version='3.47', help='AZ for the new volume creation to restore. By default it ' 'will be the same as backup AZ. This option is not valid when ' 'used with the "volume" option. Default=None.') def do_backup_restore(cs, args): """Restores a backup.""" if args.volume: volume_id = utils.find_volume(cs, args.volume).id if args.name: args.name = None print('Mutually exclusive options are specified simultaneously: ' '"volume" and "name". The volume option takes priority.') else: volume_id = None volume_type = getattr(args, 'volume_type', None) az = getattr(args, 'availability_zone', None) if (volume_type or az) and args.volume: msg = ('The "volume-type" and "availability-zone" options are not ' 'valid when used with the "volume" option.') raise exceptions.ClientException(code=1, message=msg) backup = shell_utils.find_backup(cs, args.backup) info = {"backup_id": backup.id} if volume_type or (az and az != backup.availability_zone): # Implement restoring a backup to a newly created volume of a # specific volume type or in a different AZ by using the # volume-create API. The default volume name matches the pattern # cinder uses (see I23730834058d88e30be62624ada3b24cdaeaa6f3). volume_name = args.name or 'restore_backup_%s' % backup.id volume = cs.volumes.create(size=backup.size, name=volume_name, volume_type=volume_type, availability_zone=az, backup_id=backup.id) info['volume_id'] = volume._info['id'] info['volume_name'] = volume_name else: restore = cs.restores.restore(backup.id, volume_id, args.name) info.update(restore._info) info.pop('links', None) utils.print_dict(info) @utils.arg('--detail', action='store_true', help='Show detailed information about pools.') @utils.arg('--filters', action=AppendFilters, type=str, nargs='*', start_version='3.33', metavar='', default=None, help="Filter key and value pairs. Please use 'cinder list-filters' " "to check enabled filters from server, Default=None.") def do_get_pools(cs, args): """Show pool information for backends. Admin only.""" # pylint: disable=function-redefined search_opts = {} # Update search option with `filters` if AppendFilters.filters: search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) if cs.api_version >= api_versions.APIVersion("3.33"): pools = cs.volumes.get_pools(args.detail, search_opts) else: pools = cs.volumes.get_pools(args.detail) infos = dict() infos.update(pools._info) for info in infos['pools']: backend = dict() backend['name'] = info['name'] if args.detail: backend.update(info['capabilities']) utils.print_dict(backend) AppendFilters.filters = [] RESET_STATE_RESOURCES = {'volume': utils.find_volume, 'backup': shell_utils.find_backup, 'snapshot': shell_utils.find_volume_snapshot, 'group': shell_utils.find_group, 'group-snapshot': shell_utils.find_group_snapshot} @utils.arg('--group_id', metavar='', default=None, help="Filters results by a group_id. Default=None." "%s" % FILTER_DEPRECATED, start_version='3.10') @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=0, help='Shows details for all tenants. Admin only.') @utils.arg('--all_tenants', nargs='?', type=int, const=1, help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help="Filters results by a name. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--status', metavar='', default=None, help="Filters results by a status. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--bootable', metavar='', const=True, nargs='?', choices=['True', 'true', 'False', 'false'], help="Filters results by bootable status. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--migration_status', metavar='', default=None, help="Filters results by a migration status. Default=None. " "Admin only. " "%s" % FILTER_DEPRECATED) @utils.arg('--metadata', nargs='*', metavar='', default=None, help="Filters results by a metadata key and value pair. " "Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--image_metadata', nargs='*', metavar='', default=None, start_version='3.4', help="Filters results by a image metadata key and value pair. " "Require volume api version >=3.4. Default=None." "%s" % FILTER_DEPRECATED) @utils.arg('--marker', metavar='', default=None, help='Begin returning volumes that appear later in the volume ' 'list than that represented by this volume id. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, help='Maximum number of volumes to return. Default=None.') @utils.arg('--fields', default=None, metavar='', help='Comma-separated list of fields to display. ' 'Use the show command to see which fields are available. ' 'Unavailable/non-existent fields will be ignored. ' 'Default=None.') @utils.arg('--sort', metavar='[:]', default=None, help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) @utils.arg('--tenant', type=str, dest='tenant', nargs='?', metavar='', help='Display information from single tenant (Admin only).') @utils.arg('--filters', action=AppendFilters, type=str, nargs='*', start_version='3.33', metavar='', default=None, help="Filter key and value pairs. Please use 'cinder list-filters' " "to check enabled filters from server. Use 'key~=value' " "for inexact filtering if the key supports. Default=None.") @utils.arg('--with-count', type=bool, default=False, const=True, nargs='?', start_version='3.45', metavar='', help="Show total number of volume entities. This is useful when " "pagination is applied in the request.") def do_list(cs, args): """Lists all volumes.""" # pylint: disable=function-redefined # NOTE(thingee): Backwards-compatibility with v1 args if args.display_name is not None: args.name = args.display_name show_count = True if hasattr( args, 'with_count') and args.with_count else False all_tenants = 1 if args.tenant else \ int(os.environ.get("ALL_TENANTS", args.all_tenants)) search_opts = { 'all_tenants': all_tenants, 'project_id': args.tenant, 'name': args.name, 'status': args.status, 'bootable': args.bootable, 'migration_status': args.migration_status, 'metadata': shell_utils.extract_metadata(args) if args.metadata else None, 'glance_metadata': shell_utils.extract_metadata(args, type='image_metadata') if hasattr(args, 'image_metadata') and args.image_metadata else None, 'group_id': getattr(args, 'group_id', None), } # Update search option with `filters` if AppendFilters.filters: search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) # If unavailable/non-existent fields are specified, these fields will # be removed from key_list at the print_list() during key validation. field_titles = [] if args.fields: for field_title in args.fields.split(','): field_titles.append(field_title) total_count = 0 if show_count: search_opts['with_count'] = args.with_count volumes, total_count = cs.volumes.list( search_opts=search_opts, marker=args.marker, limit=args.limit, sort=args.sort) else: volumes = cs.volumes.list(search_opts=search_opts, marker=args.marker, limit=args.limit, sort=args.sort) shell_utils.translate_volume_keys(volumes) # Create a list of servers to which the volume is attached for vol in volumes: servers = [s.get('server_id') for s in vol.attachments] setattr(vol, 'attached_to', ','.join(map(str, servers))) with cs.volumes.completion_cache('uuid', cinderclient.v3.volumes.Volume, mode="w"): for vol in volumes: cs.volumes.write_to_completion_cache('uuid', vol.id) with cs.volumes.completion_cache('name', cinderclient.v3.volumes.Volume, mode="w"): for vol in volumes: if vol.name is None: continue cs.volumes.write_to_completion_cache('name', vol.name) if field_titles: # Remove duplicate fields key_list = ['ID'] unique_titles = [k for k in collections.OrderedDict.fromkeys( [x.title().strip() for x in field_titles]) if k != 'Id'] key_list.extend(unique_titles) else: key_list = ['ID', 'Status', 'Name', 'Size', 'Consumes Quota', 'Volume Type', 'Bootable', 'Attached to'] # If all_tenants is specified, print # Tenant ID as well. if search_opts['all_tenants']: key_list.insert(1, 'Tenant ID') if args.sort: sortby_index = None else: sortby_index = 0 utils.print_list(volumes, key_list, exclude_unavailable=True, sortby_index=sortby_index) if show_count: print("Volume in total: %s" % total_count) AppendFilters.filters = [] @utils.arg('entity', metavar='', nargs='+', help='Name or ID of entity to update.') @utils.arg('--type', metavar='', default='volume', choices=RESET_STATE_RESOURCES.keys(), help="Type of entity to update. Available resources " "are: 'volume', 'snapshot', 'backup', " "'group' (since 3.20) and " "'group-snapshot' (since 3.19), Default=volume.") @utils.arg('--state', metavar='', default=None, help=("The state to assign to the entity. " "NOTE: This command simply changes the state of the " "entity in the database with no regard to actual status, " "exercise caution when using. Default=None, that means the " "state is unchanged.")) @utils.arg('--attach-status', metavar='', default=None, help=('This is only used for a volume entity. The attach status ' 'to assign to the volume in the database, with no regard ' 'to the actual status. Valid values are "attached" and ' '"detached". Default=None, that means the status ' 'is unchanged.')) @utils.arg('--reset-migration-status', action='store_true', help=('This is only used for a volume entity. Clears the migration ' 'status of the volume in the DataBase that indicates the ' 'volume is source or destination of volume migration, ' 'with no regard to the actual status.')) def do_reset_state(cs, args): """Explicitly updates the entity state in the Cinder database. Being a database change only, this has no impact on the true state of the entity and may not match the actual state. This can render a entity unusable in the case of changing to the 'available' state. """ # pylint: disable=function-redefined failure_count = 0 single = (len(args.entity) == 1) migration_status = 'none' if args.reset_migration_status else None collector = RESET_STATE_RESOURCES[args.type] argument = (args.state,) if args.type == 'volume': argument += (args.attach_status, migration_status) for entity in args.entity: try: collector(cs, entity).reset_state(*argument) except Exception as e: print(e) failure_count += 1 msg = "Reset state for entity %s failed: %s" % (entity, e) if not single: print(msg) if failure_count == len(args.entity): msg = "Unable to reset the state for the specified entity(s)." raise exceptions.CommandError(msg) @utils.arg('size', metavar='', nargs='?', type=int, action=CheckSizeArgForCreate, help='Size of volume, in GiBs. (Required unless ' 'snapshot-id/source-volid/backup-id is specified).') @utils.arg('--consisgroup-id', metavar='', default=None, help='ID of a consistency group where the new volume belongs to. ' 'Default=None.') @utils.arg('--group-id', metavar='', default=None, help='ID of a group where the new volume belongs to. ' 'Default=None.', start_version='3.13') @utils.arg('--snapshot-id', metavar='', default=None, help='Creates volume from snapshot ID. Default=None.') @utils.arg('--snapshot_id', help=argparse.SUPPRESS) @utils.arg('--source-volid', metavar='', default=None, help='Creates volume from volume ID. Default=None.') @utils.arg('--source_volid', help=argparse.SUPPRESS) @utils.arg('--image-id', metavar='', default=None, help='Creates volume from image ID. Default=None.') @utils.arg('--image_id', help=argparse.SUPPRESS) @utils.arg('--image', metavar='', default=None, help='Creates a volume from image (ID or name). Default=None.') @utils.arg('--backup-id', metavar='', default=None, start_version='3.47', help='Creates a volume from backup ID. Default=None.') @utils.arg('--image_ref', help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help='Volume name. Default=None.') @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--display_name', help=argparse.SUPPRESS) @utils.arg('--description', metavar='', default=None, help='Volume description. Default=None.') @utils.arg('--display-description', help=argparse.SUPPRESS) @utils.arg('--display_description', help=argparse.SUPPRESS) @utils.arg('--volume-type', metavar='', default=None, help='Volume type. Default=None, that is, use the default ' 'volume type configured for the Block Storage API. You ' "can see what type this is by using the 'cinder type-default'" ' command.') @utils.arg('--volume_type', help=argparse.SUPPRESS) @utils.arg('--availability-zone', metavar='', default=None, help='Availability zone for volume. Default=None.') @utils.arg('--availability_zone', help=argparse.SUPPRESS) @utils.arg('--metadata', nargs='*', metavar='', default=None, help='Metadata key and value pairs. Default=None.') @utils.arg('--hint', metavar='', dest='scheduler_hints', action='append', default=[], help='Scheduler hint, similar to nova. Repeat option to set ' 'multiple hints. Values with the same key will be stored ' 'as a list.') @utils.arg('--poll', action="store_true", help=('Wait for volume creation until it completes.')) def do_create(cs, args): """Creates a volume.""" # NOTE(thingee): Backwards-compatibility with v1 args if args.display_name is not None: args.name = args.display_name if args.display_description is not None: args.description = args.display_description volume_metadata = None if args.metadata is not None: volume_metadata = shell_utils.extract_metadata(args) # NOTE(N.S.): take this piece from novaclient hints = {} if args.scheduler_hints: for hint in args.scheduler_hints: key, _sep, value = hint.partition('=') # NOTE(vish): multiple copies of same hint will # result in a list of values if key in hints: if isinstance(hints[key], str): hints[key] = [hints[key]] hints[key] += [value] else: hints[key] = value # NOTE(N.S.): end of taken piece # Keep backward compatibility with image_id, favoring explicit ID image_ref = args.image_id or args.image or args.image_ref try: group_id = args.group_id except AttributeError: group_id = None backup_id = args.backup_id if hasattr(args, 'backup_id') else None volume = cs.volumes.create(args.size, args.consisgroup_id, group_id, args.snapshot_id, args.source_volid, args.name, args.description, args.volume_type, availability_zone=args.availability_zone, imageRef=image_ref, metadata=volume_metadata, scheduler_hints=hints, backup_id=backup_id) info = dict() volume = cs.volumes.get(volume.id) info.update(volume._info) if 'readonly' in info['metadata']: info['readonly'] = info['metadata']['readonly'] info.pop('links', None) if args.poll: timeout_period = os.environ.get("POLL_TIMEOUT_PERIOD", 3600) shell_utils._poll_for_status( cs.volumes.get, volume.id, info, 'creating', ['available'], timeout_period, cs.client.global_request_id, cs.messages) volume = cs.volumes.get(volume.id) info.update(volume._info) utils.print_dict(info) with cs.volumes.completion_cache('uuid', cinderclient.v3.volumes.Volume, mode="a"): cs.volumes.write_to_completion_cache('uuid', volume.id) if volume.name is not None: with cs.volumes.completion_cache('name', cinderclient.v3.volumes.Volume, mode="a"): cs.volumes.write_to_completion_cache('name', volume.name) @utils.arg('volume', metavar='', help='Name or ID of volume for which to update metadata.') @utils.arg('action', metavar='', choices=['set', 'unset'], help='The action. Valid values are "set" or "unset."') @utils.arg('metadata', metavar='', nargs='+', default=[], end_version='3.14', help='Metadata key and value pair to set or unset. ' 'For unset, specify only the key.') @utils.arg('metadata', metavar='', nargs='+', default=[], start_version='3.15', help='Metadata key and value pair to set or unset. ' 'For unset, specify only the key(s): ') def do_metadata(cs, args): """Sets or deletes volume metadata.""" volume = utils.find_volume(cs, args.volume) metadata = shell_utils.extract_metadata(args) if args.action == 'set': cs.volumes.set_metadata(volume, metadata) elif args.action == 'unset': # NOTE(zul): Make sure py2/py3 sorting is the same cs.volumes.delete_metadata(volume, sorted(metadata.keys(), reverse=True)) @api_versions.wraps('3.12') @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=utils.env('ALL_TENANTS', default=0), help='Shows details for all tenants. Admin only.') def do_summary(cs, args): """Get volumes summary.""" all_tenants = args.all_tenants info = cs.volumes.summary(all_tenants) formatters = ['total_size', 'total_count'] if cs.api_version >= api_versions.APIVersion("3.36"): formatters.append('metadata') utils.print_dict(info['volume-summary'], formatters=formatters) @api_versions.wraps('3.11') @utils.arg('--filters', action=AppendFilters, type=str, nargs='*', start_version='3.52', metavar='', default=None, help="Filter key and value pairs. Admin only.") def do_group_type_list(cs, args): """Lists available 'group types'. (Admin only will see private types)""" search_opts = {} # Update search option with `filters` if AppendFilters.filters: search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) gtypes = cs.group_types.list(search_opts=search_opts) shell_utils.print_group_type_list(gtypes) AppendFilters.filters = [] @api_versions.wraps('3.11') def do_group_type_default(cs, args): """List the default group type.""" gtype = cs.group_types.default() shell_utils.print_group_type_list([gtype]) @api_versions.wraps('3.11') @utils.arg('group_type', metavar='', help='Name or ID of the group type.') def do_group_type_show(cs, args): """Show group type details.""" gtype = shell_utils.find_gtype(cs, args.group_type) info = dict() info.update(gtype._info) info.pop('links', None) utils.print_dict(info, formatters=['group_specs']) @api_versions.wraps('3.11') @utils.arg('id', metavar='', help='ID of the group type.') @utils.arg('--name', metavar='', help='Name of the group type.') @utils.arg('--description', metavar='', help='Description of the group type.') @utils.arg('--is-public', metavar='', help='Make type accessible to the public or not.') def do_group_type_update(cs, args): """Updates group type name, description, and/or is_public.""" is_public = strutils.bool_from_string(args.is_public) gtype = cs.group_types.update(args.id, args.name, args.description, is_public) shell_utils.print_group_type_list([gtype]) @api_versions.wraps('3.11') def do_group_specs_list(cs, args): """Lists current group types and specs.""" gtypes = cs.group_types.list() utils.print_list(gtypes, ['ID', 'Name', 'group_specs']) @api_versions.wraps('3.11') @utils.arg('name', metavar='', help='Name of new group type.') @utils.arg('--description', metavar='', help='Description of new group type.') @utils.arg('--is-public', metavar='', default=True, help='Make type accessible to the public (default true).') def do_group_type_create(cs, args): """Creates a group type.""" is_public = strutils.bool_from_string(args.is_public) gtype = cs.group_types.create(args.name, args.description, is_public) shell_utils.print_group_type_list([gtype]) @api_versions.wraps('3.11') @utils.arg('group_type', metavar='', nargs='+', help='Name or ID of group type or types to delete.') def do_group_type_delete(cs, args): """Deletes group type or types.""" failure_count = 0 for group_type in args.group_type: try: gtype = shell_utils.find_group_type(cs, group_type) cs.group_types.delete(gtype) print("Request to delete group type %s has been accepted." % group_type) except Exception as e: failure_count += 1 print("Delete for group type %s failed: %s" % (group_type, e)) if failure_count == len(args.group_type): raise exceptions.CommandError("Unable to delete any of the " "specified types.") @api_versions.wraps('3.11') @utils.arg('gtype', metavar='', help='Name or ID of group type.') @utils.arg('action', metavar='', choices=['set', 'unset'], help='The action. Valid values are "set" or "unset."') @utils.arg('metadata', metavar='', nargs='+', default=[], help='The group specs key and value pair to set or unset. ' 'For unset, specify only the key.') def do_group_type_key(cs, args): """Sets or unsets group_spec for a group type.""" gtype = shell_utils.find_group_type(cs, args.gtype) keypair = shell_utils.extract_metadata(args) if args.action == 'set': gtype.set_keys(keypair) elif args.action == 'unset': gtype.unset_keys(list(keypair)) @utils.arg('tenant', metavar='', help='ID of tenant for which to set quotas.') @utils.arg('--volumes', metavar='', type=int, default=None, help='The new "volumes" quota value. Default=None.') @utils.arg('--snapshots', metavar='', type=int, default=None, help='The new "snapshots" quota value. Default=None.') @utils.arg('--gigabytes', metavar='', type=int, default=None, help='The new "gigabytes" quota value. Default=None.') @utils.arg('--backups', metavar='', type=int, default=None, help='The new "backups" quota value. Default=None.') @utils.arg('--backup-gigabytes', metavar='', type=int, default=None, help='The new "backup_gigabytes" quota value. Default=None.') @utils.arg('--groups', metavar='', type=int, default=None, help='The new "groups" quota value. Default=None.', start_version='3.13') @utils.arg('--volume-type', metavar='', default=None, help='Volume type. Default=None.') @utils.arg('--per-volume-gigabytes', metavar='', type=int, default=None, help='Set max volume size limit. Default=None.') @utils.arg('--skip-validation', metavar='', default=False, help='Skip validate the existing resource quota. Default=False.') def do_quota_update(cs, args): """Updates quotas for a tenant.""" shell_utils.quota_update(cs.quotas, args.tenant, args) @utils.arg('volume', metavar='', help='Name or ID of volume to snapshot.') @utils.arg('--force', metavar='', const=True, nargs='?', default=False, help='Enables or disables upload of ' 'a volume that is attached to an instance. ' 'Default=False. ' 'This option may not be supported by your cloud.') @utils.arg('--container-format', metavar='', default='bare', help='Container format type. ' 'Default is bare.') @utils.arg('--container_format', help=argparse.SUPPRESS) @utils.arg('--disk-format', metavar='', default='raw', help='Disk format type. ' 'Default is raw.') @utils.arg('--disk_format', help=argparse.SUPPRESS) @utils.arg('image_name', metavar='', help='The new image name.') @utils.arg('--image_name', help=argparse.SUPPRESS) @utils.arg('--visibility', metavar='', help='Set image visibility to public, private, community or ' 'shared. Default=private.', default='private', start_version='3.1') @utils.arg('--protected', metavar='', help='Prevents image from being deleted. Default=False.', default=False, start_version='3.1') def do_upload_to_image(cs, args): """Uploads volume to Image Service as an image.""" volume = utils.find_volume(cs, args.volume) if cs.api_version >= api_versions.APIVersion("3.1"): (resp, body) = volume.upload_to_image(args.force, args.image_name, args.container_format, args.disk_format, args.visibility, args.protected) shell_utils.print_volume_image((resp, body)) else: (resp, body) = volume.upload_to_image(args.force, args.image_name, args.container_format, args.disk_format) shell_utils.print_volume_image((resp, body)) @utils.arg('volume', metavar='', help='ID of volume to migrate.') # NOTE(geguileo): host is positional but optional in order to maintain backward # compatibility even with mutually exclusive arguments. If version is < 3.16 # then only host positional argument will be possible, and since the # exclusive_arg group has required=True it will be required even if it's # optional. @utils.exclusive_arg('destination', 'host', required=True, nargs='?', metavar='', help='Destination host. Takes the ' 'form: host@backend-name#pool') @utils.exclusive_arg('destination', '--cluster', required=True, help='Destination cluster. Takes the form: ' 'cluster@backend-name#pool', start_version='3.16') @utils.arg('--force-host-copy', metavar='', choices=['True', 'False'], required=False, const=True, nargs='?', default=False, help='Enables or disables generic host-based ' 'force-migration, which bypasses driver ' 'optimizations. Default=False.') @utils.arg('--lock-volume', metavar='', choices=['True', 'False'], required=False, const=True, nargs='?', default=False, help='Enables or disables the termination of volume migration ' 'caused by other commands. This option applies to the ' 'available volume. True means it locks the volume ' 'state and does not allow the migration to be aborted. The ' 'volume status will be in maintenance during the ' 'migration. False means it allows the volume migration ' 'to be aborted. The volume status is still in the original ' 'status. Default=False.') def do_migrate(cs, args): """Migrates volume to a new host.""" volume = utils.find_volume(cs, args.volume) try: volume.migrate_volume(args.host, args.force_host_copy, args.lock_volume, getattr(args, 'cluster', None)) print("Request to migrate volume %s has been accepted." % (volume.id)) except Exception as e: print("Migration for volume %s failed: %s." % (volume.id, str(e))) @api_versions.wraps('3.9') @utils.arg('backup', metavar='', help='Name or ID of backup to rename.') @utils.arg('--name', nargs='?', metavar='', help='New name for backup.') @utils.arg('--description', metavar='', help='Backup description. Default=None.') @utils.arg('--metadata', nargs='*', metavar='', default=None, help='Metadata key and value pairs. Default=None.', start_version='3.43') def do_backup_update(cs, args): """Updates a backup.""" kwargs = {} if args.name is not None: kwargs['name'] = args.name if args.description is not None: kwargs['description'] = args.description if cs.api_version >= api_versions.APIVersion("3.43"): if args.metadata is not None: kwargs['metadata'] = shell_utils.extract_metadata(args) if not kwargs: msg = 'Must supply at least one: name, description or metadata.' raise exceptions.ClientException(code=1, message=msg) shell_utils.find_backup(cs, args.backup).update(**kwargs) print("Request to update backup '%s' has been accepted." % args.backup) @api_versions.wraps('3.7') @utils.arg('--name', metavar='', default=None, help='Filter by cluster name, without backend will list all ' 'clustered services from the same cluster. Default=None.') @utils.arg('--binary', metavar='', default=None, help='Cluster binary. Default=None.') @utils.arg('--is-up', metavar='', default=None, choices=('True', 'true', 'False', 'false'), help='Filter by up/down status. Default=None.') @utils.arg('--disabled', metavar='', default=None, choices=('True', 'true', 'False', 'false'), help='Filter by disabled status. Default=None.') @utils.arg('--num-hosts', metavar='', default=None, help='Filter by number of hosts in the cluster.') @utils.arg('--num-down-hosts', metavar='', default=None, help='Filter by number of hosts that are down.') @utils.arg('--detailed', dest='detailed', default=False, help='Get detailed clustered service information (Default=False).', action='store_true') def do_cluster_list(cs, args): """Lists clustered services with optional filtering.""" clusters = cs.clusters.list(name=args.name, binary=args.binary, is_up=args.is_up, disabled=args.disabled, num_hosts=args.num_hosts, num_down_hosts=args.num_down_hosts, detailed=args.detailed) columns = ['Name', 'Binary', 'State', 'Status'] if args.detailed: columns.extend(('Num Hosts', 'Num Down Hosts', 'Last Heartbeat', 'Disabled Reason', 'Created At', 'Updated at')) utils.print_list(clusters, columns) @api_versions.wraps('3.7') @utils.arg('binary', metavar='', nargs='?', default='cinder-volume', help='Binary to filter by. Default: cinder-volume.') @utils.arg('name', metavar='', help='Name of the clustered service to show.') def do_cluster_show(cs, args): """Show detailed information on a clustered service.""" cluster = cs.clusters.show(args.name, args.binary) utils.print_dict(cluster.to_dict()) @api_versions.wraps('3.7') @utils.arg('binary', metavar='', nargs='?', default='cinder-volume', help='Binary to filter by. Default: cinder-volume.') @utils.arg('name', metavar='', help='Name of the clustered services to update.') def do_cluster_enable(cs, args): """Enables clustered services.""" cluster = cs.clusters.update(args.name, args.binary, disabled=False) utils.print_dict(cluster.to_dict()) @api_versions.wraps('3.7') @utils.arg('binary', metavar='', nargs='?', default='cinder-volume', help='Binary to filter by. Default: cinder-volume.') @utils.arg('name', metavar='', help='Name of the clustered services to update.') @utils.arg('--reason', metavar='', default=None, help='Reason for disabling clustered service.') def do_cluster_disable(cs, args): """Disables clustered services.""" cluster = cs.clusters.update(args.name, args.binary, disabled=True, disabled_reason=args.reason) utils.print_dict(cluster.to_dict()) @api_versions.wraps('3.24') @utils.arg('--cluster', metavar='', default=None, help='Cluster name. Default=None.') @utils.arg('--host', metavar='', default=None, help='Service host name. Default=None.') @utils.arg('--binary', metavar='', default=None, help='Service binary. Default=None.') @utils.arg('--is-up', metavar='', dest='is_up', default=None, choices=('True', 'true', 'False', 'false'), help='Filter by up/down status, if set to true services need to be' ' up, if set to false services need to be down. Default is ' 'None, which means up/down status is ignored.') @utils.arg('--disabled', metavar='', default=None, choices=('True', 'true', 'False', 'false'), help='Filter by disabled status. Default=None.') @utils.arg('--resource-id', metavar='', default=None, help='UUID of a resource to cleanup. Default=None.') @utils.arg('--resource-type', metavar='', default=None, choices=('Volume', 'Snapshot'), help='Type of resource to cleanup.') @utils.arg('--service-id', metavar='', type=int, default=None, help='The service id field from the DB, not the uuid of the' ' service. Default=None.') def do_work_cleanup(cs, args): """Request cleanup of services with optional filtering.""" filters = dict(cluster_name=args.cluster, host=args.host, binary=args.binary, is_up=args.is_up, disabled=args.disabled, resource_id=args.resource_id, resource_type=args.resource_type, service_id=args.service_id) filters = {k: v for k, v in filters.items() if v is not None} cleaning, unavailable = cs.workers.clean(**filters) columns = ('ID', 'Cluster Name', 'Host', 'Binary') if cleaning: print('Following services will be cleaned:') utils.print_list(cleaning, columns) if unavailable: print('There are no alternative nodes to do cleanup for the following ' 'services:') utils.print_list(unavailable, columns) if not (cleaning or unavailable): print('No cleanable services matched cleanup criteria.') @utils.arg('host', metavar='', help='Cinder host on which the existing volume resides; ' 'takes the form: host@backend-name#pool') @utils.arg('--cluster', help='Cinder cluster on which the existing volume resides; ' 'takes the form: cluster@backend-name#pool', start_version='3.16') @utils.arg('identifier', metavar='', help='Name or other Identifier for existing volume') @utils.arg('--id-type', metavar='', default='source-name', help='Type of backend device identifier provided, ' 'typically source-name or source-id (Default=source-name)') @utils.arg('--name', metavar='', help='Volume name (Default=None)') @utils.arg('--description', metavar='', help='Volume description (Default=None)') @utils.arg('--volume-type', metavar='', help='Volume type (Default=None)') @utils.arg('--availability-zone', metavar='', help='Availability zone for volume (Default=None)') @utils.arg('--metadata', type=str, nargs='*', metavar='', help='Metadata key=value pairs (Default=None)') @utils.arg('--bootable', action='store_true', help='Specifies that the newly created volume should be' ' marked as bootable') def do_manage(cs, args): """Manage an existing volume.""" volume_metadata = None if args.metadata is not None: volume_metadata = shell_utils.extract_metadata(args) # Build a dictionary of key/value pairs to pass to the API. ref_dict = {args.id_type: args.identifier} # The recommended way to specify an existing volume is by ID or name, and # have the Cinder driver look for 'source-name' or 'source-id' elements in # the ref structure. To make things easier for the user, we have special # --source-name and --source-id CLI options that add the appropriate # element to the ref structure. # # Note how argparse converts hyphens to underscores. We use hyphens in the # dictionary so that it is consistent with what the user specified on the # CLI. if hasattr(args, 'source_name') and args.source_name is not None: ref_dict['source-name'] = args.source_name if hasattr(args, 'source_id') and args.source_id is not None: ref_dict['source-id'] = args.source_id volume = cs.volumes.manage(host=args.host, ref=ref_dict, name=args.name, description=args.description, volume_type=args.volume_type, availability_zone=args.availability_zone, metadata=volume_metadata, bootable=args.bootable, cluster=getattr(args, 'cluster', None)) info = {} volume = cs.volumes.get(volume.id) info.update(volume._info) info.pop('links', None) utils.print_dict(info) @api_versions.wraps('3.8') # NOTE(geguileo): host is positional but optional in order to maintain backward # compatibility even with mutually exclusive arguments. If version is < 3.16 # then only host positional argument will be possible, and since the # exclusive_arg group has required=True it will be required even if it's # optional. @utils.exclusive_arg('source', 'host', required=True, nargs='?', metavar='', help='Cinder host on which to list manageable volumes; ' 'takes the form: host@backend-name#pool') @utils.exclusive_arg('source', '--cluster', required=True, metavar='CLUSTER', help='Cinder cluster on which to list manageable ' 'volumes; takes the form: cluster@backend-name#pool', start_version='3.17') @utils.arg('--detailed', metavar='', default=True, help='Returned detailed information (default true).') @utils.arg('--marker', metavar='', default=None, help='Begin returning volumes that appear later in the volume ' 'list than that represented by this reference. This reference ' 'should be json like. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, help='Maximum number of volumes to return. Default=None.') @utils.arg('--offset', metavar='', default=None, help='Number of volumes to skip after marker. Default=None.') @utils.arg('--sort', metavar='[:]', default=None, help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.' ) % ', '.join(base.SORT_MANAGEABLE_KEY_VALUES))) def do_manageable_list(cs, args): """Lists all manageable volumes.""" # pylint: disable=function-redefined detailed = strutils.bool_from_string(args.detailed) cluster = getattr(args, 'cluster', None) volumes = cs.volumes.list_manageable(host=args.host, detailed=detailed, marker=args.marker, limit=args.limit, offset=args.offset, sort=args.sort, cluster=cluster) columns = ['reference', 'size', 'safe_to_manage'] if detailed: columns.extend(['reason_not_safe', 'cinder_id', 'extra_info']) utils.print_list(volumes, columns, sortby_index=None) @api_versions.wraps('3.13') @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=utils.env('ALL_TENANTS', default=None), help='Shows details for all tenants. Admin only.') @utils.arg('--filters', action=AppendFilters, type=str, nargs='*', start_version='3.33', metavar='', default=None, help="Filter key and value pairs. Please use 'cinder list-filters' " "to check enabled filters from server. Use 'key~=value' " "for inexact filtering if the key supports. Default=None.") def do_group_list(cs, args): """Lists all groups.""" search_opts = {'all_tenants': args.all_tenants} # Update search option with `filters` if AppendFilters.filters: search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) groups = cs.groups.list(search_opts=search_opts) columns = ['ID', 'Status', 'Name'] utils.print_list(groups, columns) with cs.groups.completion_cache( 'uuid', cinderclient.v3.groups.Group, mode='w'): for group in groups: cs.groups.write_to_completion_cache('uuid', group.id) with cs.groups.completion_cache('name', cinderclient.v3.groups.Group, mode='w'): for group in groups: if group.name is None: continue cs.groups.write_to_completion_cache('name', group.name) AppendFilters.filters = [] @api_versions.wraps('3.13') @utils.arg('--list-volume', dest='list_volume', metavar='', nargs='?', type=bool, const=True, default=False, help='Shows volumes included in the group.', start_version='3.25') @utils.arg('group', metavar='', help='Name or ID of a group.') def do_group_show(cs, args): """Shows details of a group.""" info = dict() if getattr(args, 'list_volume', None): group = shell_utils.find_group(cs, args.group, list_volume=args.list_volume) else: group = shell_utils.find_group(cs, args.group) info.update(group._info) info.pop('links', None) utils.print_dict(info) @api_versions.wraps('3.13') @utils.arg('grouptype', metavar='', help='Group type.') @utils.arg('volumetypes', metavar='', help='Comma-separated list of volume types.') @utils.arg('--name', metavar='', help='Name of a group.') @utils.arg('--description', metavar='', default=None, help='Description of a group. Default=None.') @utils.arg('--availability-zone', metavar='', default=None, help='Availability zone for group. Default=None.') def do_group_create(cs, args): """Creates a group.""" group = cs.groups.create( args.grouptype, args.volumetypes, args.name, args.description, availability_zone=args.availability_zone) info = dict() group = cs.groups.get(group.id) info.update(group._info) info.pop('links', None) utils.print_dict(info) with cs.groups.completion_cache('uuid', cinderclient.v3.groups.Group, mode='a'): cs.groups.write_to_completion_cache('uuid', group.id) if group.name is not None: with cs.groups.completion_cache('name', cinderclient.v3.groups.Group, mode='a'): cs.groups.write_to_completion_cache('name', group.name) @api_versions.wraps('3.14') @utils.arg('--group-snapshot', metavar='', help='Name or ID of a group snapshot. Default=None.') @utils.arg('--source-group', metavar='', help='Name or ID of a source group. Default=None.') @utils.arg('--name', metavar='', help='Name of a group. Default=None.') @utils.arg('--description', metavar='', help='Description of a group. Default=None.') def do_group_create_from_src(cs, args): """Creates a group from a group snapshot or a source group.""" if not args.group_snapshot and not args.source_group: msg = ('Cannot create group because neither ' 'group snapshot nor source group is provided.') raise exceptions.ClientException(code=1, message=msg) if args.group_snapshot and args.source_group: msg = ('Cannot create group because both ' 'group snapshot and source group are provided.') raise exceptions.ClientException(code=1, message=msg) group_snapshot = None if args.group_snapshot: group_snapshot = shell_utils.find_group_snapshot(cs, args.group_snapshot) source_group = None if args.source_group: source_group = shell_utils.find_group(cs, args.source_group) info = cs.groups.create_from_src( group_snapshot.id if group_snapshot else None, source_group.id if source_group else None, args.name, args.description) info.pop('links', None) utils.print_dict(info) @api_versions.wraps('3.13') @utils.arg('group', metavar='', nargs='+', help='Name or ID of one or more groups ' 'to be deleted.') @utils.arg('--delete-volumes', action='store_true', default=False, help='Allows or disallows groups to be deleted ' 'if they are not empty. If the group is empty, ' 'it can be deleted without the delete-volumes flag. ' 'If the group is not empty, the delete-volumes ' 'flag is required for it to be deleted. If True, ' 'all volumes in the group will also be deleted.') def do_group_delete(cs, args): """Removes one or more groups.""" failure_count = 0 for group in args.group: try: shell_utils.find_group(cs, group).delete(args.delete_volumes) except Exception as e: failure_count += 1 print("Delete for group %s failed: %s" % (group, e)) if failure_count == len(args.group): raise exceptions.CommandError("Unable to delete any of the specified " "groups.") @api_versions.wraps('3.13') @utils.arg('group', metavar='', help='Name or ID of a group.') @utils.arg('--name', metavar='', help='New name for group. Default=None.') @utils.arg('--description', metavar='', help='New description for group. Default=None.') @utils.arg('--add-volumes', metavar='', help='UUID of one or more volumes ' 'to be added to the group, ' 'separated by commas. Default=None.') @utils.arg('--remove-volumes', metavar='', help='UUID of one or more volumes ' 'to be removed from the group, ' 'separated by commas. Default=None.') def do_group_update(cs, args): """Updates a group.""" kwargs = {} if args.name is not None: kwargs['name'] = args.name if args.description is not None: kwargs['description'] = args.description if args.add_volumes is not None: kwargs['add_volumes'] = args.add_volumes if args.remove_volumes is not None: kwargs['remove_volumes'] = args.remove_volumes if not kwargs: msg = ('At least one of the following args must be supplied: ' 'name, description, add-volumes, remove-volumes.') raise exceptions.ClientException(code=1, message=msg) shell_utils.find_group(cs, args.group).update(**kwargs) print("Request to update group '%s' has been accepted." % args.group) @api_versions.wraps('3.38') @utils.arg('group', metavar='', help='Name or ID of the group.') def do_group_enable_replication(cs, args): """Enables replication for group.""" shell_utils.find_group(cs, args.group).enable_replication() @api_versions.wraps('3.38') @utils.arg('group', metavar='', help='Name or ID of the group.') def do_group_disable_replication(cs, args): """Disables replication for group.""" shell_utils.find_group(cs, args.group).disable_replication() @api_versions.wraps('3.38') @utils.arg('group', metavar='', help='Name or ID of the group.') @utils.arg('--allow-attached-volume', action='store_true', default=False, help='Allows or disallows group with ' 'attached volumes to be failed over.') @utils.arg('--secondary-backend-id', metavar='', help='Secondary backend id. Default=None.') def do_group_failover_replication(cs, args): """Fails over replication for group.""" shell_utils.find_group(cs, args.group).failover_replication( allow_attached_volume=args.allow_attached_volume, secondary_backend_id=args.secondary_backend_id) @api_versions.wraps('3.38') @utils.arg('group', metavar='', help='Name or ID of the group.') def do_group_list_replication_targets(cs, args): """Lists replication targets for group. Example value for replication_targets: .. code-block: json { 'replication_targets': [{'backend_id': 'vendor-id-1', 'unique_key': 'val1', ......}, {'backend_id': 'vendor-id-2', 'unique_key': 'val2', ......}] } """ rc, replication_targets = shell_utils.find_group( cs, args.group).list_replication_targets() rep_targets = replication_targets.get('replication_targets') if rep_targets and len(rep_targets) > 0: utils.print_list(rep_targets, [key for key in rep_targets[0].keys()]) @api_versions.wraps('3.14') @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=utils.env('ALL_TENANTS', default=None), help='Shows details for all tenants. Admin only.') @utils.arg('--status', metavar='', default=None, help="Filters results by a status. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--group-id', metavar='', default=None, help="Filters results by a group ID. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--filters', action=AppendFilters, type=str, nargs='*', start_version='3.33', metavar='', default=None, help="Filter key and value pairs. Please use 'cinder list-filters' " "to check enabled filters from server. Use 'key~=value' " "for inexact filtering if the key supports. Default=None.") def do_group_snapshot_list(cs, args): """Lists all group snapshots.""" search_opts = { 'all_tenants': args.all_tenants, 'status': args.status, 'group_id': args.group_id, } # Update search option with `filters` if AppendFilters.filters: search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) group_snapshots = cs.group_snapshots.list(search_opts=search_opts) columns = ['ID', 'Status', 'Name'] utils.print_list(group_snapshots, columns) AppendFilters.filters = [] @api_versions.wraps('3.14') @utils.arg('group_snapshot', metavar='', help='Name or ID of group snapshot.') def do_group_snapshot_show(cs, args): """Shows group snapshot details.""" info = dict() group_snapshot = shell_utils.find_group_snapshot(cs, args.group_snapshot) info.update(group_snapshot._info) info.pop('links', None) utils.print_dict(info) @api_versions.wraps('3.14') @utils.arg('group', metavar='', help='Name or ID of a group.') @utils.arg('--name', metavar='', default=None, help='Group snapshot name. Default=None.') @utils.arg('--description', metavar='', default=None, help='Group snapshot description. Default=None.') def do_group_snapshot_create(cs, args): """Creates a group snapshot.""" group = shell_utils.find_group(cs, args.group) group_snapshot = cs.group_snapshots.create( group.id, args.name, args.description) info = dict() group_snapshot = cs.group_snapshots.get(group_snapshot.id) info.update(group_snapshot._info) info.pop('links', None) utils.print_dict(info) @api_versions.wraps('3.14') @utils.arg('group_snapshot', metavar='', nargs='+', help='Name or ID of one or more group snapshots to be deleted.') def do_group_snapshot_delete(cs, args): """Removes one or more group snapshots.""" failure_count = 0 for group_snapshot in args.group_snapshot: try: shell_utils.find_group_snapshot(cs, group_snapshot).delete() except Exception as e: failure_count += 1 print("Delete for group snapshot %s failed: %s" % (group_snapshot, e)) if failure_count == len(args.group_snapshot): raise exceptions.CommandError("Unable to delete any of the specified " "group snapshots.") @api_versions.wraps('3.0') @utils.arg('--host', metavar='', default=None, help='Host name. Default=None.') @utils.arg('--binary', metavar='', default=None, help='Service binary. Default=None.') @utils.arg('--withreplication', metavar='', const=True, nargs='?', default=False, start_version='3.7', help='Enables or disables display of ' 'Replication info for c-vol services. Default=False.') def do_service_list(cs, args): """Lists all services. Filter by host and service binary.""" if hasattr(args, 'withreplication'): replication = strutils.bool_from_string(args.withreplication, strict=True) else: replication = False result = cs.services.list(host=args.host, binary=args.binary) columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] if cs.api_version.matches('3.7'): columns.append('Cluster') if replication: columns.extend(["Replication Status", "Active Backend ID", "Frozen"]) # NOTE(jay-lau-513): we check if the response has disabled_reason # so as not to add the column when the extended ext is not enabled. if result and hasattr(result[0], 'disabled_reason'): columns.append("Disabled Reason") if cs.api_version.matches('3.49'): columns.extend(["Backend State"]) utils.print_list(result, columns) @api_versions.wraps('3.8') # NOTE(geguileo): host is positional but optional in order to maintain backward # compatibility even with mutually exclusive arguments. If version is < 3.16 # then only host positional argument will be possible, and since the # exclusive_arg group has required=True it will be required even if it's # optional. @utils.exclusive_arg('source', 'host', required=True, nargs='?', metavar='', help='Cinder host on which to list manageable snapshots; ' 'takes the form: host@backend-name#pool') @utils.exclusive_arg('source', '--cluster', required=True, help='Cinder cluster on which to list manageable ' 'snapshots; takes the form: cluster@backend-name#pool', start_version='3.17') @utils.arg('--detailed', metavar='', default=True, help='Returned detailed information (default true).') @utils.arg('--marker', metavar='', default=None, help='Begin returning volumes that appear later in the volume ' 'list than that represented by this reference. This reference ' 'should be json like. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, help='Maximum number of volumes to return. Default=None.') @utils.arg('--offset', metavar='', default=None, help='Number of volumes to skip after marker. Default=None.') @utils.arg('--sort', metavar='[:]', default=None, help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.' ) % ', '.join(base.SORT_MANAGEABLE_KEY_VALUES))) def do_snapshot_manageable_list(cs, args): """Lists all manageable snapshots.""" # pylint: disable=function-redefined detailed = strutils.bool_from_string(args.detailed) cluster = getattr(args, 'cluster', None) snapshots = cs.volume_snapshots.list_manageable(host=args.host, detailed=detailed, marker=args.marker, limit=args.limit, offset=args.offset, sort=args.sort, cluster=cluster) columns = ['reference', 'size', 'safe_to_manage', 'source_reference'] if detailed: columns.extend(['reason_not_safe', 'cinder_id', 'extra_info']) utils.print_list(snapshots, columns, sortby_index=None) @api_versions.wraps("3.0") def do_api_version(cs, args): """Display the server API version information.""" columns = ['ID', 'Status', 'Version', 'Min_version'] response = cs.services.server_api_version() utils.print_list(response, columns) @api_versions.wraps("3.40") @utils.arg( 'snapshot', metavar='', help='Name or ID of the snapshot to restore. The snapshot must be the ' 'most recent one known to cinder.') def do_revert_to_snapshot(cs, args): """Revert a volume to the specified snapshot.""" snapshot = shell_utils.find_volume_snapshot(cs, args.snapshot) volume = utils.find_volume(cs, snapshot.volume_id) volume.revert_to_snapshot(snapshot) @api_versions.wraps("3.3") @utils.arg('--marker', metavar='', default=None, start_version='3.5', help='Begin returning message that appear later in the message ' 'list than that represented by this id. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, start_version='3.5', help='Maximum number of messages to return. Default=None.') @utils.arg('--sort', metavar='[:]', default=None, start_version='3.5', help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) @utils.arg('--resource_uuid', metavar='', default=None, help="Filters results by a resource uuid. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--resource_type', metavar='', default=None, help="Filters results by a resource type. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--event_id', metavar='', default=None, help="Filters results by event id. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--request_id', metavar='', default=None, help="Filters results by request id. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--level', metavar='', default=None, help="Filters results by the message level. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--filters', action=AppendFilters, type=str, nargs='*', start_version='3.33', metavar='', default=None, help="Filter key and value pairs. Please use 'cinder list-filters' " "to check enabled filters from server. Use 'key~=value' " "for inexact filtering if the key supports. Default=None.") def do_message_list(cs, args): """Lists all messages.""" search_opts = { 'resource_uuid': args.resource_uuid, 'event_id': args.event_id, 'request_id': args.request_id, } # Update search option with `filters` if AppendFilters.filters: search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) if args.resource_type: search_opts['resource_type'] = args.resource_type.upper() if args.level: search_opts['message_level'] = args.level.upper() marker = args.marker if hasattr(args, 'marker') else None limit = args.limit if hasattr(args, 'limit') else None sort = args.sort if hasattr(args, 'sort') else None messages = cs.messages.list(search_opts=search_opts, marker=marker, limit=limit, sort=sort) columns = ['ID', 'Resource Type', 'Resource UUID', 'Event ID', 'User Message'] if sort: sortby_index = None else: sortby_index = 0 utils.print_list(messages, columns, sortby_index=sortby_index) AppendFilters.filters = [] @api_versions.wraps("3.3") @utils.arg('message', metavar='', help='ID of message.') def do_message_show(cs, args): """Shows message details.""" info = dict() message = shell_utils.find_message(cs, args.message) info.update(message._info) info.pop('links', None) utils.print_dict(info) @api_versions.wraps("3.3") @utils.arg('message', metavar='', nargs='+', help='ID of one or more message to be deleted.') def do_message_delete(cs, args): """Removes one or more messages.""" failure_count = 0 for message in args.message: try: shell_utils.find_message(cs, message).delete() except Exception as e: failure_count += 1 print("Delete for message %s failed: %s" % (message, e)) if failure_count == len(args.message): raise exceptions.CommandError("Unable to delete any of the specified " "messages.") @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=0, help='Shows details for all tenants. Admin only.') @utils.arg('--all_tenants', nargs='?', type=int, const=1, help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help="Filters results by a name. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--display_name', help=argparse.SUPPRESS) @utils.arg('--status', metavar='', default=None, help="Filters results by a status. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--volume-id', metavar='', default=None, help="Filters results by a volume ID. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--volume_id', help=argparse.SUPPRESS) @utils.arg('--marker', metavar='', default=None, help='Begin returning snapshots that appear later in the snapshot ' 'list than that represented by this id. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, help='Maximum number of snapshots to return. Default=None.') @utils.arg('--sort', metavar='[:]', default=None, help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) @utils.arg('--tenant', type=str, dest='tenant', nargs='?', metavar='', help='Display information from single tenant (Admin only).') @utils.arg('--metadata', nargs='*', metavar='', default=None, start_version='3.22', help="Filters results by a metadata key and value pair. Require " "volume api version >=3.22. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--filters', action=AppendFilters, type=str, nargs='*', start_version='3.33', metavar='', default=None, help="Filter key and value pairs. Please use 'cinder list-filters' " "to check enabled filters from server. Use 'key~=value' " "for inexact filtering if the key supports. Default=None.") @utils.arg('--with-count', type=bool, default=False, const=True, nargs='?', start_version='3.45', metavar='', help="Show total number of snapshot entities. This is useful when " "pagination is applied in the request.") def do_snapshot_list(cs, args): """Lists all snapshots.""" # pylint: disable=function-redefined show_count = True if hasattr( args, 'with_count') and args.with_count else False all_tenants = (1 if args.tenant else int(os.environ.get("ALL_TENANTS", args.all_tenants))) if args.display_name is not None: args.name = args.display_name metadata = None try: if args.metadata: metadata = shell_utils.extract_metadata(args) except AttributeError: pass search_opts = { 'all_tenants': all_tenants, 'name': args.name, 'status': args.status, 'volume_id': args.volume_id, 'project_id': args.tenant, 'metadata': metadata } # Update search option with `filters` if AppendFilters.filters: search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) total_count = 0 if show_count: search_opts['with_count'] = args.with_count snapshots, total_count = cs.volume_snapshots.list( search_opts=search_opts, marker=args.marker, limit=args.limit, sort=args.sort) else: snapshots = cs.volume_snapshots.list(search_opts=search_opts, marker=args.marker, limit=args.limit, sort=args.sort) shell_utils.translate_volume_snapshot_keys(snapshots) sortby_index = None if args.sort else 0 # It's the server's responsibility to return the appropriate fields for the # requested microversion, we present all known fields and skip those that # are missing. utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Consumes Quota', 'User ID'], exclude_unavailable=True, sortby_index=sortby_index) if show_count: print("Snapshot in total: %s" % total_count) with cs.volume_snapshots.completion_cache( 'uuid', cinderclient.v3.volume_snapshots.Snapshot, mode='w'): for snapshot in snapshots: cs.volume_snapshots.write_to_completion_cache('uuid', snapshot.id) AppendFilters.filters = [] @api_versions.wraps("3.0", "3.65") @utils.arg('volume', metavar='', help='Name or ID of volume to snapshot.') @utils.arg('--force', metavar='', const=True, nargs='?', default=False, end_version='3.65', help='Allows or disallows snapshot of ' 'a volume when the volume is attached to an instance. ' 'If set to True, ignores the current status of the ' 'volume when attempting to snapshot it rather ' 'than forcing it to be available. From microversion 3.66, ' 'all snapshots are "forced" and this option is invalid. ' 'Default=False.') @utils.arg('--force', metavar='', nargs='?', default=None, start_version='3.66', help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help='Snapshot name. Default=None.') @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--display_name', help=argparse.SUPPRESS) @utils.arg('--description', metavar='', default=None, help='Snapshot description. Default=None.') @utils.arg('--display-description', help=argparse.SUPPRESS) @utils.arg('--display_description', help=argparse.SUPPRESS) @utils.arg('--metadata', nargs='*', metavar='', default=None, help='Snapshot metadata key and value pairs. Default=None.') def do_snapshot_create(cs, args): """Creates a snapshot.""" if args.display_name is not None: args.name = args.display_name if args.display_description is not None: args.description = args.display_description snapshot_metadata = None if args.metadata is not None: snapshot_metadata = shell_utils.extract_metadata(args) volume = utils.find_volume(cs, args.volume) snapshot = cs.volume_snapshots.create(volume.id, args.force, args.name, args.description, metadata=snapshot_metadata) shell_utils.print_volume_snapshot(snapshot) @api_versions.wraps("3.66") @utils.arg('volume', metavar='', help='Name or ID of volume to snapshot.') @utils.arg('--force', nargs='?', help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help='Snapshot name. Default=None.') @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--display_name', help=argparse.SUPPRESS) @utils.arg('--description', metavar='', default=None, help='Snapshot description. Default=None.') @utils.arg('--display-description', help=argparse.SUPPRESS) @utils.arg('--display_description', help=argparse.SUPPRESS) @utils.arg('--metadata', nargs='*', metavar='', default=None, help='Snapshot metadata key and value pairs. Default=None.') def do_snapshot_create(cs, args): # noqa: F811 """Creates a snapshot.""" # TODO(rosmaita): we really need to look into removing this v1 # compatibility code and the v1 options entirely. Note that if you # include the --name and also --display_name, the latter will be used. # Not sure that's desirable, but it is consistent with all the other # functions in this file, so we'll do it here too. if args.display_name is not None: args.name = args.display_name if args.display_description is not None: args.description = args.display_description snapshot_metadata = None if args.metadata is not None: snapshot_metadata = shell_utils.extract_metadata(args) force = getattr(args, 'force', None) volume = utils.find_volume(cs, args.volume) # this is a little weird, but for consistency with the API we # will silently ignore the --force option when it's passed with # a value that evaluates to True; otherwise, we report that the # --force option is illegal for this call try: snapshot = cs.volume_snapshots.create(volume.id, force=force, name=args.name, description=args.description, metadata=snapshot_metadata) except ValueError as ve: # make sure it's the exception we expect em = cinderclient.v3.volume_snapshots.MV_3_66_FORCE_FLAG_ERROR if em == str(ve): raise exceptions.UnsupportedAttribute( 'force', start_version=None, end_version=api_versions.APIVersion('3.65')) else: raise shell_utils.print_volume_snapshot(snapshot) @api_versions.wraps('3.27') @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=utils.env('ALL_TENANTS', default=0), help='Shows details for all tenants. Admin only.') @utils.arg('--volume-id', metavar='', default=None, help="Filters results by a volume ID. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--status', metavar='', default=None, help="Filters results by a status. Default=None. " "%s" % FILTER_DEPRECATED) @utils.arg('--marker', metavar='', default=None, help='Begin returning attachments that appear later in ' 'attachment list than that represented by this id. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, help='Maximum number of attachments to return. Default=None.') @utils.arg('--sort', metavar='[:]', default=None, help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) @utils.arg('--tenant', type=str, dest='tenant', nargs='?', metavar='', help='Display information from single tenant (Admin only).') @utils.arg('--filters', action=AppendFilters, type=str, nargs='*', start_version='3.33', metavar='', default=None, help="Filter key and value pairs. Please use 'cinder list-filters' " "to check enabled filters from server. Use 'key~=value' " "for inexact filtering if the key supports. Default=None.") def do_attachment_list(cs, args): """Lists all attachments.""" search_opts = { 'all_tenants': 1 if args.tenant else args.all_tenants, 'project_id': args.tenant, 'status': args.status, 'volume_id': args.volume_id, } # Update search option with `filters` if AppendFilters.filters: search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) attachments = cs.attachments.list(search_opts=search_opts, marker=args.marker, limit=args.limit, sort=args.sort) for attachment in attachments: setattr(attachment, 'server_id', getattr(attachment, 'instance', None)) columns = ['ID', 'Volume ID', 'Status', 'Server ID'] if args.sort: sortby_index = None else: sortby_index = 0 utils.print_list(attachments, columns, sortby_index=sortby_index) AppendFilters.filters = [] @api_versions.wraps('3.27') @utils.arg('attachment', metavar='', help='ID of attachment.') def do_attachment_show(cs, args): """Show detailed information for attachment.""" attachment = cs.attachments.show(args.attachment) attachment_dict = attachment.to_dict() connection_dict = attachment_dict.pop('connection_info', {}) utils.print_dict(attachment_dict) # TODO(jdg): Need to add checks here like admin/policy for displaying the # connection_info, this is still experimental so we'll leave it enabled for # now if connection_dict: utils.print_dict(connection_dict) @api_versions.wraps('3.27') @utils.arg('volume', metavar='', help='Name or ID of volume or volumes to attach.') @utils.arg('server_id', metavar='', nargs='?', default=None, help='ID of server attaching to.') @utils.arg('--connect', metavar='', default=False, help='Make an active connection using provided connector info ' '(True or False).') @utils.arg('--initiator', metavar='', default=None, help='iqn of the initiator attaching to. Default=None.') @utils.arg('--ip', metavar='', default=None, help='ip of the system attaching to. Default=None.') @utils.arg('--host', metavar='', default=None, help='Name of the host attaching to. Default=None.') @utils.arg('--platform', metavar='', default='x86_64', help='Platform type. Default=x86_64.') @utils.arg('--ostype', metavar='', default='linux2', help='OS type. Default=linux2.') @utils.arg('--multipath', metavar='', default=False, help='Use multipath. Default=False.') @utils.arg('--mountpoint', metavar='', default=None, help='Mountpoint volume will be attached at. Default=None.') @utils.arg('--mode', metavar='', default='null', start_version='3.54', help='Mode of attachment, rw, ro and null, where null ' 'indicates we want to honor any existing ' 'admin-metadata settings. Default=null.') def do_attachment_create(cs, args): """Create an attachment for a cinder volume.""" connector = {} if strutils.bool_from_string(args.connect, strict=True): # FIXME(jdg): Add in all the options when they're finalized connector = {'initiator': args.initiator, 'ip': args.ip, 'platform': args.platform, 'host': args.host, 'os_type': args.ostype, 'multipath': args.multipath, 'mountpoint': args.mountpoint} volume = utils.find_volume(cs, args.volume) mode = getattr(args, 'mode', 'null') attachment = cs.attachments.create(volume.id, connector, args.server_id, mode) connector_dict = attachment.pop('connection_info', None) utils.print_dict(attachment) if connector_dict: utils.print_dict(connector_dict) @api_versions.wraps('3.27') @utils.arg('attachment', metavar='', help='ID of attachment.') @utils.arg('--initiator', metavar='', default=None, help='iqn of the initiator attaching to. Default=None.') @utils.arg('--ip', metavar='', default=None, help='ip of the system attaching to. Default=None.') @utils.arg('--host', metavar='', default=None, help='Name of the host attaching to. Default=None.') @utils.arg('--platform', metavar='', default='x86_64', help='Platform type. Default=x86_64.') @utils.arg('--ostype', metavar='', default='linux2', help='OS type. Default=linux2.') @utils.arg('--multipath', metavar='', default=False, help='Use multipath. Default=False.') @utils.arg('--mountpoint', metavar='', default=None, help='Mountpoint volume will be attached at. Default=None.') def do_attachment_update(cs, args): """Update an attachment for a cinder volume. This call is designed to be more of an attachment completion than anything else. It expects the value of a connector object to notify the driver that the volume is going to be connected and where it's being connected to. """ connector = {'initiator': args.initiator, 'ip': args.ip, 'platform': args.platform, 'host': args.host, 'os_type': args.ostype, 'multipath': args.multipath, 'mountpoint': args.mountpoint} attachment = cs.attachments.update(args.attachment, connector) attachment_dict = attachment.to_dict() connector_dict = attachment_dict.pop('connection_info', None) utils.print_dict(attachment_dict) if connector_dict: utils.print_dict(connector_dict) @api_versions.wraps('3.27') @utils.arg('attachment', metavar='', nargs='+', help='ID of attachment or attachments to delete.') def do_attachment_delete(cs, args): """Delete an attachment for a cinder volume.""" for attachment in args.attachment: cs.attachments.delete(attachment) @api_versions.wraps('3.44') @utils.arg('attachment', metavar='', nargs='+', help='ID of attachment or attachments to delete.') def do_attachment_complete(cs, args): """Complete an attachment for a cinder volume.""" for attachment in args.attachment: cs.attachments.complete(attachment) @api_versions.wraps('3.0') def do_version_list(cs, args): """List all API versions.""" result = cs.services.server_api_version() if 'min_version' in dir(result[0]): columns = ["Id", "Status", "Updated", "Min Version", "Version"] else: columns = ["Id", "Status", "Updated"] print("Client supported API versions:") print("Minimum version %(v)s" % {'v': api_versions.MIN_VERSION}) print("Maximum version %(v)s" % {'v': api_versions.MAX_VERSION}) print("\nServer supported API versions:") utils.print_list(result, columns) @api_versions.wraps('3.32') @utils.arg('level', metavar='', choices=('INFO', 'WARNING', 'ERROR', 'DEBUG', 'info', 'warning', 'error', 'debug'), help='Desired log level.') @utils.arg('--binary', choices=('', '*', 'cinder-api', 'cinder-volume', 'cinder-scheduler', 'cinder-backup'), default='', help='Binary to change.') @utils.arg('--server', default='', help='Host or cluster value for service.') @utils.arg('--prefix', default='', help='Prefix for the log. ie: "cinder.volume.drivers.".') def do_service_set_log(cs, args): """Sets the service log level.""" cs.services.set_log_levels(args.level, args.binary, args.server, args.prefix) @api_versions.wraps('3.32') @utils.arg('--binary', choices=('', '*', 'cinder-api', 'cinder-volume', 'cinder-scheduler', 'cinder-backup'), default='', help='Binary to query.') @utils.arg('--server', default='', help='Host or cluster value for service.') @utils.arg('--prefix', default='', help='Prefix for the log. ie: "sqlalchemy.".') def do_service_get_log(cs, args): """Gets the service log level.""" log_levels = cs.services.get_log_levels(args.binary, args.server, args.prefix) columns = ('Binary', 'Host', 'Prefix', 'Level') utils.print_list(log_levels, columns) @utils.arg('volume', metavar='', help='Name or ID of volume to backup.') @utils.arg('--container', metavar='', default=None, help='Backup container name. Default=None.') @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help='Backup name. Default=None.') @utils.arg('--display-description', help=argparse.SUPPRESS) @utils.arg('--description', metavar='', default=None, help='Backup description. Default=None.') @utils.arg('--incremental', action='store_true', help='Incremental backup. Default=False.', default=False) @utils.arg('--force', action='store_true', help='Allows or disallows backup of a volume ' 'when the volume is attached to an instance. ' 'If set to True, backs up the volume whether ' 'its status is "available" or "in-use". The backup ' 'of an "in-use" volume means your data is crash ' 'consistent. Default=False.', default=False) @utils.arg('--snapshot-id', metavar='', default=None, help='ID of snapshot to backup. Default=None.') @utils.arg('--metadata', nargs='*', metavar='', default=None, start_version='3.43', help='Metadata key and value pairs. Default=None.') @utils.arg('--availability-zone', default=None, start_version='3.51', help='AZ where the backup should be stored, by default it will be ' 'the same as the source.') def do_backup_create(cs, args): """Creates a volume backup.""" if args.display_name is not None: args.name = args.display_name if args.display_description is not None: args.description = args.display_description kwargs = {} if getattr(args, 'metadata', None): kwargs['metadata'] = shell_utils.extract_metadata(args) az = getattr(args, 'availability_zone', None) if az: kwargs['availability_zone'] = az volume = utils.find_volume(cs, args.volume) backup = cs.backups.create(volume.id, args.container, args.name, args.description, args.incremental, args.force, args.snapshot_id, **kwargs) info = {"volume_id": volume.id} info.update(backup._info) if 'links' in info: info.pop('links') utils.print_dict(info) with cs.backups.completion_cache( 'uuid', cinderclient.v3.volume_backups.VolumeBackup, mode="a"): cs.backups.write_to_completion_cache('uuid', backup.id) @utils.arg('volume', metavar='', help='Name or ID of volume to transfer.') @utils.arg('--name', metavar='', default=None, help='Transfer name. Default=None.') @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--no-snapshots', action='store_true', help='Allows or disallows transfer volumes without snapshots. ' 'Default=False.', start_version='3.55', default=False) def do_transfer_create(cs, args): """Creates a volume transfer.""" if args.display_name is not None: args.name = args.display_name kwargs = {} no_snapshots = getattr(args, 'no_snapshots', None) if no_snapshots is not None: kwargs['no_snapshots'] = no_snapshots volume = utils.find_volume(cs, args.volume) transfer = cs.transfers.create(volume.id, args.name, **kwargs) info = dict() info.update(transfer._info) info.pop('links', None) utils.print_dict(info) @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=0, help='Shows details for all tenants. Admin only.') @utils.arg('--all_tenants', nargs='?', type=int, const=1, help=argparse.SUPPRESS) @utils.arg('--sort', metavar='[:]', default=None, help='Sort keys and directions in the form of [:].', start_version='3.59') @utils.arg('--filters', action=AppendFilters, type=str, nargs='*', start_version='3.52', metavar='', default=None, help="Filter key and value pairs.") def do_transfer_list(cs, args): """Lists all transfers.""" all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants)) search_opts = { 'all_tenants': all_tenants, } if AppendFilters.filters: search_opts.update(shell_utils.extract_filters(AppendFilters.filters)) sort = getattr(args, 'sort', None) if sort: sort_args = sort.split(':') if len(sort_args) > 2: raise exceptions.CommandError( 'Invalid sort parameter provided. Argument must be in the ' 'form "key[:]".') transfers = cs.transfers.list(search_opts=search_opts, sort=sort) columns = ['ID', 'Volume ID', 'Name'] utils.print_list(transfers, columns) AppendFilters.filters = [] @api_versions.wraps('3.62') @utils.arg('volume_type', metavar='', help='Name or ID of the volume type.') @utils.arg('project', metavar='', help='ID of project for which to set default type.') def do_default_type_set(cs, args): """Sets a default volume type for a project.""" volume_type = args.volume_type project = args.project default_type = cs.default_types.create(volume_type, project) utils.print_dict(default_type._info) @api_versions.wraps('3.62') @utils.arg('--project-id', metavar='', default=None, help='ID of project for which to show the default type.') def do_default_type_list(cs, args): """Lists all default volume types.""" project_id = args.project_id default_types = cs.default_types.list(project_id) columns = ['Volume Type ID', 'Project ID'] if project_id: utils.print_dict(default_types._info) else: utils.print_list(default_types, columns) @api_versions.wraps('3.62') @utils.arg('project_id', metavar='', nargs='+', help='ID of project for which to unset default type.') def do_default_type_unset(cs, args): """Unset default volume types.""" for project_id in args.project_id: try: cs.default_types.delete(project_id) print("Default volume type for project %s has been unset " "successfully." % (project_id)) except Exception as e: print("Unset for default volume type for project %s failed: %s" % (project_id, e)) @api_versions.wraps('3.68') @utils.arg('volume', metavar='', help='Name or ID of volume to reimage') @utils.arg('image_id', metavar='', help='The image id of the image that will be used to reimage ' 'the volume.') @utils.arg('--reimage-reserved', metavar='', default=False, help='Enables or disables reimage for a volume that is in ' 'reserved state otherwise only volumes in "available" ' ' or "error" status may be re-imaged. Default=False.') def do_reimage(cs, args): """Rebuilds a volume, overwriting all content with the specified image""" volume = utils.find_volume(cs, args.volume) volume.reimage(args.image_id, args.reimage_reserved) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/shell_base.py0000664000175000017500000025526500000000000023214 0ustar00zuulzuul00000000000000# Copyright (c) 2013-2014 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse import collections import copy import os from oslo_utils import strutils from cinderclient import base from cinderclient import exceptions from cinderclient import shell_utils from cinderclient import utils from cinderclient.v3 import availability_zones def _translate_attachments(info): attachments = [] attached_servers = [] for attachment in info['attachments']: attachments.append(attachment['attachment_id']) attached_servers.append(attachment['server_id']) info.pop('attachments', None) info['attachment_ids'] = attachments info['attached_servers'] = attached_servers return info @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=0, help='Shows details for all tenants. Admin only.') @utils.arg('--all_tenants', nargs='?', type=int, const=1, help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help='Filters results by a name. Default=None.') @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--status', metavar='', default=None, help='Filters results by a status. Default=None.') @utils.arg('--bootable', metavar='', const=True, nargs='?', choices=['True', 'true', 'False', 'false'], help='Filters results by bootable status. Default=None.') @utils.arg('--migration_status', metavar='', default=None, help='Filters results by a migration status. Default=None. ' 'Admin only.') @utils.arg('--metadata', nargs='*', metavar='', default=None, help='Filters results by a image metadata key and value pair. ' 'Default=None.') @utils.arg('--marker', metavar='', default=None, help='Begin returning volumes that appear later in the volume ' 'list than that represented by this volume id. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, help='Maximum number of volumes to return. Default=None.') @utils.arg('--fields', default=None, metavar='', help='Comma-separated list of fields to display. ' 'Use the show command to see which fields are available. ' 'Unavailable/non-existent fields will be ignored. ' 'Default=None.') @utils.arg('--sort', metavar='[:]', default=None, help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) @utils.arg('--tenant', type=str, dest='tenant', nargs='?', metavar='', help='Display information from single tenant (Admin only).') def do_list(cs, args): """Lists all volumes.""" # NOTE(thingee): Backwards-compatibility with v1 args if args.display_name is not None: args.name = args.display_name all_tenants = 1 if args.tenant else \ int(os.environ.get("ALL_TENANTS", args.all_tenants)) search_opts = { 'all_tenants': all_tenants, 'project_id': args.tenant, 'name': args.name, 'status': args.status, 'bootable': args.bootable, 'migration_status': args.migration_status, 'metadata': (shell_utils.extract_metadata(args) if args.metadata else None), } # If unavailable/non-existent fields are specified, these fields will # be removed from key_list at the print_list() during key validation. field_titles = [] if args.fields: for field_title in args.fields.split(','): field_titles.append(field_title) volumes = cs.volumes.list(search_opts=search_opts, marker=args.marker, limit=args.limit, sort=args.sort) shell_utils.translate_volume_keys(volumes) # Create a list of servers to which the volume is attached for vol in volumes: servers = [s.get('server_id') for s in vol.attachments] setattr(vol, 'attached_to', ','.join(map(str, servers))) if field_titles: # Remove duplicate fields key_list = ['ID'] unique_titles = [k for k in collections.OrderedDict.fromkeys( [x.title().strip() for x in field_titles]) if k != 'Id'] key_list.extend(unique_titles) else: key_list = ['ID', 'Status', 'Name', 'Size', 'Volume Type', 'Bootable', 'Attached to'] # If all_tenants is specified, print # Tenant ID as well. if search_opts['all_tenants']: key_list.insert(1, 'Tenant ID') if args.sort: sortby_index = None else: sortby_index = 0 utils.print_list(volumes, key_list, exclude_unavailable=True, sortby_index=sortby_index) @utils.arg('volume', metavar='', help='Name or ID of volume.') def do_show(cs, args): """Shows volume details.""" info = dict() volume = utils.find_volume(cs, args.volume) info.update(volume._info) if 'readonly' in info['metadata']: info['readonly'] = info['metadata']['readonly'] info.pop('links', None) info = _translate_attachments(info) utils.print_dict(info, formatters=['metadata', 'volume_image_metadata', 'attachment_ids', 'attached_servers']) class CheckSizeArgForCreate(argparse.Action): def __call__(self, parser, args, values, option_string=None): if ((args.snapshot_id or args.source_volid) is None and values is None): if not hasattr(args, 'backup_id') or args.backup_id is None: parser.error('Size is a required parameter if snapshot ' 'or source volume or backup is not specified.') setattr(args, self.dest, values) @utils.arg('size', metavar='', nargs='?', type=int, action=CheckSizeArgForCreate, help='Size of volume, in GiBs. (Required unless ' 'snapshot-id/source-volid is specified).') @utils.arg('--consisgroup-id', metavar='', default=None, help='ID of a consistency group where the new volume belongs to. ' 'Default=None.') @utils.arg('--snapshot-id', metavar='', default=None, help='Creates volume from snapshot ID. Default=None.') @utils.arg('--snapshot_id', help=argparse.SUPPRESS) @utils.arg('--source-volid', metavar='', default=None, help='Creates volume from volume ID. Default=None.') @utils.arg('--source_volid', help=argparse.SUPPRESS) @utils.arg('--image-id', metavar='', default=None, help='Creates volume from image ID. Default=None.') @utils.arg('--image_id', help=argparse.SUPPRESS) @utils.arg('--image', metavar='', default=None, help='Creates a volume from image (ID or name). Default=None.') @utils.arg('--image_ref', help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help='Volume name. Default=None.') @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--display_name', help=argparse.SUPPRESS) @utils.arg('--description', metavar='', default=None, help='Volume description. Default=None.') @utils.arg('--display-description', help=argparse.SUPPRESS) @utils.arg('--display_description', help=argparse.SUPPRESS) @utils.arg('--volume-type', metavar='', default=None, help='Volume type. Default=None.') @utils.arg('--volume_type', help=argparse.SUPPRESS) @utils.arg('--availability-zone', metavar='', default=None, help='Availability zone for volume. Default=None.') @utils.arg('--availability_zone', help=argparse.SUPPRESS) @utils.arg('--metadata', nargs='*', metavar='', default=None, help='Metadata key and value pairs. Default=None.') @utils.arg('--hint', metavar='', dest='scheduler_hints', action='append', default=[], help='Scheduler hint, similar to nova. Repeat option to set ' 'multiple hints. Values with the same key will be stored ' 'as a list.') def do_create(cs, args): """Creates a volume.""" # NOTE(thingee): Backwards-compatibility with v1 args if args.display_name is not None: args.name = args.display_name if args.display_description is not None: args.description = args.display_description volume_metadata = None if args.metadata is not None: volume_metadata = shell_utils.extract_metadata(args) # NOTE(N.S.): take this piece from novaclient hints = {} if args.scheduler_hints: for hint in args.scheduler_hints: key, _sep, value = hint.partition('=') # NOTE(vish): multiple copies of same hint will # result in a list of values if key in hints: if isinstance(hints[key], str): hints[key] = [hints[key]] hints[key] += [value] else: hints[key] = value # NOTE(N.S.): end of taken piece # Keep backward compatibility with image_id, favoring explicit ID image_ref = args.image_id or args.image or args.image_ref volume = cs.volumes.create(args.size, args.consisgroup_id, args.snapshot_id, args.source_volid, args.name, args.description, args.volume_type, availability_zone=args.availability_zone, imageRef=image_ref, metadata=volume_metadata, scheduler_hints=hints) info = dict() volume = cs.volumes.get(volume.id) info.update(volume._info) if 'readonly' in info['metadata']: info['readonly'] = info['metadata']['readonly'] info.pop('links', None) info = _translate_attachments(info) utils.print_dict(info) @utils.arg('--cascade', action='store_true', default=False, help='Remove any snapshots along with volume. Default=False.') @utils.arg('volume', metavar='', nargs='+', help='Name or ID of volume or volumes to delete.') def do_delete(cs, args): """Removes one or more volumes.""" failure_count = 0 for volume in args.volume: try: utils.find_volume(cs, volume).delete(cascade=args.cascade) print("Request to delete volume %s has been accepted." % (volume)) except Exception as e: failure_count += 1 print("Delete for volume %s failed: %s" % (volume, e)) if failure_count == len(args.volume): raise exceptions.CommandError("Unable to delete any of the specified " "volumes.") @utils.arg('volume', metavar='', nargs='+', help='Name or ID of volume or volumes to delete.') def do_force_delete(cs, args): """Attempts force-delete of volume, regardless of state.""" failure_count = 0 for volume in args.volume: try: utils.find_volume(cs, volume).force_delete() except Exception as e: failure_count += 1 print("Delete for volume %s failed: %s" % (volume, e)) if failure_count == len(args.volume): raise exceptions.CommandError("Unable to force delete any of the " "specified volumes.") @utils.arg('volume', metavar='', nargs='+', help='Name or ID of volume to modify.') @utils.arg('--state', metavar='', default=None, help=('The state to assign to the volume. Valid values are ' '"available", "error", "creating", "deleting", "in-use", ' '"attaching", "detaching", "error_deleting" and ' '"maintenance". ' 'NOTE: This command simply changes the state of the ' 'Volume in the DataBase with no regard to actual status, ' 'exercise caution when using. Default=None, that means the ' 'state is unchanged.')) @utils.arg('--attach-status', metavar='', default=None, help=('The attach status to assign to the volume in the DataBase, ' 'with no regard to the actual status. Valid values are ' '"attached" and "detached". Default=None, that means the ' 'status is unchanged.')) @utils.arg('--reset-migration-status', action='store_true', help=('Clears the migration status of the volume in the DataBase ' 'that indicates the volume is source or destination of ' 'volume migration, with no regard to the actual status.')) def do_reset_state(cs, args): """Explicitly updates the volume state in the Cinder database. Note that this does not affect whether the volume is actually attached to the Nova compute host or instance and can result in an unusable volume. Being a database change only, this has no impact on the true state of the volume and may not match the actual state. This can render a volume unusable in the case of change to the 'available' state. """ failure_flag = False migration_status = 'none' if args.reset_migration_status else None if not (args.state or args.attach_status or migration_status): # Nothing specified, default to resetting state args.state = 'available' for volume in args.volume: try: utils.find_volume(cs, volume).reset_state(args.state, args.attach_status, migration_status) except Exception as e: failure_flag = True msg = "Reset state for volume %s failed: %s" % (volume, e) print(msg) if failure_flag: msg = "Unable to reset the state for the specified volume(s)." raise exceptions.CommandError(msg) @utils.arg('volume', metavar='', help='Name or ID of volume to rename.') @utils.arg('name', nargs='?', metavar='', help='New name for volume.') @utils.arg('--description', metavar='', help='Volume description. Default=None.', default=None) @utils.arg('--display-description', help=argparse.SUPPRESS) @utils.arg('--display_description', help=argparse.SUPPRESS) def do_rename(cs, args): """Renames a volume.""" kwargs = {} if args.name is not None: kwargs['name'] = args.name if args.display_description is not None: kwargs['description'] = args.display_description elif args.description is not None: kwargs['description'] = args.description if not any(kwargs): msg = 'Must supply either name or description.' raise exceptions.ClientException(code=1, message=msg) utils.find_volume(cs, args.volume).update(**kwargs) @utils.arg('volume', metavar='', help='Name or ID of volume for which to update metadata.') @utils.arg('action', metavar='', choices=['set', 'unset'], help='The action. Valid values are "set" or "unset."') @utils.arg('metadata', metavar='', nargs='+', default=[], help='Metadata key and value pair to set or unset. ' 'For unset, specify only the key.') def do_metadata(cs, args): """Sets or deletes volume metadata.""" volume = utils.find_volume(cs, args.volume) metadata = shell_utils.extract_metadata(args) if args.action == 'set': cs.volumes.set_metadata(volume, metadata) elif args.action == 'unset': # NOTE(zul): Make sure py2/py3 sorting is the same cs.volumes.delete_metadata(volume, sorted(metadata.keys(), reverse=True)) @utils.arg('volume', metavar='', help='Name or ID of volume for which to update metadata.') @utils.arg('action', metavar='', choices=['set', 'unset'], help="The action. Valid values are 'set' or 'unset.'") @utils.arg('metadata', metavar='', nargs='+', default=[], help='Metadata key and value pair to set or unset. ' 'For unset, specify only the key.') def do_image_metadata(cs, args): """Sets or deletes volume image metadata.""" volume = utils.find_volume(cs, args.volume) metadata = shell_utils.extract_metadata(args) if args.action == 'set': cs.volumes.set_image_metadata(volume, metadata) elif args.action == 'unset': cs.volumes.delete_image_metadata(volume, sorted(metadata.keys(), reverse=True)) @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=0, help='Shows details for all tenants. Admin only.') @utils.arg('--all_tenants', nargs='?', type=int, const=1, help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help='Filters results by a name. Default=None.') @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--display_name', help=argparse.SUPPRESS) @utils.arg('--status', metavar='', default=None, help='Filters results by a status. Default=None.') @utils.arg('--volume-id', metavar='', default=None, help='Filters results by a volume ID. Default=None.') @utils.arg('--volume_id', help=argparse.SUPPRESS) @utils.arg('--marker', metavar='', default=None, help='Begin returning snapshots that appear later in the snapshot ' 'list than that represented by this id. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, help='Maximum number of snapshots to return. Default=None.') @utils.arg('--sort', metavar='[:]', default=None, help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) @utils.arg('--tenant', type=str, dest='tenant', nargs='?', metavar='', help='Display information from single tenant (Admin only).') def do_snapshot_list(cs, args): """Lists all snapshots.""" all_tenants = (1 if args.tenant else int(os.environ.get("ALL_TENANTS", args.all_tenants))) if args.display_name is not None: args.name = args.display_name search_opts = { 'all_tenants': all_tenants, 'name': args.name, 'status': args.status, 'volume_id': args.volume_id, 'project_id': args.tenant, } snapshots = cs.volume_snapshots.list(search_opts=search_opts, marker=args.marker, limit=args.limit, sort=args.sort) shell_utils.translate_volume_snapshot_keys(snapshots) if args.sort: sortby_index = None else: sortby_index = 0 utils.print_list(snapshots, ['ID', 'Volume ID', 'Status', 'Name', 'Size'], sortby_index=sortby_index) @utils.arg('snapshot', metavar='', help='Name or ID of snapshot.') def do_snapshot_show(cs, args): """Shows snapshot details.""" snapshot = shell_utils.find_volume_snapshot(cs, args.snapshot) shell_utils.print_volume_snapshot(snapshot) @utils.arg('snapshot', metavar='', nargs='+', help='Name or ID of the snapshot(s) to delete.') @utils.arg('--force', action="store_true", help='Allows deleting snapshot of a volume ' 'when its status is other than "available" or "error". ' 'Default=False.') def do_snapshot_delete(cs, args): """Removes one or more snapshots.""" failure_count = 0 for snapshot in args.snapshot: try: shell_utils.find_volume_snapshot(cs, snapshot).delete(args.force) except Exception as e: failure_count += 1 print("Delete for snapshot %s failed: %s" % (snapshot, e)) if failure_count == len(args.snapshot): raise exceptions.CommandError("Unable to delete any of the specified " "snapshots.") @utils.arg('snapshot', metavar='', help='Name or ID of snapshot.') @utils.arg('name', nargs='?', metavar='', help='New name for snapshot.') @utils.arg('--description', metavar='', default=None, help='Snapshot description. Default=None.') @utils.arg('--display-description', help=argparse.SUPPRESS) @utils.arg('--display_description', help=argparse.SUPPRESS) def do_snapshot_rename(cs, args): """Renames a snapshot.""" kwargs = {} if args.name is not None: kwargs['name'] = args.name if args.description is not None: kwargs['description'] = args.description elif args.display_description is not None: kwargs['description'] = args.display_description if not any(kwargs): msg = 'Must supply either name or description.' raise exceptions.ClientException(code=1, message=msg) shell_utils.find_volume_snapshot(cs, args.snapshot).update(**kwargs) print("Request to rename snapshot '%s' has been accepted." % ( args.snapshot)) @utils.arg('snapshot', metavar='', nargs='+', help='Name or ID of snapshot to modify.') @utils.arg('--state', metavar='', default='available', help=('The state to assign to the snapshot. Valid values are ' '"available", "error", "creating", "deleting", and ' '"error_deleting". NOTE: This command simply changes ' 'the state of the Snapshot in the DataBase with no regard ' 'to actual status, exercise caution when using. ' 'Default=available.')) def do_snapshot_reset_state(cs, args): """Explicitly updates the snapshot state.""" failure_count = 0 single = (len(args.snapshot) == 1) for snapshot in args.snapshot: try: shell_utils.find_volume_snapshot( cs, snapshot).reset_state(args.state) except Exception as e: failure_count += 1 msg = "Reset state for snapshot %s failed: %s" % (snapshot, e) if not single: print(msg) if failure_count == len(args.snapshot): if not single: msg = ("Unable to reset the state for any of the specified " "snapshots.") raise exceptions.CommandError(msg) def do_type_list(cs, args): """Lists available 'volume types'. (Only admin and tenant users will see private types) """ vtypes = cs.volume_types.list() shell_utils.print_volume_type_list(vtypes) def do_type_default(cs, args): """List the default volume type. The Block Storage service allows configuration of a default type for each project, as well as the system default, so use this command to determine what your effective default volume type is. """ vtype = cs.volume_types.default() shell_utils.print_volume_type_list([vtype]) @utils.arg('volume_type', metavar='', help='Name or ID of the volume type.') def do_type_show(cs, args): """Show volume type details.""" vtype = shell_utils.find_vtype(cs, args.volume_type) info = dict() info.update(vtype._info) info.pop('links', None) utils.print_dict(info, formatters=['extra_specs']) @utils.arg('id', metavar='', help='ID of the volume type.') @utils.arg('--name', metavar='', help='Name of the volume type.') @utils.arg('--description', metavar='', help='Description of the volume type.') @utils.arg('--is-public', metavar='', help='Make type accessible to the public or not.') def do_type_update(cs, args): """Updates volume type name, description, and/or is_public.""" is_public = args.is_public if args.name is None and args.description is None and is_public is None: raise exceptions.CommandError('Specify a new type name, description, ' 'is_public or a combination thereof.') if is_public is not None: is_public = strutils.bool_from_string(args.is_public, strict=True) vtype = cs.volume_types.update(args.id, args.name, args.description, is_public) shell_utils.print_volume_type_list([vtype]) def do_extra_specs_list(cs, args): """Lists current volume types and extra specs.""" vtypes = cs.volume_types.list() utils.print_list(vtypes, ['ID', 'Name', 'extra_specs']) @utils.arg('name', metavar='', help='Name of new volume type.') @utils.arg('--description', metavar='', help='Description of new volume type.') @utils.arg('--is-public', metavar='', default=True, help='Make type accessible to the public (default true).') def do_type_create(cs, args): """Creates a volume type.""" is_public = strutils.bool_from_string(args.is_public, strict=True) vtype = cs.volume_types.create(args.name, args.description, is_public) shell_utils.print_volume_type_list([vtype]) @utils.arg('vol_type', metavar='', nargs='+', help='Name or ID of volume type or types to delete.') def do_type_delete(cs, args): """Deletes volume type or types.""" failure_count = 0 for vol_type in args.vol_type: try: vtype = shell_utils.find_volume_type(cs, vol_type) cs.volume_types.delete(vtype) print("Request to delete volume type %s has been accepted." % (vol_type)) except Exception as e: failure_count += 1 print("Delete for volume type %s failed: %s" % (vol_type, e)) if failure_count == len(args.vol_type): raise exceptions.CommandError("Unable to delete any of the " "specified types.") @utils.arg('vtype', metavar='', help='Name or ID of volume type.') @utils.arg('action', metavar='', choices=['set', 'unset'], help='The action. Valid values are "set" or "unset."') @utils.arg('metadata', metavar='', nargs='+', default=[], help='The extra specs key and value pair to set or unset. ' 'For unset, specify only the key.') def do_type_key(cs, args): """Sets or unsets extra_spec for a volume type.""" vtype = shell_utils.find_volume_type(cs, args.vtype) keypair = shell_utils.extract_metadata(args) if args.action == 'set': vtype.set_keys(keypair) elif args.action == 'unset': vtype.unset_keys(list(keypair)) @utils.arg('--volume-type', metavar='', required=True, help='Filter results by volume type name or ID.') def do_type_access_list(cs, args): """Print access information about the given volume type.""" volume_type = shell_utils.find_volume_type(cs, args.volume_type) if volume_type.is_public: raise exceptions.CommandError("Failed to get access list " "for public volume type.") access_list = cs.volume_type_access.list(volume_type) columns = ['Volume_type_ID', 'Project_ID'] utils.print_list(access_list, columns) @utils.arg('--volume-type', metavar='', required=True, help='Volume type name or ID to add access for the given project.') @utils.arg('--project-id', metavar='', required=True, help='Project ID to add volume type access for.') def do_type_access_add(cs, args): """Adds volume type access for the given project.""" vtype = shell_utils.find_volume_type(cs, args.volume_type) cs.volume_type_access.add_project_access(vtype, args.project_id) @utils.arg('--volume-type', metavar='', required=True, help=('Volume type name or ID to remove access ' 'for the given project.')) @utils.arg('--project-id', metavar='', required=True, help='Project ID to remove volume type access for.') def do_type_access_remove(cs, args): """Removes volume type access for the given project.""" vtype = shell_utils.find_volume_type(cs, args.volume_type) cs.volume_type_access.remove_project_access( vtype, args.project_id) @utils.arg('tenant', metavar='', help='ID of tenant for which to list quotas.') def do_quota_show(cs, args): """Lists quotas for a tenant.""" shell_utils.quota_show(cs.quotas.get(args.tenant)) @utils.arg('tenant', metavar='', help='ID of tenant for which to list quota usage.') def do_quota_usage(cs, args): """Lists quota usage for a tenant.""" shell_utils.quota_usage_show(cs.quotas.get(args.tenant, usage=True)) @utils.arg('tenant', metavar='', help='ID of tenant for which to list quota defaults.') def do_quota_defaults(cs, args): """Lists default quotas for a tenant.""" shell_utils.quota_show(cs.quotas.defaults(args.tenant)) @utils.arg('tenant', metavar='', help='ID of tenant for which to set quotas.') @utils.arg('--volumes', metavar='', type=int, default=None, help='The new "volumes" quota value. Default=None.') @utils.arg('--snapshots', metavar='', type=int, default=None, help='The new "snapshots" quota value. Default=None.') @utils.arg('--gigabytes', metavar='', type=int, default=None, help='The new "gigabytes" quota value. Default=None.') @utils.arg('--backups', metavar='', type=int, default=None, help='The new "backups" quota value. Default=None.') @utils.arg('--backup-gigabytes', metavar='', type=int, default=None, help='The new "backup_gigabytes" quota value. Default=None.') @utils.arg('--volume-type', metavar='', default=None, help='Volume type. Default=None.') @utils.arg('--per-volume-gigabytes', metavar='', type=int, default=None, help='Set max volume size limit. Default=None.') def do_quota_update(cs, args): """Updates quotas for a tenant.""" shell_utils.quota_update(cs.quotas, args.tenant, args) @utils.arg('tenant', metavar='', help='UUID of tenant to delete the quotas for.') def do_quota_delete(cs, args): """Delete the quotas for a tenant.""" cs.quotas.delete(args.tenant) @utils.arg('class_name', metavar='', help='Name of quota class for which to list quotas.') def do_quota_class_show(cs, args): """Lists quotas for a quota class.""" shell_utils.quota_show(cs.quota_classes.get(args.class_name)) @utils.arg('class_name', metavar='', help='Name of quota class for which to set quotas.') @utils.arg('--volumes', metavar='', type=int, default=None, help='The new "volumes" quota value. Default=None.') @utils.arg('--snapshots', metavar='', type=int, default=None, help='The new "snapshots" quota value. Default=None.') @utils.arg('--gigabytes', metavar='', type=int, default=None, help='The new "gigabytes" quota value. Default=None.') @utils.arg('--backups', metavar='', type=int, default=None, help='The new "backups" quota value. Default=None.') @utils.arg('--backup-gigabytes', metavar='', type=int, default=None, help='The new "backup_gigabytes" quota value. Default=None.') @utils.arg('--volume-type', metavar='', default=None, help='Volume type. Default=None.') @utils.arg('--per-volume-gigabytes', metavar='', type=int, default=None, help='Set max volume size limit. Default=None.') def do_quota_class_update(cs, args): """Updates quotas for a quota class.""" shell_utils.quota_update(cs.quota_classes, args.class_name, args) @utils.arg('tenant', metavar='', nargs='?', default=None, help='Display information for a single tenant (Admin only).') def do_absolute_limits(cs, args): """Lists absolute limits for a user.""" limits = cs.limits.get(args.tenant).absolute columns = ['Name', 'Value'] utils.print_list(limits, columns) @utils.arg('tenant', metavar='', nargs='?', default=None, help='Display information for a single tenant (Admin only).') def do_rate_limits(cs, args): """Lists rate limits for a user.""" limits = cs.limits.get(args.tenant).rate columns = ['Verb', 'URI', 'Value', 'Remain', 'Unit', 'Next_Available'] utils.print_list(limits, columns) @utils.arg('volume', metavar='', help='Name or ID of volume to snapshot.') @utils.arg('--force', metavar='', const=True, nargs='?', default=False, help='Enables or disables upload of ' 'a volume that is attached to an instance. ' 'Default=False. ' 'This option may not be supported by your cloud.') @utils.arg('--container-format', metavar='', default='bare', help='Container format type. ' 'Default is bare.') @utils.arg('--container_format', help=argparse.SUPPRESS) @utils.arg('--disk-format', metavar='', default='raw', help='Disk format type. ' 'Default is raw.') @utils.arg('--disk_format', help=argparse.SUPPRESS) @utils.arg('image_name', metavar='', help='The new image name.') @utils.arg('--image_name', help=argparse.SUPPRESS) def do_upload_to_image(cs, args): """Uploads volume to Image Service as an image.""" volume = utils.find_volume(cs, args.volume) shell_utils.print_volume_image( volume.upload_to_image(args.force, args.image_name, args.container_format, args.disk_format)) @utils.arg('volume', metavar='', help='ID of volume to migrate.') @utils.arg('host', metavar='', help='Destination host. Takes the form: ' 'host@backend-name#pool') @utils.arg('--force-host-copy', metavar='', choices=['True', 'False'], required=False, const=True, nargs='?', default=False, help='Enables or disables generic host-based ' 'force-migration, which bypasses driver ' 'optimizations. Default=False.') @utils.arg('--lock-volume', metavar='', choices=['True', 'False'], required=False, const=True, nargs='?', default=False, help='Enables or disables the termination of volume migration ' 'caused by other commands. This option applies to the ' 'available volume. True means it locks the volume ' 'state and does not allow the migration to be aborted. The ' 'volume status will be in maintenance during the ' 'migration. False means it allows the volume migration ' 'to be aborted. The volume status is still in the original ' 'status. Default=False.') def do_migrate(cs, args): """Migrates volume to a new host.""" volume = utils.find_volume(cs, args.volume) try: volume.migrate_volume(args.host, args.force_host_copy, args.lock_volume) print("Request to migrate volume %s has been accepted." % (volume.id)) except Exception as e: print("Migration for volume %s failed: %s." % (volume.id, e)) @utils.arg('volume', metavar='', help='Name or ID of volume for which to modify type.') @utils.arg('new_type', metavar='', help='New volume type.') @utils.arg('--migration-policy', metavar='', required=False, choices=['never', 'on-demand'], default='never', help='Migration policy during retype of volume.') def do_retype(cs, args): """Changes the volume type for a volume.""" volume = utils.find_volume(cs, args.volume) volume.retype(args.new_type, args.migration_policy) @utils.arg('volume', metavar='', help='Name or ID of volume to backup.') @utils.arg('--container', metavar='', default=None, help='Backup container name. Default=None.') @utils.arg('--display-name', help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help='Backup name. Default=None.') @utils.arg('--display-description', help=argparse.SUPPRESS) @utils.arg('--description', metavar='', default=None, help='Backup description. Default=None.') @utils.arg('--incremental', action='store_true', help='Incremental backup. Default=False.', default=False) @utils.arg('--force', action='store_true', help='Allows or disallows backup of a volume ' 'when the volume is attached to an instance. ' 'If set to True, backs up the volume whether ' 'its status is "available" or "in-use". The backup ' 'of an "in-use" volume means your data is crash ' 'consistent. Default=False.', default=False) @utils.arg('--snapshot-id', metavar='', default=None, help='ID of snapshot to backup. Default=None.') def do_backup_create(cs, args): """Creates a volume backup.""" if args.display_name is not None: args.name = args.display_name if args.display_description is not None: args.description = args.display_description volume = utils.find_volume(cs, args.volume) backup = cs.backups.create(volume.id, args.container, args.name, args.description, args.incremental, args.force, args.snapshot_id) info = {"volume_id": volume.id} info.update(backup._info) if 'links' in info: info.pop('links') utils.print_dict(info) @utils.arg('backup', metavar='', help='Name or ID of backup.') def do_backup_show(cs, args): """Shows backup details.""" backup = shell_utils.find_backup(cs, args.backup) info = dict() info.update(backup._info) info.pop('links', None) utils.print_dict(info) @utils.arg('--all-tenants', metavar='', nargs='?', type=int, const=1, default=0, help='Shows details for all tenants. Admin only.') @utils.arg('--all_tenants', nargs='?', type=int, const=1, help=argparse.SUPPRESS) @utils.arg('--name', metavar='', default=None, help='Filters results by a name. Default=None.') @utils.arg('--status', metavar='', default=None, help='Filters results by a status. Default=None.') @utils.arg('--volume-id', metavar='', default=None, help='Filters results by a volume ID. Default=None.') @utils.arg('--volume_id', help=argparse.SUPPRESS) @utils.arg('--marker', metavar='', default=None, help='Begin returning backups that appear later in the backup ' 'list than that represented by this id. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, help='Maximum number of backups to return. Default=None.') @utils.arg('--sort', metavar='[:]', default=None, help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) def do_backup_list(cs, args): """Lists all backups.""" search_opts = { 'all_tenants': args.all_tenants, 'name': args.name, 'status': args.status, 'volume_id': args.volume_id, } backups = cs.backups.list(search_opts=search_opts, marker=args.marker, limit=args.limit, sort=args.sort) shell_utils.translate_volume_snapshot_keys(backups) columns = ['ID', 'Volume ID', 'Status', 'Name', 'Size', 'Object Count', 'Container'] if args.sort: sortby_index = None else: sortby_index = 0 utils.print_list(backups, columns, sortby_index=sortby_index) @utils.arg('--force', action="store_true", help='Allows deleting backup of a volume ' 'when its status is other than "available" or "error". ' 'Default=False.') @utils.arg('backup', metavar='', nargs='+', help='Name or ID of backup(s) to delete.') def do_backup_delete(cs, args): """Removes one or more backups.""" failure_count = 0 for backup in args.backup: try: shell_utils.find_backup(cs, backup).delete(args.force) print("Request to delete backup %s has been accepted." % (backup)) except Exception as e: failure_count += 1 print("Delete for backup %s failed: %s" % (backup, e)) if failure_count == len(args.backup): raise exceptions.CommandError("Unable to delete any of the specified " "backups.") @utils.arg('backup', metavar='', help='Name or ID of backup to restore.') @utils.arg('--volume-id', metavar='', default=None, help=argparse.SUPPRESS) @utils.arg('--volume', metavar='', default=None, help='Name or ID of existing volume to which to restore. ' 'This is mutually exclusive with --name and takes priority. ' 'Default=None.') @utils.arg('--name', metavar='', default=None, help='Use the name for new volume creation to restore. ' 'This is mutually exclusive with --volume (or the deprecated ' '--volume-id) and --volume (or --volume-id) takes priority. ' 'Default=None.') def do_backup_restore(cs, args): """Restores a backup.""" vol = args.volume or args.volume_id if vol: volume_id = utils.find_volume(cs, vol).id if args.name: args.name = None print('Mutually exclusive options are specified simultaneously: ' '"--volume (or the deprecated --volume-id) and --name". ' 'The --volume (or --volume-id) option takes priority.') else: volume_id = None backup = shell_utils.find_backup(cs, args.backup) restore = cs.restores.restore(backup.id, volume_id, args.name) info = {"backup_id": backup.id} info.update(restore._info) info.pop('links', None) utils.print_dict(info) @utils.arg('backup', metavar='', help='ID of the backup to export.') def do_backup_export(cs, args): """Export backup metadata record.""" info = cs.backups.export_record(args.backup) utils.print_dict(info) @utils.arg('backup_service', metavar='', help='Backup service to use for importing the backup.') @utils.arg('backup_url', metavar='', help='Backup URL for importing the backup metadata.') def do_backup_import(cs, args): """Import backup metadata record.""" info = cs.backups.import_record(args.backup_service, args.backup_url) info.pop('links', None) utils.print_dict(info) @utils.arg('backup', metavar='', nargs='+', help='Name or ID of the backup to modify.') @utils.arg('--state', metavar='', default='available', help='The state to assign to the backup. Valid values are ' '"available", "error". Default=available.') def do_backup_reset_state(cs, args): """Explicitly updates the backup state.""" failure_count = 0 single = (len(args.backup) == 1) for backup in args.backup: try: shell_utils.find_backup(cs, backup).reset_state(args.state) print("Request to update backup '%s' has been accepted." % backup) except Exception as e: failure_count += 1 msg = "Reset state for backup %s failed: %s" % (backup, e) if not single: print(msg) if failure_count == len(args.backup): if not single: msg = ("Unable to reset the state for any of the specified " "backups.") raise exceptions.CommandError(msg) @utils.arg('volume', metavar='', help='Name or ID of volume to transfer.') @utils.arg('--name', metavar='', default=None, help='Transfer name. Default=None.') @utils.arg('--display-name', help=argparse.SUPPRESS) def do_transfer_create(cs, args): """Creates a volume transfer.""" if args.display_name is not None: args.name = args.display_name volume = utils.find_volume(cs, args.volume) transfer = cs.transfers.create(volume.id, args.name) info = dict() info.update(transfer._info) info.pop('links', None) utils.print_dict(info) @utils.arg('transfer', metavar='', help='Name or ID of transfer to delete.') def do_transfer_delete(cs, args): """Undoes a transfer.""" transfer = shell_utils.find_transfer(cs, args.transfer) transfer.delete() @utils.arg('transfer', metavar='', help='ID of transfer to accept.') @utils.arg('auth_key', metavar='', help='Authentication key of transfer to accept.') def do_transfer_accept(cs, args): """Accepts a volume transfer.""" transfer = cs.transfers.accept(args.transfer, args.auth_key) info = dict() info.update(transfer._info) info.pop('links', None) utils.print_dict(info) @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=0, help='Shows details for all tenants. Admin only.') @utils.arg('--all_tenants', nargs='?', type=int, const=1, help=argparse.SUPPRESS) def do_transfer_list(cs, args): """Lists all transfers.""" all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants)) search_opts = { 'all_tenants': all_tenants, } transfers = cs.transfers.list(search_opts=search_opts) columns = ['ID', 'Volume ID', 'Name'] utils.print_list(transfers, columns) @utils.arg('transfer', metavar='', help='Name or ID of transfer to accept.') def do_transfer_show(cs, args): """Shows transfer details.""" transfer = shell_utils.find_transfer(cs, args.transfer) info = dict() info.update(transfer._info) info.pop('links', None) utils.print_dict(info) @utils.arg('volume', metavar='', help='Name or ID of volume to extend.') @utils.arg('new_size', metavar='', type=int, help='New size of volume, in GiBs.') def do_extend(cs, args): """Attempts to extend size of an existing volume.""" volume = utils.find_volume(cs, args.volume) cs.volumes.extend(volume, args.new_size) @utils.arg('--host', metavar='', default=None, help='Host name. Default=None.') @utils.arg('--binary', metavar='', default=None, help='Service binary. Default=None.') @utils.arg('--withreplication', metavar='', const=True, nargs='?', default=False, help='Enables or disables display of ' 'Replication info for c-vol services. Default=False.') def do_service_list(cs, args): """Lists all services. Filter by host and service binary.""" replication = strutils.bool_from_string(args.withreplication, strict=True) result = cs.services.list(host=args.host, binary=args.binary) columns = ["Binary", "Host", "Zone", "Status", "State", "Updated_at"] if replication: columns.extend(["Replication Status", "Active Backend ID", "Frozen"]) # NOTE(jay-lau-513): we check if the response has disabled_reason # so as not to add the column when the extended ext is not enabled. if result and hasattr(result[0], 'disabled_reason'): columns.append("Disabled Reason") utils.print_list(result, columns) @utils.arg('host', metavar='', help='Host name.') @utils.arg('binary', metavar='', help='Service binary.') def do_service_enable(cs, args): """Enables the service.""" result = cs.services.enable(args.host, args.binary) columns = ["Host", "Binary", "Status"] utils.print_list([result], columns) @utils.arg('host', metavar='', help='Host name.') @utils.arg('binary', metavar='', help='Service binary.') @utils.arg('--reason', metavar='', help='Reason for disabling service.') def do_service_disable(cs, args): """Disables the service.""" columns = ["Host", "Binary", "Status"] if args.reason: columns.append('Disabled Reason') result = cs.services.disable_log_reason(args.host, args.binary, args.reason) else: result = cs.services.disable(args.host, args.binary) utils.print_list([result], columns) def treeizeAvailabilityZone(zone): """Builds a tree view for availability zones.""" AvailabilityZone = availability_zones.AvailabilityZone az = AvailabilityZone(zone.manager, copy.deepcopy(zone._info), zone._loaded) result = [] # Zone tree view item az.zoneName = zone.zoneName az.zoneState = ('available' if zone.zoneState['available'] else 'not available') az._info['zoneName'] = az.zoneName az._info['zoneState'] = az.zoneState result.append(az) if getattr(zone, "hosts", None) and zone.hosts is not None: for (host, services) in zone.hosts.items(): # Host tree view item az = AvailabilityZone(zone.manager, copy.deepcopy(zone._info), zone._loaded) az.zoneName = '|- %s' % host az.zoneState = '' az._info['zoneName'] = az.zoneName az._info['zoneState'] = az.zoneState result.append(az) for (svc, state) in services.items(): # Service tree view item az = AvailabilityZone(zone.manager, copy.deepcopy(zone._info), zone._loaded) az.zoneName = '| |- %s' % svc az.zoneState = '%s %s %s' % ( 'enabled' if state['active'] else 'disabled', ':-)' if state['available'] else 'XXX', state['updated_at']) az._info['zoneName'] = az.zoneName az._info['zoneState'] = az.zoneState result.append(az) return result def do_availability_zone_list(cs, _args): """Lists all availability zones.""" try: availability_zones = cs.availability_zones.list() except exceptions.Forbidden: # policy doesn't allow probably try: availability_zones = cs.availability_zones.list(detailed=False) except Exception: raise result = [] for zone in availability_zones: result += treeizeAvailabilityZone(zone) shell_utils.translate_availability_zone_keys(result) utils.print_list(result, ['Name', 'Status']) def do_encryption_type_list(cs, args): """Shows encryption type details for volume types. Admin only.""" result = cs.volume_encryption_types.list() utils.print_list(result, ['Volume Type ID', 'Provider', 'Cipher', 'Key Size', 'Control Location']) @utils.arg('volume_type', metavar='', type=str, help='Name or ID of volume type.') def do_encryption_type_show(cs, args): """Shows encryption type details for a volume type. Admin only.""" volume_type = shell_utils.find_volume_type(cs, args.volume_type) result = cs.volume_encryption_types.get(volume_type) # Display result or an empty table if no result if hasattr(result, 'volume_type_id'): shell_utils.print_volume_encryption_type_list([result]) else: shell_utils.print_volume_encryption_type_list([]) @utils.arg('volume_type', metavar='', type=str, help='Name or ID of volume type.') @utils.arg('provider', metavar='', type=str, help='The encryption provider format. ' 'For example, "luks" or "plain".') @utils.arg('--cipher', metavar='', type=str, required=False, default=None, help='The encryption algorithm or mode. ' 'For example, aes-xts-plain64. Default=None.') @utils.arg('--key-size', metavar='', type=int, required=False, default=None, help='Size of encryption key, in bits. ' 'For example, 128 or 256. Default=None.') @utils.arg('--key_size', type=int, required=False, default=None, help=argparse.SUPPRESS) @utils.arg('--control-location', metavar='', choices=['front-end', 'back-end'], type=str, required=False, default='front-end', help='Notional service where encryption is performed. ' 'Valid values are "front-end" or "back-end". ' 'For example, front-end=Nova. Default is "front-end".') @utils.arg('--control_location', type=str, required=False, default='front-end', help=argparse.SUPPRESS) def do_encryption_type_create(cs, args): """Creates encryption type for a volume type. Admin only.""" volume_type = shell_utils.find_volume_type(cs, args.volume_type) body = { 'provider': args.provider, 'cipher': args.cipher, 'key_size': args.key_size, 'control_location': args.control_location } result = cs.volume_encryption_types.create(volume_type, body) shell_utils.print_volume_encryption_type_list([result]) @utils.arg('volume_type', metavar='', type=str, help="Name or ID of the volume type") @utils.arg('--provider', metavar='', type=str, required=False, default=argparse.SUPPRESS, help="Encryption provider format (e.g. 'luks' or 'plain').") @utils.arg('--cipher', metavar='', type=str, nargs='?', required=False, default=argparse.SUPPRESS, const=None, help="Encryption algorithm/mode to use (e.g., aes-xts-plain64). " "Provide parameter without value to set to provider default.") @utils.arg('--key-size', dest='key_size', metavar='', type=int, nargs='?', required=False, default=argparse.SUPPRESS, const=None, help="Size of the encryption key, in bits (e.g., 128, 256). " "Provide parameter without value to set to provider default. ") @utils.arg('--control-location', dest='control_location', metavar='', choices=['front-end', 'back-end'], type=str, required=False, default=argparse.SUPPRESS, help="Notional service where encryption is performed (e.g., " "front-end=Nova). Values: 'front-end', 'back-end'") def do_encryption_type_update(cs, args): """Update encryption type information for a volume type (Admin Only).""" volume_type = shell_utils.find_volume_type(cs, args.volume_type) # An argument should only be pulled if the user specified the parameter. body = {} for attr in ['provider', 'cipher', 'key_size', 'control_location']: if hasattr(args, attr): body[attr] = getattr(args, attr) cs.volume_encryption_types.update(volume_type, body) result = cs.volume_encryption_types.get(volume_type) shell_utils.print_volume_encryption_type_list([result]) @utils.arg('volume_type', metavar='', type=str, help='Name or ID of volume type.') def do_encryption_type_delete(cs, args): """Deletes encryption type for a volume type. Admin only.""" volume_type = shell_utils.find_volume_type(cs, args.volume_type) cs.volume_encryption_types.delete(volume_type) @utils.arg('name', metavar='', help='Name of new QoS specifications.') @utils.arg('metadata', metavar='', nargs='+', default=[], help='QoS specifications.') def do_qos_create(cs, args): """Creates a qos specs.""" keypair = None if args.metadata is not None: keypair = shell_utils.extract_metadata(args) qos_specs = cs.qos_specs.create(args.name, keypair) shell_utils.print_qos_specs(qos_specs) def do_qos_list(cs, args): """Lists qos specs.""" qos_specs = cs.qos_specs.list() shell_utils.print_qos_specs_list(qos_specs) @utils.arg('qos_specs', metavar='', help='ID of QoS specifications to show.') def do_qos_show(cs, args): """Shows qos specs details.""" qos_specs = shell_utils.find_qos_specs(cs, args.qos_specs) shell_utils.print_qos_specs(qos_specs) @utils.arg('qos_specs', metavar='', help='ID of QoS specifications to delete.') @utils.arg('--force', metavar='', const=True, nargs='?', default=False, help='Enables or disables deletion of in-use ' 'QoS specifications. Default=False.') def do_qos_delete(cs, args): """Deletes a specified qos specs.""" force = strutils.bool_from_string(args.force, strict=True) qos_specs = shell_utils.find_qos_specs(cs, args.qos_specs) cs.qos_specs.delete(qos_specs, force) @utils.arg('qos_specs', metavar='', help='ID of QoS specifications.') @utils.arg('vol_type_id', metavar='', help='ID of volume type with which to associate ' 'QoS specifications.') def do_qos_associate(cs, args): """Associates qos specs with specified volume type.""" cs.qos_specs.associate(args.qos_specs, args.vol_type_id) @utils.arg('qos_specs', metavar='', help='ID of QoS specifications.') @utils.arg('vol_type_id', metavar='', help='ID of volume type with which to associate ' 'QoS specifications.') def do_qos_disassociate(cs, args): """Disassociates qos specs from specified volume type.""" cs.qos_specs.disassociate(args.qos_specs, args.vol_type_id) @utils.arg('qos_specs', metavar='', help='ID of QoS specifications on which to operate.') def do_qos_disassociate_all(cs, args): """Disassociates qos specs from all its associations.""" cs.qos_specs.disassociate_all(args.qos_specs) @utils.arg('qos_specs', metavar='', help='ID of QoS specifications.') @utils.arg('action', metavar='', choices=['set', 'unset'], help='The action. Valid values are "set" or "unset."') @utils.arg('metadata', metavar='key=value', nargs='+', default=[], help='Metadata key and value pair to set or unset. ' 'For unset, specify only the key.') def do_qos_key(cs, args): """Sets or unsets specifications for a qos spec.""" keypair = shell_utils.extract_metadata(args) if args.action == 'set': cs.qos_specs.set_keys(args.qos_specs, keypair) elif args.action == 'unset': cs.qos_specs.unset_keys(args.qos_specs, list(keypair)) @utils.arg('qos_specs', metavar='', help='ID of QoS specifications.') def do_qos_get_association(cs, args): """Lists all associations for specified qos specs.""" associations = cs.qos_specs.get_associations(args.qos_specs) shell_utils.print_associations_list(associations) @utils.arg('snapshot', metavar='', help='ID of snapshot for which to update metadata.') @utils.arg('action', metavar='', choices=['set', 'unset'], help='The action. Valid values are "set" or "unset."') @utils.arg('metadata', metavar='', nargs='+', default=[], help='Metadata key and value pair to set or unset. ' 'For unset, specify only the key.') def do_snapshot_metadata(cs, args): """Sets or deletes snapshot metadata.""" snapshot = shell_utils.find_volume_snapshot(cs, args.snapshot) metadata = shell_utils.extract_metadata(args) if args.action == 'set': metadata = snapshot.set_metadata(metadata) utils.print_dict(metadata._info) elif args.action == 'unset': snapshot.delete_metadata(list(metadata.keys())) @utils.arg('snapshot', metavar='', help='ID of snapshot.') def do_snapshot_metadata_show(cs, args): """Shows snapshot metadata.""" snapshot = shell_utils.find_volume_snapshot(cs, args.snapshot) utils.print_dict(snapshot._info['metadata'], 'Metadata-property') @utils.arg('volume', metavar='', help='ID of volume.') def do_metadata_show(cs, args): """Shows volume metadata.""" volume = utils.find_volume(cs, args.volume) utils.print_dict(volume._info['metadata'], 'Metadata-property') @utils.arg('volume', metavar='', help='ID of volume.') def do_image_metadata_show(cs, args): """Shows volume image metadata.""" volume = utils.find_volume(cs, args.volume) resp, body = volume.show_image_metadata(volume) utils.print_dict(body['metadata'], 'Metadata-property') @utils.arg('volume', metavar='', help='ID of volume for which to update metadata.') @utils.arg('metadata', metavar='', nargs='+', default=[], help='Metadata key and value pair or pairs to update.') def do_metadata_update_all(cs, args): """Updates volume metadata.""" volume = utils.find_volume(cs, args.volume) metadata = shell_utils.extract_metadata(args) metadata = volume.update_all_metadata(metadata) utils.print_dict(metadata['metadata'], 'Metadata-property') @utils.arg('snapshot', metavar='', help='ID of snapshot for which to update metadata.') @utils.arg('metadata', metavar='', nargs='+', default=[], help='Metadata key and value pair to update.') def do_snapshot_metadata_update_all(cs, args): """Updates snapshot metadata.""" snapshot = shell_utils.find_volume_snapshot(cs, args.snapshot) metadata = shell_utils.extract_metadata(args) metadata = snapshot.update_all_metadata(metadata) utils.print_dict(metadata) @utils.arg('volume', metavar='', help='ID of volume to update.') @utils.arg('read_only', metavar='', choices=['True', 'true', 'False', 'false'], help='Enables or disables update of volume to ' 'read-only access mode.') def do_readonly_mode_update(cs, args): """Updates volume read-only access-mode flag.""" volume = utils.find_volume(cs, args.volume) cs.volumes.update_readonly_flag(volume, strutils.bool_from_string(args.read_only, strict=True)) @utils.arg('volume', metavar='', help='ID of the volume to update.') @utils.arg('bootable', metavar='', choices=['True', 'true', 'False', 'false'], help='Flag to indicate whether volume is bootable.') def do_set_bootable(cs, args): """Update bootable status of a volume.""" volume = utils.find_volume(cs, args.volume) cs.volumes.set_bootable(volume, strutils.bool_from_string(args.bootable, strict=True)) @utils.arg('host', metavar='', help='Cinder host on which the existing volume resides; ' 'takes the form: host@backend-name#pool') @utils.arg('identifier', metavar='', help='Name or other Identifier for existing volume') @utils.arg('--id-type', metavar='', default='source-name', help='Type of backend device identifier provided, ' 'typically source-name or source-id (Default=source-name)') @utils.arg('--name', metavar='', help='Volume name (Default=None)') @utils.arg('--description', metavar='', help='Volume description (Default=None)') @utils.arg('--volume-type', metavar='', help='Volume type (Default=None)') @utils.arg('--availability-zone', metavar='', help='Availability zone for volume (Default=None)') @utils.arg('--metadata', nargs='*', metavar='', help='Metadata key=value pairs (Default=None)') @utils.arg('--bootable', action='store_true', help='Specifies that the newly created volume should be' ' marked as bootable') def do_manage(cs, args): """Manage an existing volume.""" volume_metadata = None if args.metadata is not None: volume_metadata = shell_utils.extract_metadata(args) # Build a dictionary of key/value pairs to pass to the API. ref_dict = {args.id_type: args.identifier} # The recommended way to specify an existing volume is by ID or name, and # have the Cinder driver look for 'source-name' or 'source-id' elements in # the ref structure. To make things easier for the user, we have special # --source-name and --source-id CLI options that add the appropriate # element to the ref structure. # # Note how argparse converts hyphens to underscores. We use hyphens in the # dictionary so that it is consistent with what the user specified on the # CLI. if hasattr(args, 'source_name') and args.source_name is not None: ref_dict['source-name'] = args.source_name if hasattr(args, 'source_id') and args.source_id is not None: ref_dict['source-id'] = args.source_id volume = cs.volumes.manage(host=args.host, ref=ref_dict, name=args.name, description=args.description, volume_type=args.volume_type, availability_zone=args.availability_zone, metadata=volume_metadata, bootable=args.bootable) info = {} volume = cs.volumes.get(volume.id) info.update(volume._info) info.pop('links', None) utils.print_dict(info) @utils.arg('volume', metavar='', help='Name or ID of the volume to unmanage.') def do_unmanage(cs, args): """Stop managing a volume.""" volume = utils.find_volume(cs, args.volume) cs.volumes.unmanage(volume.id) @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=0, help='Shows details for all tenants. Admin only.') def do_consisgroup_list(cs, args): """Lists all consistency groups.""" consistencygroups = cs.consistencygroups.list() columns = ['ID', 'Status', 'Name'] utils.print_list(consistencygroups, columns) @utils.arg('consistencygroup', metavar='', help='Name or ID of a consistency group.') def do_consisgroup_show(cs, args): """Shows details of a consistency group.""" info = dict() consistencygroup = shell_utils.find_consistencygroup(cs, args.consistencygroup) info.update(consistencygroup._info) info.pop('links', None) utils.print_dict(info) @utils.arg('volumetypes', metavar='', help='Volume types.') @utils.arg('--name', metavar='', help='Name of a consistency group.') @utils.arg('--description', metavar='', default=None, help='Description of a consistency group. Default=None.') @utils.arg('--availability-zone', metavar='', default=None, help='Availability zone for volume. Default=None.') def do_consisgroup_create(cs, args): """Creates a consistency group.""" consistencygroup = cs.consistencygroups.create( args.volumetypes, args.name, args.description, availability_zone=args.availability_zone) info = dict() consistencygroup = cs.consistencygroups.get(consistencygroup.id) info.update(consistencygroup._info) info.pop('links', None) utils.print_dict(info) @utils.arg('--cgsnapshot', metavar='', help='Name or ID of a cgsnapshot. Default=None.') @utils.arg('--source-cg', metavar='', help='Name or ID of a source CG. Default=None.') @utils.arg('--name', metavar='', help='Name of a consistency group. Default=None.') @utils.arg('--description', metavar='', help='Description of a consistency group. Default=None.') def do_consisgroup_create_from_src(cs, args): """Creates a consistency group from a cgsnapshot or a source CG.""" if not args.cgsnapshot and not args.source_cg: msg = ('Cannot create consistency group because neither ' 'cgsnapshot nor source CG is provided.') raise exceptions.ClientException(code=1, message=msg) if args.cgsnapshot and args.source_cg: msg = ('Cannot create consistency group because both ' 'cgsnapshot and source CG are provided.') raise exceptions.ClientException(code=1, message=msg) cgsnapshot = None if args.cgsnapshot: cgsnapshot = shell_utils.find_cgsnapshot(cs, args.cgsnapshot) source_cg = None if args.source_cg: source_cg = shell_utils.find_consistencygroup(cs, args.source_cg) info = cs.consistencygroups.create_from_src( cgsnapshot.id if cgsnapshot else None, source_cg.id if source_cg else None, args.name, args.description) info.pop('links', None) utils.print_dict(info) @utils.arg('consistencygroup', metavar='', nargs='+', help='Name or ID of one or more consistency groups ' 'to be deleted.') @utils.arg('--force', action='store_true', default=False, help='Allows or disallows consistency groups ' 'to be deleted. If the consistency group is empty, ' 'it can be deleted without the force flag. ' 'If the consistency group is not empty, the force ' 'flag is required for it to be deleted.') def do_consisgroup_delete(cs, args): """Removes one or more consistency groups.""" failure_count = 0 for consistencygroup in args.consistencygroup: try: shell_utils.find_consistencygroup( cs, consistencygroup).delete(args.force) except Exception as e: failure_count += 1 print("Delete for consistency group %s failed: %s" % (consistencygroup, e)) if failure_count == len(args.consistencygroup): raise exceptions.CommandError("Unable to delete any of the specified " "consistency groups.") @utils.arg('consistencygroup', metavar='', help='Name or ID of a consistency group.') @utils.arg('--name', metavar='', help='New name for consistency group. Default=None.') @utils.arg('--description', metavar='', help='New description for consistency group. Default=None.') @utils.arg('--add-volumes', metavar='', help='UUID of one or more volumes ' 'to be added to the consistency group, ' 'separated by commas. Default=None.') @utils.arg('--remove-volumes', metavar='', help='UUID of one or more volumes ' 'to be removed from the consistency group, ' 'separated by commas. Default=None.') def do_consisgroup_update(cs, args): """Updates a consistency group.""" kwargs = {} if args.name is not None: kwargs['name'] = args.name if args.description is not None: kwargs['description'] = args.description if args.add_volumes is not None: kwargs['add_volumes'] = args.add_volumes if args.remove_volumes is not None: kwargs['remove_volumes'] = args.remove_volumes if not kwargs: msg = ('At least one of the following args must be supplied: ' 'name, description, add-volumes, remove-volumes.') raise exceptions.ClientException(code=1, message=msg) shell_utils.find_consistencygroup( cs, args.consistencygroup).update(**kwargs) print("Request to update consistency group '%s' has been accepted." % ( args.consistencygroup)) @utils.arg('--all-tenants', dest='all_tenants', metavar='<0|1>', nargs='?', type=int, const=1, default=0, help='Shows details for all tenants. Admin only.') @utils.arg('--status', metavar='', default=None, help='Filters results by a status. Default=None.') @utils.arg('--consistencygroup-id', metavar='', default=None, help='Filters results by a consistency group ID. Default=None.') def do_cgsnapshot_list(cs, args): """Lists all cgsnapshots.""" all_tenants = int(os.environ.get("ALL_TENANTS", args.all_tenants)) search_opts = { 'all_tenants': all_tenants, 'status': args.status, 'consistencygroup_id': args.consistencygroup_id, } cgsnapshots = cs.cgsnapshots.list(search_opts=search_opts) columns = ['ID', 'Status', 'Name'] utils.print_list(cgsnapshots, columns) @utils.arg('cgsnapshot', metavar='', help='Name or ID of cgsnapshot.') def do_cgsnapshot_show(cs, args): """Shows cgsnapshot details.""" info = dict() cgsnapshot = shell_utils.find_cgsnapshot(cs, args.cgsnapshot) info.update(cgsnapshot._info) info.pop('links', None) utils.print_dict(info) @utils.arg('consistencygroup', metavar='', help='Name or ID of a consistency group.') @utils.arg('--name', metavar='', default=None, help='Cgsnapshot name. Default=None.') @utils.arg('--description', metavar='', default=None, help='Cgsnapshot description. Default=None.') def do_cgsnapshot_create(cs, args): """Creates a cgsnapshot.""" consistencygroup = shell_utils.find_consistencygroup(cs, args.consistencygroup) cgsnapshot = cs.cgsnapshots.create( consistencygroup.id, args.name, args.description) info = dict() cgsnapshot = cs.cgsnapshots.get(cgsnapshot.id) info.update(cgsnapshot._info) info.pop('links', None) utils.print_dict(info) @utils.arg('cgsnapshot', metavar='', nargs='+', help='Name or ID of one or more cgsnapshots to be deleted.') def do_cgsnapshot_delete(cs, args): """Removes one or more cgsnapshots.""" failure_count = 0 for cgsnapshot in args.cgsnapshot: try: shell_utils.find_cgsnapshot(cs, cgsnapshot).delete() except Exception as e: failure_count += 1 print("Delete for cgsnapshot %s failed: %s" % (cgsnapshot, e)) if failure_count == len(args.cgsnapshot): raise exceptions.CommandError("Unable to delete any of the specified " "cgsnapshots.") @utils.arg('--detail', action='store_true', help='Show detailed information about pools.') def do_get_pools(cs, args): """Show pool information for backends. Admin only.""" pools = cs.volumes.get_pools(args.detail) infos = dict() infos.update(pools._info) for info in infos['pools']: backend = dict() backend['name'] = info['name'] if args.detail: backend.update(info['capabilities']) utils.print_dict(backend) @utils.arg('host', metavar='', help='Cinder host to show backend volume stats and properties; ' 'takes the form: host@backend-name') def do_get_capabilities(cs, args): """Show backend volume stats and properties. Admin only.""" capabilities = cs.capabilities.get(args.host) infos = dict() infos.update(capabilities._info) prop = infos.pop('properties', None) utils.print_dict(infos, "Volume stats") utils.print_dict(prop, "Backend properties", formatters=sorted(prop.keys())) @utils.arg('volume', metavar='', help='Cinder volume that already exists in the volume backend.') @utils.arg('identifier', metavar='', help='Name or other identifier for existing snapshot. This is ' 'backend specific.') @utils.arg('--id-type', metavar='', default='source-name', help='Type of backend device identifier provided, ' 'typically source-name or source-id (Default=source-name).') @utils.arg('--name', metavar='', help='Snapshot name (Default=None).') @utils.arg('--description', metavar='', help='Snapshot description (Default=None).') @utils.arg('--metadata', nargs='*', metavar='', help='Metadata key=value pairs (Default=None).') def do_snapshot_manage(cs, args): """Manage an existing snapshot.""" snapshot_metadata = None if args.metadata is not None: snapshot_metadata = shell_utils.extract_metadata(args) # Build a dictionary of key/value pairs to pass to the API. ref_dict = {args.id_type: args.identifier} if hasattr(args, 'source_name') and args.source_name is not None: ref_dict['source-name'] = args.source_name if hasattr(args, 'source_id') and args.source_id is not None: ref_dict['source-id'] = args.source_id volume = utils.find_volume(cs, args.volume) snapshot = cs.volume_snapshots.manage(volume_id=volume.id, ref=ref_dict, name=args.name, description=args.description, metadata=snapshot_metadata) info = {} snapshot = cs.volume_snapshots.get(snapshot.id) info.update(snapshot._info) info.pop('links', None) utils.print_dict(info) @utils.arg('snapshot', metavar='', help='Name or ID of the snapshot to unmanage.') def do_snapshot_unmanage(cs, args): """Stop managing a snapshot.""" snapshot = shell_utils.find_volume_snapshot(cs, args.snapshot) cs.volume_snapshots.unmanage(snapshot.id) @utils.arg('host', metavar='', help='Host name.') def do_freeze_host(cs, args): """Freeze and disable the specified cinder-volume host.""" cs.services.freeze_host(args.host) @utils.arg('host', metavar='', help='Host name.') def do_thaw_host(cs, args): """Thaw and enable the specified cinder-volume host.""" cs.services.thaw_host(args.host) @utils.arg('host', metavar='', help='Host name.') @utils.arg('--backend_id', metavar='', help='ID of backend to failover to (Default=None)') def do_failover_host(cs, args): """Failover a replicating cinder-volume host.""" cs.services.failover_host(args.host, args.backend_id) @utils.arg('host', metavar='', help='Cinder host on which to list manageable volumes; ' 'takes the form: host@backend-name#pool') @utils.arg('--detailed', metavar='', default=True, help='Returned detailed information (default true).') @utils.arg('--marker', metavar='', default=None, help='Begin returning volumes that appear later in the volume ' 'list than that represented by this volume id. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, help='Maximum number of volumes to return. Default=None.') @utils.arg('--offset', metavar='', default=None, help='Number of volumes to skip after marker. Default=None.') @utils.arg('--sort', metavar='[:]', default=None, help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) def do_manageable_list(cs, args): """Lists all manageable volumes.""" detailed = strutils.bool_from_string(args.detailed) volumes = cs.volumes.list_manageable(host=args.host, detailed=detailed, marker=args.marker, limit=args.limit, offset=args.offset, sort=args.sort) columns = ['reference', 'size', 'safe_to_manage'] if detailed: columns.extend(['reason_not_safe', 'cinder_id', 'extra_info']) utils.print_list(volumes, columns, sortby_index=None) @utils.arg('host', metavar='', help='Cinder host on which to list manageable snapshots; ' 'takes the form: host@backend-name#pool') @utils.arg('--detailed', metavar='', default=True, help='Returned detailed information (default true).') @utils.arg('--marker', metavar='', default=None, help='Begin returning snapshots that appear later in the snapshot ' 'list than that represented by this snapshot id. ' 'Default=None.') @utils.arg('--limit', metavar='', default=None, help='Maximum number of snapshots to return. Default=None.') @utils.arg('--offset', metavar='', default=None, help='Number of snapshots to skip after marker. Default=None.') @utils.arg('--sort', metavar='[:]', default=None, help=(('Comma-separated list of sort keys and directions in the ' 'form of [:]. ' 'Valid keys: %s. ' 'Default=None.') % ', '.join(base.SORT_KEY_VALUES))) def do_snapshot_manageable_list(cs, args): """Lists all manageable snapshots.""" detailed = strutils.bool_from_string(args.detailed) snapshots = cs.volume_snapshots.list_manageable(host=args.host, detailed=detailed, marker=args.marker, limit=args.limit, offset=args.offset, sort=args.sort) columns = ['reference', 'size', 'safe_to_manage', 'source_reference'] if detailed: columns.extend(['reason_not_safe', 'cinder_id', 'extra_info']) utils.print_list(snapshots, columns, sortby_index=None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/volume_backups.py0000664000175000017500000002133400000000000024116 0ustar00zuulzuul00000000000000# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. # 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. """ Volume Backups interface (v3 extension). """ from cinderclient import api_versions from cinderclient.apiclient import base as common_base from cinderclient import base class VolumeBackup(base.Resource): """A volume backup is a block level backup of a volume.""" def __repr__(self): return "" % self.id def delete(self, force=False): """Delete this volume backup.""" return self.manager.delete(self, force) def reset_state(self, state): return self.manager.reset_state(self, state) def update(self, **kwargs): """Update the name or description for this backup.""" return self.manager.update(self, **kwargs) class VolumeBackupManager(base.ManagerWithFind): """Manage :class:`VolumeBackup` resources.""" resource_class = VolumeBackup @api_versions.wraps("3.9") def update(self, backup, **kwargs): """Update the name or description for a backup. :param backup: The :class:`Backup` to update. """ if not kwargs: return body = {"backup": kwargs} return self._update("/backups/%s" % base.getid(backup), body) @api_versions.wraps("3.0") def create(self, volume_id, container=None, name=None, description=None, incremental=False, force=False, snapshot_id=None): """Creates a volume backup. :param volume_id: The ID of the volume to backup. :param container: The name of the backup service container. :param name: The name of the backup. :param description: The description of the backup. :param incremental: Incremental backup. :param force: If True, allows an in-use volume to be backed up. :param snapshot_id: The ID of the snapshot to backup. This should be a snapshot of the src volume, when specified, the new backup will be based on the snapshot. :rtype: :class:`VolumeBackup` """ return self._create_backup(volume_id, container, name, description, incremental, force, snapshot_id) @api_versions.wraps("3.43") def create(self, volume_id, container=None, # noqa: F811 name=None, description=None, incremental=False, force=False, snapshot_id=None, metadata=None): """Creates a volume backup. :param volume_id: The ID of the volume to backup. :param container: The name of the backup service container. :param name: The name of the backup. :param description: The description of the backup. :param incremental: Incremental backup. :param force: If True, allows an in-use volume to be backed up. :param metadata: Key Value pairs :param snapshot_id: The ID of the snapshot to backup. This should be a snapshot of the src volume, when specified, the new backup will be based on the snapshot. :rtype: :class:`VolumeBackup` """ # pylint: disable=function-redefined return self._create_backup(volume_id, container, name, description, incremental, force, snapshot_id, metadata) @api_versions.wraps("3.51") def create(self, volume_id, container=None, name=None, # noqa: F811 description=None, incremental=False, force=False, snapshot_id=None, metadata=None, availability_zone=None): return self._create_backup(volume_id, container, name, description, incremental, force, snapshot_id, metadata, availability_zone) def _create_backup(self, volume_id, container=None, name=None, description=None, incremental=False, force=False, snapshot_id=None, metadata=None, availability_zone=None): """Creates a volume backup. :param volume_id: The ID of the volume to backup. :param container: The name of the backup service container. :param name: The name of the backup. :param description: The description of the backup. :param incremental: Incremental backup. :param force: If True, allows an in-use volume to be backed up. :param metadata: Key Value pairs :param snapshot_id: The ID of the snapshot to backup. This should be a snapshot of the src volume, when specified, the new backup will be based on the snapshot. :param availability_zone: The AZ where we want the backup stored. :rtype: :class:`VolumeBackup` """ # pylint: disable=function-redefined body = {'backup': {'volume_id': volume_id, 'container': container, 'name': name, 'description': description, 'incremental': incremental, 'force': force, 'snapshot_id': snapshot_id, }} if metadata: body['backup']['metadata'] = metadata if availability_zone: body['backup']['availability_zone'] = availability_zone return self._create('/backups', body, 'backup') def get(self, backup_id): """Show volume backup details. :param backup_id: The ID of the backup to display. :rtype: :class:`VolumeBackup` """ return self._get("/backups/%s" % backup_id, "backup") def list(self, detailed=True, search_opts=None, marker=None, limit=None, sort=None): """Get a list of all volume backups. :rtype: list of :class:`VolumeBackup` """ resource_type = "backups" url = self._build_list_url(resource_type, detailed=detailed, search_opts=search_opts, marker=marker, limit=limit, sort=sort) return self._list(url, resource_type, limit=limit) def delete(self, backup, force=False): """Delete a volume backup. :param backup: The :class:`VolumeBackup` to delete. :param force: Allow delete in state other than error or available. """ if force: return self._action('os-force_delete', backup) else: return self._delete("/backups/%s" % base.getid(backup)) def reset_state(self, backup, state): """Update the specified volume backup with the provided state.""" return self._action('os-reset_status', backup, {'status': state} if state else {}) def _action(self, action, backup, info=None, **kwargs): """Perform a volume backup action.""" body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/backups/%s/action' % base.getid(backup) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) def export_record(self, backup_id): """Export volume backup metadata record. :param backup_id: The ID of the backup to export. :rtype: A dictionary containing 'backup_url' and 'backup_service'. """ resp, body = \ self.api.client.get("/backups/%s/export_record" % backup_id) return common_base.DictWithMeta(body['backup-record'], resp) def import_record(self, backup_service, backup_url): """Import volume backup metadata record. :param backup_service: Backup service to use for importing the backup :param backup_url: Backup URL for importing the backup metadata :rtype: A dictionary containing volume backup metadata. """ body = {'backup-record': {'backup_service': backup_service, 'backup_url': backup_url}} self.run_hooks('modify_body_for_update', body, 'backup-record') resp, body = self.api.client.post("/backups/import_record", body=body) return common_base.DictWithMeta(body['backup'], resp) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/volume_backups_restore.py0000664000175000017500000000320500000000000025656 0ustar00zuulzuul00000000000000# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. # 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. """Volume Backups Restore interface (v3 extension). This is part of the Volume Backups interface. """ from cinderclient import base class VolumeBackupsRestore(base.Resource): """A Volume Backups Restore represents a restore operation.""" def __repr__(self): return "" % self.volume_id class VolumeBackupRestoreManager(base.Manager): """Manage :class:`VolumeBackupsRestore` resources.""" resource_class = VolumeBackupsRestore def restore(self, backup_id, volume_id=None, name=None): """Restore a backup to a volume. :param backup_id: The ID of the backup to restore. :param volume_id: The ID of the volume to restore the backup to. :param name : The name for new volume creation to restore. :rtype: :class:`Restore` """ body = {'restore': {'volume_id': volume_id, 'name': name}} return self._create("/backups/%s/restore" % backup_id, body, "restore") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/volume_encryption_types.py0000664000175000017500000000756100000000000026112 0ustar00zuulzuul00000000000000# Copyright (c) 2013 The Johns Hopkins University/Applied Physics Laboratory # 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. """ Volume Encryption Type interface """ from cinderclient.apiclient import base as common_base from cinderclient import base class VolumeEncryptionType(base.Resource): """ A Volume Encryption Type is a collection of settings used to conduct encryption for a specific volume type. """ def __repr__(self): return "" % self.encryption_id class VolumeEncryptionTypeManager(base.ManagerWithFind): """ Manage :class: `VolumeEncryptionType` resources. """ resource_class = VolumeEncryptionType def list(self, search_opts=None): """ List all volume encryption types. :param search_opts: Search options to filter out volume encryption types :return: a list of :class: VolumeEncryptionType instances """ # Since the encryption type is a volume type extension, we cannot get # all encryption types without going through all volume types. volume_types = self.api.volume_types.list() encryption_types = [] list_of_resp = [] for volume_type in volume_types: encryption_type = self._get("/types/%s/encryption" % base.getid(volume_type)) if hasattr(encryption_type, 'volume_type_id'): encryption_types.append(encryption_type) list_of_resp.extend(encryption_type.request_ids) return common_base.ListWithMeta(encryption_types, list_of_resp) def get(self, volume_type): """ Get the volume encryption type for the specified volume type. :param volume_type: the volume type to query :return: an instance of :class: VolumeEncryptionType """ return self._get("/types/%s/encryption" % base.getid(volume_type)) def create(self, volume_type, specs): """ Creates encryption type for a volume type. Default: admin only. :param volume_type: the volume type on which to add an encryption type :param specs: the encryption type specifications to add :return: an instance of :class: VolumeEncryptionType """ body = {'encryption': specs} return self._create("/types/%s/encryption" % base.getid(volume_type), body, "encryption") def update(self, volume_type, specs): """ Update the encryption type information for the specified volume type. :param volume_type: the volume type whose encryption type information must be updated :param specs: the encryption type specifications to update :return: an instance of :class: VolumeEncryptionType """ body = {'encryption': specs} return self._update("/types/%s/encryption/provider" % base.getid(volume_type), body) def delete(self, volume_type): """ Delete the encryption type information for the specified volume type. :param volume_type: the volume type whose encryption type information must be deleted """ return self._delete("/types/%s/encryption/provider" % base.getid(volume_type)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/volume_snapshots.py0000664000175000017500000002516200000000000024513 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Volume snapshot interface (v3 extension).""" from oslo_utils import strutils from cinderclient import api_versions from cinderclient.apiclient import base as common_base from cinderclient import base MV_3_66_FORCE_FLAG_ERROR = ( "Since microversion 3.66 of the Block Storage API, the 'force' option is " "invalid for this request. For backward compatibility, however, when the " "'force' flag is passed with a value evaluating to True, it is silently " "ignored.") class Snapshot(base.Resource): """A Snapshot is a point-in-time snapshot of an openstack volume.""" def __repr__(self): return "" % self.id def delete(self, force=False): """Delete this snapshot.""" return self.manager.delete(self, force) def update(self, **kwargs): """Update the name or description for this snapshot.""" return self.manager.update(self, **kwargs) @property def progress(self): return self._info.get('os-extended-snapshot-attributes:progress') @property def project_id(self): return self._info.get('os-extended-snapshot-attributes:project_id') def reset_state(self, state): """Update the snapshot with the provided state.""" return self.manager.reset_state(self, state) def set_metadata(self, metadata): """Set metadata of this snapshot.""" return self.manager.set_metadata(self, metadata) def delete_metadata(self, keys): """Delete metadata of this snapshot.""" return self.manager.delete_metadata(self, keys) def update_all_metadata(self, metadata): """Update_all metadata of this snapshot.""" return self.manager.update_all_metadata(self, metadata) def manage(self, volume_id, ref, name=None, description=None, metadata=None): """Manage an existing snapshot.""" self.manager.manage(volume_id=volume_id, ref=ref, name=name, description=description, metadata=metadata) def list_manageable(self, host, detailed=True, marker=None, limit=None, offset=None, sort=None, cluster=None): return self.manager.list_manageable(host, detailed=detailed, marker=marker, limit=limit, offset=offset, sort=sort, cluster=cluster) def unmanage(self, snapshot): """Unmanage a snapshot.""" self.manager.unmanage(snapshot) class SnapshotManager(base.ManagerWithFind): """Manage :class:`Snapshot` resources.""" resource_class = Snapshot @api_versions.wraps("3.0", "3.65") def create(self, volume_id, force=False, name=None, description=None, metadata=None): """Creates a snapshot of the given volume. :param volume_id: The ID of the volume to snapshot. :param force: If force is True, create a snapshot even if the volume is attached to an instance. Default is False. :param name: Name of the snapshot :param description: Description of the snapshot :param metadata: Metadata of the snapshot :rtype: :class:`Snapshot` """ if metadata is None: snapshot_metadata = {} else: snapshot_metadata = metadata body = {'snapshot': {'volume_id': volume_id, 'force': force, 'name': name, 'description': description, 'metadata': snapshot_metadata}} return self._create('/snapshots', body, 'snapshot') @api_versions.wraps("3.66") def create(self, volume_id, force=None, # noqa: F811 name=None, description=None, metadata=None): """Creates a snapshot of the given volume. :param volume_id: The ID of the volume to snapshot. :param force: This is technically not valid after mv 3.66, but the API silently accepts force=True for backward compatibility, so this function will, too :param name: Name of the snapshot :param description: Description of the snapshot :param metadata: Metadata of the snapshot :raises: ValueError if 'force' is not passed with a value that evaluates to true :rtype: :class:`Snapshot` """ if metadata is None: snapshot_metadata = {} else: snapshot_metadata = metadata body = {'snapshot': {'volume_id': volume_id, 'name': name, 'description': description, 'metadata': snapshot_metadata}} if force is not None: try: force = strutils.bool_from_string(force, strict=True) if not force: raise ValueError() except ValueError: raise ValueError(MV_3_66_FORCE_FLAG_ERROR) return self._create('/snapshots', body, 'snapshot') def get(self, snapshot_id): """Shows snapshot details. :param snapshot_id: The ID of the snapshot to get. :rtype: :class:`Snapshot` """ return self._get("/snapshots/%s" % snapshot_id, "snapshot") def list(self, detailed=True, search_opts=None, marker=None, limit=None, sort=None): """Get a list of all snapshots. :rtype: list of :class:`Snapshot` """ resource_type = "snapshots" url = self._build_list_url(resource_type, detailed=detailed, search_opts=search_opts, marker=marker, limit=limit, sort=sort) return self._list(url, resource_type, limit=limit) def delete(self, snapshot, force=False): """Delete a snapshot. :param snapshot: The :class:`Snapshot` to delete. :param force: Allow delete in state other than error or available. """ if force: return self._action('os-force_delete', snapshot) else: return self._delete("/snapshots/%s" % base.getid(snapshot)) def update(self, snapshot, **kwargs): """Update the name or description for a snapshot. :param snapshot: The :class:`Snapshot` to update. """ if not kwargs: return body = {"snapshot": kwargs} return self._update("/snapshots/%s" % base.getid(snapshot), body) def reset_state(self, snapshot, state): """Update the specified snapshot with the provided state.""" return self._action('os-reset_status', snapshot, {'status': state} if state else {}) def _action(self, action, snapshot, info=None, **kwargs): """Perform a snapshot action.""" body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/snapshots/%s/action' % base.getid(snapshot) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) def update_snapshot_status(self, snapshot, update_dict): return self._action('os-update_snapshot_status', base.getid(snapshot), update_dict) def set_metadata(self, snapshot, metadata): """Update/Set a snapshots metadata. :param snapshot: The :class:`Snapshot`. :param metadata: A list of keys to be set. """ body = {'metadata': metadata} return self._create("/snapshots/%s/metadata" % base.getid(snapshot), body, "metadata") def delete_metadata(self, snapshot, keys): """Delete specified keys from snapshot metadata. :param snapshot: The :class:`Snapshot`. :param keys: A list of keys to be removed. """ response_list = [] snapshot_id = base.getid(snapshot) for k in keys: resp, body = self._delete("/snapshots/%s/metadata/%s" % (snapshot_id, k)) response_list.append(resp) return common_base.ListWithMeta([], response_list) def update_all_metadata(self, snapshot, metadata): """Update_all snapshot metadata. :param snapshot: The :class:`Snapshot`. :param metadata: A list of keys to be updated. """ body = {'metadata': metadata} return self._update("/snapshots/%s/metadata" % base.getid(snapshot), body) def manage(self, volume_id, ref, name=None, description=None, metadata=None): """Manage an existing snapshot.""" body = {'snapshot': {'volume_id': volume_id, 'ref': ref, 'name': name, 'description': description, 'metadata': metadata } } return self._create('/os-snapshot-manage', body, 'snapshot') @api_versions.wraps("3.0") def list_manageable(self, host, detailed=True, marker=None, limit=None, offset=None, sort=None): url = self._build_list_url("os-snapshot-manage", detailed=detailed, search_opts={'host': host}, marker=marker, limit=limit, offset=offset, sort=sort) return self._list(url, "manageable-snapshots") @api_versions.wraps('3.8') def list_manageable(self, host, detailed=True, marker=None, # noqa: F811 limit=None, offset=None, sort=None, cluster=None): search_opts = {'cluster': cluster} if cluster else {'host': host} url = self._build_list_url("manageable_snapshots", detailed=detailed, search_opts=search_opts, marker=marker, limit=limit, offset=offset, sort=sort) return self._list(url, "manageable-snapshots") def unmanage(self, snapshot): """Unmanage a snapshot.""" return self._action('os-unmanage', snapshot, None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/volume_transfers.py0000664000175000017500000000744500000000000024504 0ustar00zuulzuul00000000000000# Copyright (C) 2013 Hewlett-Packard Development Company, L.P. # 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. """Volume transfer interface (v3 extension).""" from cinderclient import base class VolumeTransfer(base.Resource): """Transfer a volume from one tenant to another""" def __repr__(self): return "" % self.id def delete(self): """Delete this volume transfer.""" return self.manager.delete(self) class VolumeTransferManager(base.ManagerWithFind): """Manage :class:`VolumeTransfer` resources.""" resource_class = VolumeTransfer def create(self, volume_id, name=None, no_snapshots=False): """Creates a volume transfer. :param volume_id: The ID of the volume to transfer. :param name: The name of the transfer. :param no_snapshots: Transfer volumes without snapshots. :rtype: :class:`VolumeTransfer` """ body = {'transfer': {'volume_id': volume_id, 'name': name}} if self.api_version.matches('3.55'): body['transfer']['no_snapshots'] = no_snapshots return self._create('/volume-transfers', body, 'transfer') return self._create('/os-volume-transfer', body, 'transfer') def accept(self, transfer_id, auth_key): """Accept a volume transfer. :param transfer_id: The ID of the transfer to accept. :param auth_key: The auth_key of the transfer. :rtype: :class:`VolumeTransfer` """ body = {'accept': {'auth_key': auth_key}} if self.api_version.matches('3.55'): return self._create('/volume-transfers/%s/accept' % transfer_id, body, 'transfer') return self._create('/os-volume-transfer/%s/accept' % transfer_id, body, 'transfer') def get(self, transfer_id): """Show details of a volume transfer. :param transfer_id: The ID of the volume transfer to display. :rtype: :class:`VolumeTransfer` """ if self.api_version.matches('3.55'): return self._get("/volume-transfers/%s" % transfer_id, "transfer") return self._get("/os-volume-transfer/%s" % transfer_id, "transfer") def list(self, detailed=True, search_opts=None, sort=None): """Get a list of all volume transfer. :param detailed: Get detailed object information. :param search_opts: Filtering options. :param sort: Sort information :rtype: list of :class:`VolumeTransfer` """ resource_type = 'os-volume-transfer' if self.api_version.matches('3.55'): resource_type = 'volume-transfers' url = self._build_list_url(resource_type, detailed=detailed, search_opts=search_opts, sort=sort) return self._list(url, 'transfers') def delete(self, transfer_id): """Delete a volume transfer. :param transfer_id: The :class:`VolumeTransfer` to delete. """ if self.api_version.matches('3.55'): return self._delete( "/volume-transfers/%s" % base.getid(transfer_id)) return self._delete("/os-volume-transfer/%s" % base.getid(transfer_id)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/volume_type_access.py0000664000175000017500000000375000000000000024772 0ustar00zuulzuul00000000000000# Copyright 2014 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Volume type access interface.""" from cinderclient.apiclient import base as common_base from cinderclient import base class VolumeTypeAccess(base.Resource): def __repr__(self): return "" % self.project_id class VolumeTypeAccessManager(base.ManagerWithFind): """ Manage :class:`VolumeTypeAccess` resources. """ resource_class = VolumeTypeAccess def list(self, volume_type): return self._list( '/types/%s/os-volume-type-access' % base.getid(volume_type), 'volume_type_access') def add_project_access(self, volume_type, project): """Add a project to the given volume type access list.""" info = {'project': project} return self._action('addProjectAccess', volume_type, info) def remove_project_access(self, volume_type, project): """Remove a project from the given volume type access list.""" info = {'project': project} return self._action('removeProjectAccess', volume_type, info) def _action(self, action, volume_type, info, **kwargs): """Perform a volume type action.""" body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/types/%s/action' % base.getid(volume_type) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/volume_types.py0000664000175000017500000001315300000000000023632 0ustar00zuulzuul00000000000000# Copyright (c) 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. """Volume Type interface.""" from urllib import parse from cinderclient.apiclient import base as common_base from cinderclient import base class VolumeType(base.Resource): """A Volume Type is the type of volume to be created.""" def __repr__(self): return "" % self.name @property def is_public(self): """ Provide a user-friendly accessor to os-volume-type-access:is_public """ return self._info.get("os-volume-type-access:is_public", self._info.get("is_public", 'N/A')) def get_keys(self): """Get extra specs from a volume type. :param vol_type: The :class:`VolumeType` to get extra specs from """ _resp, body = self.manager.api.client.get( "/types/%s/extra_specs" % base.getid(self)) return body["extra_specs"] def set_keys(self, metadata): """Set extra specs on a volume type. :param type : The :class:`VolumeType` to set extra spec on :param metadata: A dict of key/value pairs to be set """ body = {'extra_specs': metadata} return self.manager._create( "/types/%s/extra_specs" % base.getid(self), body, "extra_specs", return_raw=True) def unset_keys(self, keys): """Unset extra specs on a volue type. :param type_id: The :class:`VolumeType` to unset extra spec on :param keys: A list of keys to be unset """ # NOTE(jdg): This wasn't actually doing all of the keys before # the return in the loop resulted in only ONE key being unset, # since on success the return was ListWithMeta class, we'll only # interrupt the loop and if an exception is raised. response_list = [] for k in keys: resp, body = self.manager._delete( "/types/%s/extra_specs/%s" % ( base.getid(self), k)) response_list.append(resp) return common_base.ListWithMeta([], response_list) class VolumeTypeManager(base.ManagerWithFind): """Manage :class:`VolumeType` resources.""" resource_class = VolumeType def list(self, search_opts=None, is_public=None): """Lists all volume types. :param search_opts: Optional search filters. :param is_public: Whether to only get public types. :return: List of :class:`VolumeType`. """ if not search_opts: search_opts = dict() # Remove 'all_tenants' option added by ManagerWithFind.findall(), # as it is not a valid search option for volume_types. search_opts.pop('all_tenants', None) # Need to keep backwards compatibility with is_public usage. If it # isn't included then cinder will assume you want is_public=True, which # negatively affects the results. if 'is_public' not in search_opts: search_opts['is_public'] = is_public query_string = "?%s" % parse.urlencode(search_opts) return self._list("/types%s" % query_string, "volume_types") def get(self, volume_type): """Get a specific volume type. :param volume_type: The ID of the :class:`VolumeType` to get. :rtype: :class:`VolumeType` """ return self._get("/types/%s" % base.getid(volume_type), "volume_type") def default(self): """Get the default volume type. :rtype: :class:`VolumeType` """ return self._get("/types/default", "volume_type") def delete(self, volume_type): """Deletes a specific volume_type. :param volume_type: The name or ID of the :class:`VolumeType` to get. """ return self._delete("/types/%s" % base.getid(volume_type)) def create(self, name, description=None, is_public=True): """Creates a volume type. :param name: Descriptive name of the volume type :param description: Description of the volume type :param is_public: Volume type visibility :rtype: :class:`VolumeType` """ body = { "volume_type": { "name": name, "description": description, "os-volume-type-access:is_public": is_public, } } return self._create("/types", body, "volume_type") def update(self, volume_type, name=None, description=None, is_public=None): """Update the name and/or description for a volume type. :param volume_type: The ID of the :class:`VolumeType` to update. :param name: Descriptive name of the volume type. :param description: Description of the volume type. :rtype: :class:`VolumeType` """ body = { "volume_type": { "name": name, "description": description } } if is_public is not None: body["volume_type"]["is_public"] = is_public return self._update("/types/%s" % base.getid(volume_type), body, response_key="volume_type") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/volumes.py0000664000175000017500000003171600000000000022576 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Volume interface (v3 extension).""" from cinderclient import api_versions from cinderclient.apiclient import base as common_base from cinderclient import base from cinderclient.v3 import volumes_base class Volume(volumes_base.Volume): def upload_to_image(self, force, image_name, container_format, disk_format, visibility=None, protected=None): """Upload a volume to image service as an image. :param force: Boolean to enables or disables upload of a volume that is attached to an instance. :param image_name: The new image name. :param container_format: Container format type. :param disk_format: Disk format type. :param visibility: The accessibility of image (allowed for 3.1-latest). :param protected: Boolean to decide whether prevents image from being deleted (allowed for 3.1-latest). :returns: tuple (response, body) """ if self.manager.api_version >= api_versions.APIVersion("3.1"): visibility = 'private' if visibility is None else visibility protected = False if protected is None else protected return self.manager.upload_to_image(self, force, image_name, container_format, disk_format, visibility, protected) return self.manager.upload_to_image(self, force, image_name, container_format, disk_format) def revert_to_snapshot(self, snapshot): """Revert a volume to a snapshot.""" self.manager.revert_to_snapshot(self, snapshot) def migrate_volume(self, host, force_host_copy, lock_volume, cluster=None): """Migrate the volume to a new host.""" return self.manager.migrate_volume(self, host, force_host_copy, lock_volume, cluster) def manage(self, host, ref, name=None, description=None, volume_type=None, availability_zone=None, metadata=None, bootable=False, cluster=None): """Manage an existing volume.""" return self.manager.manage(host=host, ref=ref, name=name, description=description, volume_type=volume_type, availability_zone=availability_zone, metadata=metadata, bootable=bootable, cluster=cluster) def reimage(self, image_id, reimage_reserved=False): """Rebuilds the volume with the new specified image""" self.manager.reimage(self, image_id, reimage_reserved) class VolumeManager(volumes_base.VolumeManager): resource_class = Volume def create(self, size, consistencygroup_id=None, group_id=None, snapshot_id=None, source_volid=None, name=None, description=None, volume_type=None, user_id=None, project_id=None, availability_zone=None, metadata=None, imageRef=None, scheduler_hints=None, backup_id=None): """Create a volume. :param size: Size of volume in GB :param consistencygroup_id: ID of the consistencygroup :param group_id: ID of the group :param snapshot_id: ID of the snapshot :param name: Name of the volume :param description: Description of the volume :param volume_type: Type of volume :param user_id: User id derived from context (IGNORED) :param project_id: Project id derived from context (IGNORED) :param availability_zone: Availability Zone to use :param metadata: Optional metadata to set on volume creation :param imageRef: reference to an image stored in glance :param source_volid: ID of source volume to clone from :param scheduler_hints: (optional extension) arbitrary key-value pairs specified by the client to help boot an instance :param backup_id: ID of the backup :rtype: :class:`Volume` """ if metadata is None: volume_metadata = {} else: volume_metadata = metadata body = {'volume': {'size': size, 'consistencygroup_id': consistencygroup_id, 'snapshot_id': snapshot_id, 'name': name, 'description': description, 'volume_type': volume_type, 'availability_zone': availability_zone, 'metadata': volume_metadata, 'imageRef': imageRef, 'source_volid': source_volid, 'backup_id': backup_id }} if group_id: body['volume']['group_id'] = group_id if scheduler_hints: body['OS-SCH-HNT:scheduler_hints'] = scheduler_hints return self._create('/volumes', body, 'volume') @api_versions.wraps('3.40') def revert_to_snapshot(self, volume, snapshot): """Revert a volume to a snapshot. The snapshot must be the most recent one known to cinder. :param volume: volume object or volume id. :param snapshot: snapshot object or snapshot id. """ return self._action('revert', volume, info={'snapshot_id': base.getid(snapshot)}) @api_versions.wraps('3.12') def summary(self, all_tenants): """Get volumes summary.""" url = "/volumes/summary" if all_tenants: url += "?all_tenants=True" _, body = self.api.client.get(url) return body @api_versions.wraps("3.0") def delete_metadata(self, volume, keys): """Delete specified keys from volumes metadata. :param volume: The :class:`Volume`. :param keys: A list of keys to be removed. """ response_list = [] for k in keys: resp, body = self._delete("/volumes/%s/metadata/%s" % (base.getid(volume), k)) response_list.append(resp) return common_base.ListWithMeta([], response_list) @api_versions.wraps("3.15") def delete_metadata(self, volume, keys): # noqa: F811 """Delete specified keys from volumes metadata. :param volume: The :class:`Volume`. :param keys: A list of keys to be removed. """ # pylint: disable=function-redefined data = self._get("/volumes/%s/metadata" % base.getid(volume)) metadata = data._info.get("metadata", {}) if set(keys).issubset(metadata.keys()): for k in keys: metadata.pop(k) body = {'metadata': metadata} kwargs = {'headers': {'If-Match': data._checksum}} return self._update("/volumes/%s/metadata" % base.getid(volume), body, **kwargs) @api_versions.wraps("3.0") def upload_to_image(self, volume, force, image_name, container_format, disk_format): """Upload volume to image service as image. :param volume: The :class:`Volume` to upload. """ return self._action('os-volume_upload_image', volume, {'force': force, 'image_name': image_name, 'container_format': container_format, 'disk_format': disk_format}) @api_versions.wraps("3.1") def upload_to_image(self, volume, force, image_name, # noqa: F811 container_format, disk_format, visibility, protected): """Upload volume to image service as image. :param volume: The :class:`Volume` to upload. """ # pylint: disable=function-redefined return self._action('os-volume_upload_image', volume, {'force': force, 'image_name': image_name, 'container_format': container_format, 'disk_format': disk_format, 'visibility': visibility, 'protected': protected}) def migrate_volume(self, volume, host, force_host_copy, lock_volume, cluster=None): """Migrate volume to new backend. The new backend is defined by the host or the cluster (not both). :param volume: The :class:`Volume` to migrate :param host: The destination host :param force_host_copy: Skip driver optimizations :param lock_volume: Lock the volume and guarantee the migration to finish :param cluster: The cluster """ body = {'host': host, 'force_host_copy': force_host_copy, 'lock_volume': lock_volume} if self.api_version.matches('3.16'): if cluster: body['cluster'] = cluster del body['host'] return self._action('os-migrate_volume', volume, body) def manage(self, host, ref, name=None, description=None, volume_type=None, availability_zone=None, metadata=None, bootable=False, cluster=None): """Manage an existing volume.""" body = {'volume': {'host': host, 'ref': ref, 'name': name, 'description': description, 'volume_type': volume_type, 'availability_zone': availability_zone, 'metadata': metadata, 'bootable': bootable }} if self.api_version.matches('3.16') and cluster: body['volume']['cluster'] = cluster return self._create('/os-volume-manage', body, 'volume') @api_versions.wraps('3.0') def list_manageable(self, host, detailed=True, marker=None, limit=None, offset=None, sort=None): url = self._build_list_url("os-volume-manage", detailed=detailed, search_opts={'host': host}, marker=marker, limit=limit, offset=offset, sort=sort) return self._list(url, "manageable-volumes") @api_versions.wraps('3.8') def list_manageable(self, host, detailed=True, marker=None, # noqa: F811 limit=None, offset=None, sort=None, cluster=None): search_opts = {'cluster': cluster} if cluster else {'host': host} url = self._build_list_url("manageable_volumes", detailed=detailed, search_opts=search_opts, marker=marker, limit=limit, offset=offset, sort=sort) return self._list(url, "manageable-volumes") @api_versions.wraps("3.0", "3.32") def get_pools(self, detail): """Show pool information for backends.""" query_string = "" if detail: query_string = "?detail=True" return self._get('/scheduler-stats/get_pools%s' % query_string, None) @api_versions.wraps("3.33") def get_pools(self, detail, search_opts): # noqa: F811 """Show pool information for backends.""" # pylint: disable=function-redefined options = {'detail': detail} options.update(search_opts) url = self._build_list_url('scheduler-stats/get_pools', detailed=False, search_opts=options) return self._get(url, None) @api_versions.wraps('3.68') def reimage(self, volume, image_id, reimage_reserved=False): """Reimage a volume .. warning:: This is a destructive action and the contents of the volume will be lost. :param volume: Volume to reimage. :param reimage_reserved: Boolean to enable or disable reimage of a volume that is in 'reserved' state otherwise only volumes in 'available' status may be re-imaged. :param image_id: The image id. """ return self._action('os-reimage', volume, {'image_id': image_id, 'reimage_reserved': reimage_reserved}) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/volumes_base.py0000664000175000017500000004725200000000000023572 0ustar00zuulzuul00000000000000# Copyright (c) 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Base Volume interface.""" from cinderclient.apiclient import base as common_base from cinderclient import base class Volume(base.Resource): """A volume is an extra block level storage to the OpenStack instances.""" def __repr__(self): return "" % self.id def delete(self, cascade=False): """Delete this volume.""" return self.manager.delete(self, cascade=cascade) def update(self, **kwargs): """Update the name or description for this volume.""" return self.manager.update(self, **kwargs) def attach(self, instance_uuid, mountpoint, mode='rw', host_name=None): """Inform Cinder that the given volume is attached to the given instance. Calling this method will not actually ask Cinder to attach a volume, but to mark it on the DB as attached. If the volume is not actually attached to the given instance, inconsistent data will result. The right flow of calls is : 1- call reserve 2- call initialize_connection 3- call attach :param instance_uuid: uuid of the attaching instance. :param mountpoint: mountpoint on the attaching instance or host. :param mode: the access mode. :param host_name: name of the attaching host. """ return self.manager.attach(self, instance_uuid, mountpoint, mode, host_name) def detach(self): """Inform Cinder that the given volume is detached from the given instance. Calling this method will not actually ask Cinder to detach a volume, but to mark it on the DB as detached. If the volume is not actually detached from the given instance, inconsistent data will result. The right flow of calls is : 1- call reserve 2- call initialize_connection 3- call detach """ return self.manager.detach(self) def reserve(self, volume): """Reserve this volume.""" return self.manager.reserve(self) def unreserve(self, volume): """Unreserve this volume.""" return self.manager.unreserve(self) def begin_detaching(self, volume): """Begin detaching volume.""" return self.manager.begin_detaching(self) def roll_detaching(self, volume): """Roll detaching volume.""" return self.manager.roll_detaching(self) def initialize_connection(self, volume, connector): """Initialize a volume connection. :param connector: connector dict from nova. """ return self.manager.initialize_connection(self, connector) def terminate_connection(self, volume, connector): """Terminate a volume connection. :param connector: connector dict from nova. """ return self.manager.terminate_connection(self, connector) def set_metadata(self, volume, metadata): """Set or Append metadata to a volume. :param volume : The :class: `Volume` to set metadata on :param metadata: A dict of key/value pairs to set """ return self.manager.set_metadata(self, metadata) def set_image_metadata(self, volume, metadata): """Set a volume's image metadata. :param volume : The :class: `Volume` to set metadata on :param metadata: A dict of key/value pairs to set """ return self.manager.set_image_metadata(self, volume, metadata) def delete_image_metadata(self, volume, keys): """Delete specified keys from volume's image metadata. :param volume: The :class:`Volume`. :param keys: A list of keys to be removed. """ return self.manager.delete_image_metadata(self, volume, keys) def show_image_metadata(self, volume): """Show a volume's image metadata. :param volume : The :class: `Volume` where the image metadata associated. """ return self.manager.show_image_metadata(self) def force_delete(self): """Delete the specified volume ignoring its current state. :param volume: The UUID of the volume to force-delete. """ return self.manager.force_delete(self) def reset_state(self, state, attach_status=None, migration_status=None): """Update the volume with the provided state. :param state: The state of the volume to set. :param attach_status: The attach_status of the volume to be set, or None to keep the current status. :param migration_status: The migration_status of the volume to be set, or None to keep the current status. """ return self.manager.reset_state(self, state, attach_status, migration_status) def extend(self, volume, new_size): """Extend the size of the specified volume. :param volume: The UUID of the volume to extend :param new_size: The desired size to extend volume to. """ return self.manager.extend(self, new_size) def retype(self, volume_type, policy): """Change a volume's type.""" return self.manager.retype(self, volume_type, policy) def update_all_metadata(self, metadata): """Update all metadata of this volume.""" return self.manager.update_all_metadata(self, metadata) def update_readonly_flag(self, volume, read_only): """Update the read-only access mode flag of the specified volume. :param volume: The UUID of the volume to update. :param read_only: The value to indicate whether to update volume to read-only access mode. """ return self.manager.update_readonly_flag(self, read_only) def list_manageable(self, host, detailed=True, marker=None, limit=None, offset=None, sort=None): return self.manager.list_manageable(host, detailed=detailed, marker=marker, limit=limit, offset=offset, sort=sort) def unmanage(self, volume): """Unmanage a volume.""" return self.manager.unmanage(volume) def get_pools(self, detail): """Show pool information for backends.""" return self.manager.get_pools(detail) class VolumeManager(base.ManagerWithFind): """Manage :class:`Volume` resources.""" resource_class = Volume def get(self, volume_id): """Get a volume. :param volume_id: The ID of the volume to get. :rtype: :class:`Volume` """ return self._get("/volumes/%s" % volume_id, "volume") def list(self, detailed=True, search_opts=None, marker=None, limit=None, sort=None): """Lists all volumes. :param detailed: Whether to return detailed volume info. :param search_opts: Search options to filter out volumes. :param marker: Begin returning volumes that appear later in the volume list than that represented by this volume id. :param limit: Maximum number of volumes to return. :param sort: Sort information :rtype: list of :class:`Volume` """ resource_type = "volumes" url = self._build_list_url(resource_type, detailed=detailed, search_opts=search_opts, marker=marker, limit=limit, sort=sort) return self._list(url, resource_type, limit=limit) def delete(self, volume, cascade=False): """Delete a volume. :param volume: The :class:`Volume` to delete. :param cascade: Also delete dependent snapshots. """ loc = "/volumes/%s" % base.getid(volume) if cascade: loc += '?cascade=True' return self._delete(loc) def update(self, volume, **kwargs): """Update the name or description for a volume. :param volume: The :class:`Volume` to update. """ if not kwargs: return body = {"volume": kwargs} return self._update("/volumes/%s" % base.getid(volume), body) def _action(self, action, volume, info=None, **kwargs): """Perform a volume "action." :returns: tuple (response, body) """ body = {action: info} self.run_hooks('modify_body_for_action', body, **kwargs) url = '/volumes/%s/action' % base.getid(volume) resp, body = self.api.client.post(url, body=body) return common_base.TupleWithMeta((resp, body), resp) def attach(self, volume, instance_uuid, mountpoint, mode='rw', host_name=None): """Set attachment metadata. :param volume: The :class:`Volume` (or its ID) you would like to attach. :param instance_uuid: uuid of the attaching instance. :param mountpoint: mountpoint on the attaching instance or host. :param mode: the access mode. :param host_name: name of the attaching host. """ body = {'mountpoint': mountpoint, 'mode': mode} if instance_uuid is not None: body.update({'instance_uuid': instance_uuid}) if host_name is not None: body.update({'host_name': host_name}) return self._action('os-attach', volume, body) def detach(self, volume, attachment_uuid=None): """Clear attachment metadata. :param volume: The :class:`Volume` (or its ID) you would like to detach. :param attachment_uuid: The uuid of the volume attachment. """ return self._action('os-detach', volume, {'attachment_id': attachment_uuid}) def reserve(self, volume): """Reserve this volume. :param volume: The :class:`Volume` (or its ID) you would like to reserve. """ return self._action('os-reserve', volume) def unreserve(self, volume): """Unreserve this volume. :param volume: The :class:`Volume` (or its ID) you would like to unreserve. """ return self._action('os-unreserve', volume) def begin_detaching(self, volume): """Begin detaching this volume. :param volume: The :class:`Volume` (or its ID) you would like to detach. """ return self._action('os-begin_detaching', volume) def roll_detaching(self, volume): """Roll detaching this volume. :param volume: The :class:`Volume` (or its ID) you would like to roll detaching. """ return self._action('os-roll_detaching', volume) def initialize_connection(self, volume, connector): """Initialize a volume connection. :param volume: The :class:`Volume` (or its ID). :param connector: connector dict from nova. """ resp, body = self._action('os-initialize_connection', volume, {'connector': connector}) return common_base.DictWithMeta(body['connection_info'], resp) def terminate_connection(self, volume, connector): """Terminate a volume connection. :param volume: The :class:`Volume` (or its ID). :param connector: connector dict from nova. """ return self._action('os-terminate_connection', volume, {'connector': connector}) def set_metadata(self, volume, metadata): """Update/Set a volumes metadata. :param volume: The :class:`Volume`. :param metadata: A list of keys to be set. """ body = {'metadata': metadata} return self._create("/volumes/%s/metadata" % base.getid(volume), body, "metadata") def delete_metadata(self, volume, keys): """Delete specified keys from volumes metadata. :param volume: The :class:`Volume`. :param keys: A list of keys to be removed. """ response_list = [] for k in keys: resp, body = self._delete("/volumes/%s/metadata/%s" % (base.getid(volume), k)) response_list.append(resp) return common_base.ListWithMeta([], response_list) def set_image_metadata(self, volume, metadata): """Set a volume's image metadata. :param volume: The :class:`Volume`. :param metadata: keys and the values to be set with. :type metadata: dict """ return self._action("os-set_image_metadata", volume, {'metadata': metadata}) def delete_image_metadata(self, volume, keys): """Delete specified keys from volume's image metadata. :param volume: The :class:`Volume`. :param keys: A list of keys to be removed. """ response_list = [] for key in keys: resp, body = self._action("os-unset_image_metadata", volume, {'key': key}) response_list.append(resp) return common_base.ListWithMeta([], response_list) def show_image_metadata(self, volume): """Show a volume's image metadata. :param volume : The :class: `Volume` where the image metadata associated. """ return self._action("os-show_image_metadata", volume) def upload_to_image(self, volume, force, image_name, container_format, disk_format): """Upload volume to image service as image. :param volume: The :class:`Volume` to upload. """ return self._action('os-volume_upload_image', volume, {'force': force, 'image_name': image_name, 'container_format': container_format, 'disk_format': disk_format}) def force_delete(self, volume): """Delete the specified volume ignoring its current state. :param volume: The :class:`Volume` to force-delete. """ return self._action('os-force_delete', base.getid(volume)) def reset_state(self, volume, state, attach_status=None, migration_status=None): """Update the provided volume with the provided state. :param volume: The :class:`Volume` to set the state. :param state: The state of the volume to be set. :param attach_status: The attach_status of the volume to be set, or None to keep the current status. :param migration_status: The migration_status of the volume to be set, or None to keep the current status. """ body = {'status': state} if state else {} if attach_status: body.update({'attach_status': attach_status}) if migration_status: body.update({'migration_status': migration_status}) return self._action('os-reset_status', volume, body) def extend(self, volume, new_size): """Extend the size of the specified volume. :param volume: The UUID of the volume to extend. :param new_size: The requested size to extend volume to. """ return self._action('os-extend', base.getid(volume), {'new_size': new_size}) def get_encryption_metadata(self, volume_id): """ Retrieve the encryption metadata from the desired volume. :param volume_id: the id of the volume to query :return: a dictionary of volume encryption metadata """ metadata = self._get("/volumes/%s/encryption" % volume_id) return common_base.DictWithMeta(metadata._info, metadata.request_ids) def migrate_volume(self, volume, host, force_host_copy, lock_volume): """Migrate volume to new host. :param volume: The :class:`Volume` to migrate :param host: The destination host :param force_host_copy: Skip driver optimizations :param lock_volume: Lock the volume and guarantee the migration to finish """ return self._action('os-migrate_volume', volume, {'host': host, 'force_host_copy': force_host_copy, 'lock_volume': lock_volume}) def migrate_volume_completion(self, old_volume, new_volume, error): """Complete the migration from the old volume to the temp new one. :param old_volume: The original :class:`Volume` in the migration :param new_volume: The new temporary :class:`Volume` in the migration :param error: Inform of an error to cause migration cleanup """ new_volume_id = base.getid(new_volume) resp, body = self._action('os-migrate_volume_completion', old_volume, {'new_volume': new_volume_id, 'error': error}) return common_base.DictWithMeta(body, resp) def update_all_metadata(self, volume, metadata): """Update all metadata of a volume. :param volume: The :class:`Volume`. :param metadata: A list of keys to be updated. """ body = {'metadata': metadata} return self._update("/volumes/%s/metadata" % base.getid(volume), body) def update_readonly_flag(self, volume, flag): return self._action('os-update_readonly_flag', base.getid(volume), {'readonly': flag}) def retype(self, volume, volume_type, policy): """Change a volume's type. :param volume: The :class:`Volume` to retype :param volume_type: New volume type :param policy: Policy for migration during the retype """ return self._action('os-retype', volume, {'new_type': volume_type, 'migration_policy': policy}) def set_bootable(self, volume, flag): return self._action('os-set_bootable', base.getid(volume), {'bootable': flag}) def manage(self, host, ref, name=None, description=None, volume_type=None, availability_zone=None, metadata=None, bootable=False): """Manage an existing volume.""" body = {'volume': {'host': host, 'ref': ref, 'name': name, 'description': description, 'volume_type': volume_type, 'availability_zone': availability_zone, 'metadata': metadata, 'bootable': bootable }} return self._create('/os-volume-manage', body, 'volume') def unmanage(self, volume): """Unmanage a volume.""" return self._action('os-unmanage', volume, None) def get_pools(self, detail): """Show pool information for backends.""" query_string = "" if detail: query_string = "?detail=True" return self._get('/scheduler-stats/get_pools%s' % query_string, None) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/v3/workers.py0000664000175000017500000000307100000000000022571 0ustar00zuulzuul00000000000000# Copyright (c) 2016 Red Hat, Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Interface to workers API """ from cinderclient import api_versions from cinderclient.apiclient import base as common_base from cinderclient import base class Service(base.Resource): def __repr__(self): return "" % (self.id, self.host, self.cluster_name or '-') @classmethod def list_factory(cls, mngr, elements): return [cls(mngr, element, loaded=True) for element in elements] class WorkerManager(base.Manager): base_url = '/workers' @api_versions.wraps('3.24') def clean(self, **filters): url = self.base_url + '/cleanup' resp, body = self.api.client.post(url, body=filters) cleaning = Service.list_factory(self, body['cleaning']) unavailable = Service.list_factory(self, body['unavailable']) result = common_base.TupleWithMeta((cleaning, unavailable), resp) return result ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/cinderclient/version.py0000664000175000017500000000132600000000000022233 0ustar00zuulzuul00000000000000# 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 pbr.version version_info = pbr.version.VersionInfo('python-cinderclient') __version__ = version_info.version_string() ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1645793255.285896 python-cinderclient-8.3.0/doc/0000775000175000017500000000000000000000000016274 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/.gitignore0000664000175000017500000000000700000000000020261 0ustar00zuulzuul00000000000000build/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/Makefile0000664000175000017500000000616400000000000017743 0ustar00zuulzuul00000000000000# Makefile for Sphinx documentation # # You can set these variables from the command line. SPHINXOPTS = SPHINXBUILD = sphinx-build SPHINXSOURCE = source 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) $(SPHINXSOURCE) .PHONY: help clean html dirhtml pickle json htmlhelp qthelp latex changes linkcheck doctest help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" @echo " dirhtml to make HTML files named index.html in directories" @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 " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" @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." dirhtml: $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml @echo @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 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/python-cinderclient.qhcp" @echo "To view the help file:" @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/python-cinderclient.qhc" latex: $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex @echo @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." @echo "Run \`make all-pdf' or \`make all-ps' in that directory to" \ "run these through (pdf)latex." 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." ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1645793255.285896 python-cinderclient-8.3.0/doc/ext/0000775000175000017500000000000000000000000017074 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/ext/__init__.py0000664000175000017500000000000000000000000021173 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/ext/cli.py0000664000175000017500000001571700000000000020230 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Sphinx extension to generate CLI documentation.""" from docutils import nodes from docutils.parsers import rst from docutils.parsers.rst import directives from docutils import statemachine as sm from sphinx.util import logging from sphinx.util import nested_parse_with_titles from cinderclient import api_versions from cinderclient import shell LOG = logging.getLogger(__name__) class CLIDocsDirective(rst.Directive): """Directive to generate CLI details into docs output.""" def _get_usage_lines(self, usage, append_value=None): """Breaks usage output into separate lines.""" results = [] lines = usage.split('\n') indent = 0 if '[' in lines[0]: indent = lines[0].index('[') for line in lines: if line.strip(): results.append(line) if append_value: results.append(' {}{}'.format(' ' * indent, append_value)) return results def _format_description_lines(self, description): """Formats option description into formatted lines.""" desc = description.split('\n') return [line.strip() for line in desc if line.strip() != ''] def run(self): """Load and document the current config options.""" cindershell = shell.OpenStackCinderShell() parser = cindershell.get_base_parser() api_version = api_versions.APIVersion(api_versions.MAX_VERSION) LOG.info('Generating CLI docs %s', api_version) cindershell.get_subcommand_parser(api_version, False, []) result = sm.ViewList() source = '<{}>'.format(__name__) result.append('.. _cinder_command_usage:', source) result.append('', source) result.append('cinder usage', source) result.append('------------', source) result.append('', source) result.append('.. code-block:: console', source) result.append('', source) result.append('', source) usage = self._get_usage_lines( parser.format_usage(), ' ...') for line in usage: result.append(' {}'.format(line), source) result.append('', source) result.append('.. _cinder_command_options:', source) result.append('', source) result.append('Optional Arguments', source) result.append('~~~~~~~~~~~~~~~~~~', source) result.append('', source) # This accesses a private variable from argparse. That's a little # risky, but since this is just for the docs and not "production" code, # and since this variable hasn't changed in years, it's a calculated # risk to make this documentation generation easier. But if something # suddenly breaks, check here first. actions = sorted(parser._actions, key=lambda x: x.option_strings[0]) for action in actions: if action.help == '==SUPPRESS==': continue opts = ', '.join(action.option_strings) result.append('``{}``'.format(opts), source) result.append(' {}'.format(action.help), source) result.append('', source) result.append('', source) result.append('.. _cinder_commands:', source) result.append('', source) result.append('Commands', source) result.append('~~~~~~~~', source) result.append('', source) for cmd in cindershell.subcommands: if 'completion' in cmd: continue result.append('``{}``'.format(cmd), source) subcmd = cindershell.subcommands[cmd] description = self._format_description_lines(subcmd.description) result.append(' {}'.format(description[0]), source) result.append('', source) result.append('', source) result.append('.. _cinder_command_details:', source) result.append('', source) result.append('Command Details', source) result.append('---------------', source) result.append('', source) for cmd in cindershell.subcommands: if 'completion' in cmd: continue subcmd = cindershell.subcommands[cmd] result.append('.. _cinder{}:'.format(cmd), source) result.append('', source) result.append(subcmd.prog, source) result.append('~' * len(subcmd.prog), source) result.append('', source) result.append('.. code-block:: console', source) result.append('', source) usage = self._get_usage_lines(subcmd.format_usage()) for line in usage: result.append(' {}'.format(line), source) result.append('', source) description = self._format_description_lines(subcmd.description) result.append(description[0], source) result.append('', source) if len(subcmd._actions) == 0: continue positional = [] optional = [] for action in subcmd._actions: if len(action.option_strings): if (action.option_strings[0] != '-h' and action.help != '==SUPPRESS=='): optional.append(action) else: positional.append(action) if positional: result.append('**Positional arguments:**', source) result.append('', source) for action in positional: result.append('``{}``'.format(action.metavar), source) result.append(' {}'.format(action.help), source) result.append('', source) if optional: result.append('**Optional arguments:**', source) result.append('', source) for action in optional: result.append('``{} {}``'.format( ', '.join(action.option_strings), action.metavar), source) result.append(' {}'.format(action.help), source) result.append('', source) node = nodes.section() node.document = self.state.document nested_parse_with_titles(self.state, result, node) return node.children def setup(app): app.add_directive('cli-docs', CLIDocsDirective) return { 'version': '1.0', 'parallel_read_safe': True, 'parallel_write_safe': True, } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/requirements.txt0000664000175000017500000000052500000000000021562 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. # These are needed for docs generation openstackdocstheme>=2.2.1 # Apache-2.0 reno>=3.2.0 # Apache-2.0 sphinx>=2.0.0,!=2.1.0 # BSD ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1645793255.285896 python-cinderclient-8.3.0/doc/source/0000775000175000017500000000000000000000000017574 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1645793255.285896 python-cinderclient-8.3.0/doc/source/cli/0000775000175000017500000000000000000000000020343 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/source/cli/details.rst0000664000175000017500000000061300000000000022522 0ustar00zuulzuul00000000000000================================================== Block Storage service (cinder) command-line client ================================================== The cinder client is the command-line interface (CLI) for the Block Storage service (cinder) API and its extensions. For help on a specific :command:`cinder` command, enter: .. code-block:: console $ cinder help COMMAND .. cli-docs:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/source/cli/index.rst0000664000175000017500000000337600000000000022215 0ustar00zuulzuul00000000000000============================== :program:`cinder` CLI man page ============================== .. program:: cinder .. highlight:: bash SYNOPSIS ======== :program:`cinder` [options] [command-options] :program:`cinder help` :program:`cinder help` DESCRIPTION =========== The :program:`cinder` command line utility interacts with OpenStack Block Storage Service (Cinder). In order to use the CLI, you must provide your OpenStack username, password, project (historically called tenant), and auth endpoint. You can use configuration options `--os-username`, `--os-password`, `--os-project-name` or `--os-project-id`, and `--os-auth-url` or set corresponding environment variables:: export OS_USERNAME=user export OS_PASSWORD=pass export OS_PROJECT_NAME=myproject export OS_AUTH_URL=http://auth.example.com:5000/v3 You can select an API version to use by `--os-volume-api-version` option or by setting corresponding environment variable:: export OS_VOLUME_API_VERSION=3 OPTIONS ======= To get a list of available commands and options run:: cinder help To get usage and options of a command:: cinder help You can see more details about the Cinder Command-Line Client at :doc:`details`. EXAMPLES ======== Get information about volume create command:: cinder help create List all the volumes:: cinder list Create new volume:: cinder create 1 --name volume01 Describe a specific volume:: cinder show 65d23a41-b13f-4345-ab65-918a4b8a6fe6 Create a snapshot:: cinder snapshot-create 65d23a41-b13f-4345-ab65-918a4b8a6fe6 \ --name qt-snap BUGS ==== Cinder client is hosted in Launchpad so you can view current bugs at https://bugs.launchpad.net/python-cinderclient/. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/source/conf.py0000664000175000017500000001046000000000000021074 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # python-cinderclient documentation build configuration file import os import sys sys.setrecursionlimit(4000) # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.append(os.path.abspath('.')) sys.path.insert(0, os.path.join(os.path.abspath('..'), 'ext')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', 'openstackdocstheme', 'reno.sphinxext', 'cli', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. copyright = 'OpenStack Contributors' # done by the openstackdocstheme ext # project = 'python-cinderclient' # version = version_info.version_string() # release = version_info.release_string() # List of directories, relative to source directory, that shouldn't be searched # for source files. exclude_trees = [] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. # html_theme = 'nature' html_theme = 'openstackdocs' # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # html_last_updated_fmt = '%Y-%m-%d %H:%M' # -- Options for manual page output ------------------------------------------ man_pages = [ ('cli/details', 'cinder', 'Client for OpenStack Block Storage API', ['OpenStack Contributors'], 1), ] # -- Options for openstackdocstheme ------------------------------------------- openstackdocs_repo_name = 'openstack/python-cinderclient' openstackdocs_bug_project = 'python-cinderclient' openstackdocs_bug_tag = 'doc' openstackdocs_pdf_link = True # -- Options for LaTeX output ------------------------------------------------- # The paper size ('letter' or 'a4'). # latex_paper_size = 'letter' # The font size ('10pt', '11pt' or '12pt'). # latex_font_size = '10pt' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', 'doc-python-cinderclient.tex', 'Cinder Client Documentation', 'Cinder Contributors', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # Additional stuff for the LaTeX preamble. # latex_preamble = '' # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_use_modindex = True # Disable usage of xindy https://bugzilla.redhat.com/show_bug.cgi?id=1643664 latex_use_xindy = False latex_domain_indices = False latex_elements = { 'makeindex': '', 'printindex': '', 'preamble': r'\setcounter{tocdepth}{3}', } latex_additional_files = ['cinderclient.sty'] ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1645793255.285896 python-cinderclient-8.3.0/doc/source/contributor/0000775000175000017500000000000000000000000022146 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/source/contributor/contributing.rst0000664000175000017500000000130500000000000025406 0ustar00zuulzuul00000000000000============================ So You Want to Contribute... ============================ For general information on contributing to OpenStack, please check out the `contributor guide `_ to get started. It covers all the basics that are common to all OpenStack projects: the accounts you need, the basics of interacting with our Gerrit review system, how we communicate as a community, etc. The python-cinderclient is maintained by the OpenStack Cinder project. To understand our development process and how you can contribute to it, please look at the Cinder project's general contributor's page: http://docs.openstack.org/cinder/latest/contributor/contributing.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/source/contributor/functional_tests.rst0000664000175000017500000000334300000000000026267 0ustar00zuulzuul00000000000000================ Functional Tests ================ Cinderclient contains a suite of functional tests, in the cinderclient/ tests/functional directory. These are currently non-voting, meaning that zuul will not reject a patched based on failure of the functional tests. It is highly recommended, however, that these tests are investigated in the case of a failure. Running the tests ----------------- Run the tests using tox, via the tox.ini file. To run all tests simply run:: tox -e functional This will create a virtual environment, load all the packages from test-requirements.txt and run all unit tests as well as run flake8 and hacking checks against the code. Note that you can inspect the tox.ini file to get more details on the available options and what the test run does by default. Running a subset of tests using tox ----------------------------------- One common activity is to just run a single test, you can do this with tox simply by specifying to just run py27 or py34 tests against a single test:: tox -e functional -- -n cinderclient.tests.functional.test_readonly_cli.CinderClientReadOnlyTests.test_list Or all tests in the test_readonly_clitest_readonly_cli.py file:: tox -e functional -- -n cinderclient.tests.functional.test_readonly_cli For more information on these options and how to run tests, please see the `stestr documentation `_. Gotchas ------- The cinderclient.tests.functional.test_cli.CinderBackupTests.test_backup_create and_delete test will fail in Devstack without c-bak service running, which requires Swift. Make sure Swift is enabled when you stack.sh by putting this in local.conf:: enable_service s-proxy s-object s-container s-account ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/source/contributor/unit_tests.rst0000664000175000017500000000353700000000000025111 0ustar00zuulzuul00000000000000========== Unit Tests ========== Cinderclient contains a suite of unit tests, in the cinderclient/tests/unit directory. Any proposed code change will be automatically rejected by the OpenStack Jenkins server if the change causes unit test failures. Running the tests ----------------- There are a number of ways to run unit tests currently, and there's a combination of frameworks used depending on what commands you use. The preferred method is to use tox, which calls ostestr via the tox.ini file. To run all tests simply run:: tox This will create a virtual environment, load all the packages from test-requirements.txt and run all unit tests as well as run flake8 and hacking checks against the code. Note that you can inspect the tox.ini file to get more details on the available options and what the test run does by default. Running a subset of tests using tox ----------------------------------- One common activity is to just run a single test, you can do this with tox simply by specifying to just run py3 tests against a single test:: tox -e py3 -- -n cinderclient.tests.unit.v3.test_volumes.VolumesTest.test_create_volume Or all tests in the test_volumes.py file:: tox -e py3 -- -n cinderclient.tests.unit.v3.test_volumes For more information on these options and how to run tests, please see the `stestr documentation `_. Gotchas ------- **Running Tests from Shared Folders** If you are running the unit tests from a shared folder, you may see tests start to fail or stop completely as a result of Python lockfile issues. You can get around this by manually setting or updating the following line in ``cinder/tests/conf_fixture.py``:: CONF['lock_path'].SetDefault('/tmp') Note that you may use any location (not just ``/tmp``!) as long as it is not a shared folder. .. rubric:: Footnotes ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/source/index.rst0000664000175000017500000003612600000000000021445 0ustar00zuulzuul00000000000000Python API ========== In order to use the Python api directly, you must first obtain an auth token and identify which endpoint you wish to speak to. Once you have done so, you can use the API like so:: >>> from cinderclient import client >>> cinder = client.Client('1', $OS_USER_NAME, $OS_PASSWORD, $OS_PROJECT_NAME, $OS_AUTH_URL) >>> cinder.volumes.list() [] >>> myvol = cinder.volumes.create(display_name="test-vol", size=1) >>> myvol.id ce06d0a8-5c1b-4e2c-81d2-39eca6bbfb70 >>> cinder.volumes.list() [] >>> myvol.delete() Alternatively, you can create a client instance using the keystoneauth session API:: >>> from keystoneauth1 import loading >>> from keystoneauth1 import session >>> from cinderclient import client >>> loader = loading.get_plugin_loader('password') >>> auth = loader.load_from_options(auth_url=AUTH_URL, ... username=USERNAME, ... password=PASSWORD, ... project_id=PROJECT_ID, ... user_domain_name=USER_DOMAIN_NAME) >>> sess = session.Session(auth=auth) >>> cinder = client.Client(VERSION, session=sess) >>> cinder.volumes.list() [] User Guides ~~~~~~~~~~~ .. toctree:: :maxdepth: 2 user/shell user/no_auth Command-Line Reference ~~~~~~~~~~~~~~~~~~~~~~ .. toctree:: :maxdepth: 2 cli/index cli/details Developer Guides ~~~~~~~~~~~~~~~~ .. toctree:: :maxdepth: 2 contributor/contributing contributor/functional_tests contributor/unit_tests Release Notes ~~~~~~~~~~~~~ All python-cinderclient release notes can now be found on the `release notes`_ page. .. _`release notes`: https://docs.openstack.org/releasenotes/python-cinderclient/ The following are kept for historical purposes. 1.4.0 ----- * Improved error reporting on reaching quota. * Volume status management for volume migration. * Added command to fetch specified backend capabilities. * Added commands for modifying image metadata. * Support for non-disruptive backup. * Support for cloning consistency groups. .. _1493612: https://bugs.launchpad.net/python-cinderclient/+bug/1493612 .. _1482988: https://bugs.launchpad.net/python-cinderclient/+bug/1482988 .. _1422046: https://bugs.launchpad.net/python-cinderclient/+bug/1422046 .. _1481478: https://bugs.launchpad.net/python-cinderclient/+bug/1481478 .. _1475430: https://bugs.launchpad.net/python-cinderclient/+bug/1475430 1.3.1 ----- * Fixed usage of the --debug option. * Documentation and API example improvements. * Set max volume size limit for the project. * Added encryption-type-update to cinderclient. * Added volume multi attach support. * Support host-attach of volumes. .. _1467628: https://bugs.launchpad.net/python-cinderclient/+bug/1467628 .. _1454436: https://bugs.launchpad.net/cinder/+bug/1454436 .. _1423884: https://bugs.launchpad.net/python-cinderclient/+bug/1423884 1.3.0 ----- * Revert version discovery support due to this breaking deployments using proxies. We will revisit this once the Kilo config option 'public_endpoint' has been available longer to allow these deployments to work again with version discovery available from the Cinder client. * Add volume multi-attach support. * Add encryption-type-update to update volume encryption types. .. _1454276: http://bugs.launchpad.net/python-cinderclient/+bug/1454276 .. _1462104: http://bugs.launchpad.net/python-cinderclient/+bug/1462104 .. _1418580: http://bugs.launchpad.net/python-cinderclient/+bug/1418580 .. _1464160: http://bugs.launchpad.net/python-cinderclient/+bug/1464160 1.2.2 ----- * IMPORTANT: version discovery breaks deployments using proxies and has been reverted in v1.3.0 . Do not use this version. * Update requirements to resolve conflicts with other OpenStack projects 1.2.1 ----- * IMPORTANT: version discovery breaks deployments using proxies and has been reverted in v1.3.0 . Do not use this version. * Remove warnings about Keystone unable to contact endpoint for discovery. * backup-create subcommand allows specifying --incremental to do an incremental backup. * Modify consistency groups using the consisgroup-update subcommand. Change the name, description, add volumes, or remove volumes. * Create consistency group from consistency group snapshot using the consisgroup-create-from-src subcommand. * --force no longer needs a boolean to be specified. .. _1341411: http://bugs.launchpad.net/python-cinderclient/+bug/1341411 .. _1429102: http://bugs.launchpad.net/python-cinderclient/+bug/1429102 .. _1447589: http://bugs.launchpad.net/python-cinderclient/+bug/1447589 .. _1447162: http://bugs.launchpad.net/python-cinderclient/+bug/1447162 .. _1448244: http://bugs.launchpad.net/python-cinderclient/+bug/1448244 .. _1244453: http://bugs.launchpad.net/python-cinderclient/+bug/1244453 1.2.0 ----- * IMPORTANT: version discovery breaks deployments using proxies and has been reverted in v1.3.0 . Do not use this version. * Add metadata during snapshot create. * Add TTY password entry when no password is environment vars or --os-password. * Ability to set backup quota in quota-update subcommand. * Force the client to use a particular Cinder API endpoint with --bypass-url. * Create a volume from an image by image name. * New type-default subcommand will display the default volume type. * New type-update subcommand allows updating a volume type's description. * type-list subcommand displays volume type description. * type-create subcommand allows setting the description. * Show pools to a backend when doing a service-list subcommand. * List and update consistency group quotas. * Create volume types that are non-public and have particular project access. * -d is available as a shorter option to --debug. * transfer-list subcommand has an option for --all-tenants. * --sort option available instead of --sort-key and --sort-dir. E.q. --sort [:]. * Volume type name can now be updated via subcommand type-update. * bash completion gives subcommands when using 'cinder help'. * Version discovery is now available. You no longer need a volumev2 service type in your keystone catalog. * Filter by tenant in list subcommand. .. _1373662: http://bugs.launchpad.net/python-cinderclient/+bug/1373662 .. _1376311: http://bugs.launchpad.net/python-cinderclient/+bug/1376311 .. _1368910: http://bugs.launchpad.net/python-cinderclient/+bug/1368910 .. _1374211: http://bugs.launchpad.net/python-cinderclient/+bug/1374211 .. _1379505: http://bugs.launchpad.net/python-cinderclient/+bug/1379505 .. _1282324: http://bugs.launchpad.net/python-cinderclient/+bug/1282324 .. _1358926: http://bugs.launchpad.net/python-cinderclient/+bug/1358926 .. _1342192: http://bugs.launchpad.net/python-cinderclient/+bug/1342192 .. _1386232: http://bugs.launchpad.net/python-cinderclient/+bug/1386232 .. _1402846: http://bugs.launchpad.net/python-cinderclient/+bug/1402846 .. _1373766: http://bugs.launchpad.net/python-cinderclient/+bug/1373766 .. _1403902: http://bugs.launchpad.net/python-cinderclient/+bug/1403902 .. _1377823: http://bugs.launchpad.net/python-cinderclient/+bug/1377823 .. _1350702: http://bugs.launchpad.net/python-cinderclient/+bug/1350702 .. _1357559: http://bugs.launchpad.net/python-cinderclient/+bug/1357559 .. _1341424: http://bugs.launchpad.net/python-cinderclient/+bug/1341424 .. _1365273: http://bugs.launchpad.net/python-cinderclient/+bug/1365273 .. _1404020: http://bugs.launchpad.net/python-cinderclient/+bug/1404020 .. _1380729: http://bugs.launchpad.net/python-cinderclient/+bug/1380729 .. _1417273: http://bugs.launchpad.net/python-cinderclient/+bug/1417273 .. _1420238: http://bugs.launchpad.net/python-cinderclient/+bug/1420238 .. _1421210: http://bugs.launchpad.net/python-cinderclient/+bug/1421210 .. _1351084: http://bugs.launchpad.net/python-cinderclient/+bug/1351084 .. _1366289: http://bugs.launchpad.net/python-cinderclient/+bug/1366289 .. _1309086: http://bugs.launchpad.net/python-cinderclient/+bug/1309086 .. _1379486: http://bugs.launchpad.net/python-cinderclient/+bug/1379486 .. _1422244: http://bugs.launchpad.net/python-cinderclient/+bug/1422244 .. _1399747: http://bugs.launchpad.net/python-cinderclient/+bug/1399747 .. _1431693: http://bugs.launchpad.net/python-cinderclient/+bug/1431693 .. _1428764: http://bugs.launchpad.net/python-cinderclient/+bug/1428764 ** Python 2.4 support removed. ** --sort-key and --sort-dir are deprecated. Use --sort instead. ** A dash will be displayed of None when there is no data to display under a column. 1.1.1 ------ .. _1370152: http://bugs.launchpad.net/python-cinderclient/+bug/1370152 1.1.0 ------ * Add support for ConsistencyGroups * Use Adapter from keystoneclient * Add support for Replication feature * Add pagination for Volume List * Note Connection refused --> Connection error commit: c9e7818f3f90ce761ad8ccd09181c705880a4266 * Note Mask Passwords in log output commit: 80582f2b860b2dadef7ae07bdbd8395bf03848b1 .. _1325773: http://bugs.launchpad.net/python-cinderclient/+bug/1325773 .. _1333257: http://bugs.launchpad.net/python-cinderclient/+bug/1333257 .. _1268480: http://bugs.launchpad.net/python-cinderclient/+bug/1268480 .. _1275025: http://bugs.launchpad.net/python-cinderclient/+bug/1275025 .. _1258489: http://bugs.launchpad.net/python-cinderclient/+bug/1258489 .. _1241682: http://bugs.launchpad.net/python-cinderclient/+bug/1241682 .. _1203471: http://bugs.launchpad.net/python-cinderclient/+bug/1203471 .. _1210874: http://bugs.launchpad.net/python-cinderclient/+bug/1210874 .. _1200214: http://bugs.launchpad.net/python-cinderclient/+bug/1200214 .. _1130572: http://bugs.launchpad.net/python-cinderclient/+bug/1130572 .. _1156994: http://bugs.launchpad.net/python-cinderclient/+bug/1156994 1.0.9 ------ .. _1255905: http://bugs.launchpad.net/python-cinderclient/+bug/1255905 .. _1267168: http://bugs.launchpad.net/python-cinderclient/+bug/1267168 .. _1284540: http://bugs.launchpad.net/python-cinderclient/+bug/1284540 1.0.8 ----- * Add support for reset-state on multiple volumes or snapshots at once * Add volume retype command .. _966329: https://bugs.launchpad.net/python-cinderclient/+bug/966329 .. _1256043: https://bugs.launchpad.net/python-cinderclient/+bug/1256043 .. _1254951: http://bugs.launchpad.net/python-cinderclient/+bug/1254951 .. _1254587: http://bugs.launchpad.net/python-cinderclient/+bug/1254587 .. _1253142: http://bugs.launchpad.net/python-cinderclient/+bug/1253142 .. _1252665: http://bugs.launchpad.net/python-cinderclient/+bug/1252665 .. _1255876: http://bugs.launchpad.net/python-cinderclient/+bug/1255876 .. _1251385: http://bugs.launchpad.net/python-cinderclient/+bug/1251385 .. _1264415: http://bugs.launchpad.net/python-cinderclient/+bug/1264415 .. _1258489: http://bugs.launchpad.net/python-cinderclient/+bug/1258489 .. _1248519: http://bugs.launchpad.net/python-cinderclient/+bug/1248519 .. _1257747: http://bugs.launchpad.net/python-cinderclient/+bug/1257747 1.0.7 ----- * Add support for read-only volumes * Add support for setting snapshot metadata * Deprecate volume-id arg to backup restore in favor of --volume * Add quota-usage command * Fix exception deprecation warning message * Report error when no args supplied to rename cmd .. _1241941: http://bugs.launchpad.net/python-cinderclient/+bug/1241941 .. _1242816: http://bugs.launchpad.net/python-cinderclient/+bug/1242816 .. _1233311: http://bugs.launchpad.net/python-cinderclient/+bug/1233311 .. _1227307: http://bugs.launchpad.net/python-cinderclient/+bug/1227307 .. _1240151: http://bugs.launchpad.net/python-cinderclient/+bug/1240151 .. _1241682: http://bugs.launchpad.net/python-cinderclient/+bug/1241682 1.0.6 ----- * Add support for multiple endpoints * Add response info for backup command * Add metadata option to cinder list command * Add timeout parameter for requests * Add update action for snapshot metadata * Add encryption metadata support * Add volume migrate support * Add support for QoS specs .. _1221104: http://bugs.launchpad.net/python-cinderclient/+bug/1221104 .. _1220590: http://bugs.launchpad.net/python-cinderclient/+bug/1220590 .. _1220147: http://bugs.launchpad.net/python-cinderclient/+bug/1220147 .. _1214176: http://bugs.launchpad.net/python-cinderclient/+bug/1214176 .. _1210874: http://bugs.launchpad.net/python-cinderclient/+bug/1210874 .. _1210296: http://bugs.launchpad.net/python-cinderclient/+bug/1210296 .. _1210292: http://bugs.launchpad.net/python-cinderclient/+bug/1210292 .. _1207635: http://bugs.launchpad.net/python-cinderclient/+bug/1207635 .. _1207609: http://bugs.launchpad.net/python-cinderclient/+bug/1207609 .. _1207260: http://bugs.launchpad.net/python-cinderclient/+bug/1207260 .. _1206968: http://bugs.launchpad.net/python-cinderclient/+bug/1206968 .. _1203471: http://bugs.launchpad.net/python-cinderclient/+bug/1203471 .. _1200214: http://bugs.launchpad.net/python-cinderclient/+bug/1200214 .. _1195014: http://bugs.launchpad.net/python-cinderclient/+bug/1195014 1.0.5 ----- * Add CLI man page * Add Availability Zone list command * Add support for scheduler-hints * Add support to extend volumes * Add support to reset state on volumes and snapshots * Add snapshot support for quota class .. _1190853: http://bugs.launchpad.net/python-cinderclient/+bug/1190853 .. _1190731: http://bugs.launchpad.net/python-cinderclient/+bug/1190731 .. _1169455: http://bugs.launchpad.net/python-cinderclient/+bug/1169455 .. _1188452: http://bugs.launchpad.net/python-cinderclient/+bug/1188452 .. _1180393: http://bugs.launchpad.net/python-cinderclient/+bug/1180393 .. _1182678: http://bugs.launchpad.net/python-cinderclient/+bug/1182678 .. _1179008: http://bugs.launchpad.net/python-cinderclient/+bug/1179008 .. _1180059: http://bugs.launchpad.net/python-cinderclient/+bug/1180059 .. _1170565: http://bugs.launchpad.net/python-cinderclient/+bug/1170565 1.0.4 ----- * Added support for backup-service commands .. _1163546: http://bugs.launchpad.net/python-cinderclient/+bug/1163546 .. _1161857: http://bugs.launchpad.net/python-cinderclient/+bug/1161857 .. _1160898: http://bugs.launchpad.net/python-cinderclient/+bug/1160898 .. _1161857: http://bugs.launchpad.net/python-cinderclient/+bug/1161857 .. _1156994: http://bugs.launchpad.net/python-cinderclient/+bug/1156994 1.0.3 ----- * Added support for V2 Cinder API * Corrected upload-volume-to-image help messaging * Align handling of metadata args for all methods * Update OSLO version * Correct parsing of volume metadata * Enable force delete of volumes and snapshots in error state * Implement clone volume API call * Add list-extensions call to cinderclient * Add bootable column to list output * Add retries to cinderclient operations * Add Type/Extra-Specs support * Add volume and snapshot rename commands .. _1155655: http://bugs.launchpad.net/python-cinderclient/+bug/1155655 .. _1130730: http://bugs.launchpad.net/python-cinderclient/+bug/1130730 .. _1068521: http://bugs.launchpad.net/python-cinderclient/+bug/1068521 .. _1052161: http://bugs.launchpad.net/python-cinderclient/+bug/1052161 .. _1071003: http://bugs.launchpad.net/python-cinderclient/+bug/1071003 .. _1065275: http://bugs.launchpad.net/python-cinderclient/+bug/1065275 .. _1053432: http://bugs.launchpad.net/python-cinderclient/+bug/1053432 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1645793255.285896 python-cinderclient-8.3.0/doc/source/user/0000775000175000017500000000000000000000000020552 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/source/user/no_auth.rst0000664000175000017500000000155100000000000022743 0ustar00zuulzuul00000000000000============ Using noauth ============ Cinder Server side API setup ============================ The changes in the cinder.conf on your cinder-api node are minimal, just set authstrategy to noauth:: [DEFAULT] auth_strategy = noauth ... Using cinderclient ------------------ To use the cinderclient you'll need to set the following env variables:: OS_AUTH_TYPE=noauth CINDER_ENDPOINT=http://:8776/v3 OS_PROJECT_ID=foo OS_VOLUME_API_VERSION=3.10 Note that you can have multiple projects, however we don't currently do any sort of authentication of ownership because, well that's the whole point, it's noauth. Each of these options can also be specified on the cmd line:: cinder --os-auth-type=noauth \ --os-endpoint=http://:8776/v3 \ --os-project-id=admin \ --os-volume-api-version=3.10 list ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/doc/source/user/shell.rst0000664000175000017500000000321300000000000022412 0ustar00zuulzuul00000000000000The :program:`cinder` shell utility =================================== .. program:: cinder .. highlight:: bash The :program:`cinder` shell utility interacts with the OpenStack Cinder API from the command line. It supports the entirety of the OpenStack Cinder API. You'll need to provide :program:`cinder` with your OpenStack username and API key. You can do this with the `--os-username`, `--os-password` and `--os-tenant-name` options, but it's easier to just set them as environment variables by setting two environment variables: .. envvar:: OS_USERNAME or CINDER_USERNAME Your OpenStack Cinder username. .. envvar:: OS_PASSWORD or CINDER_PASSWORD Your password. .. envvar:: OS_PROJECT_NAME or CINDER_PROJECT_ID Project for work. .. envvar:: OS_AUTH_URL or CINDER_URL The OpenStack API server URL. .. envvar:: OS_VOLUME_API_VERSION The OpenStack Block Storage API version. For example, in Bash you'd use:: export OS_USERNAME=yourname export OS_PASSWORD=yadayadayada export OS_PROJECT_NAME=myproject export OS_AUTH_URL=http://auth.example.com:5000/v3 export OS_VOLUME_API_VERSION=3 If OS_VOLUME_API_VERSION is not set, the highest version supported by the server will be used. If OS_VOLUME_API_VERSION exceeds the highest version supported by the server, the highest version supported by both the client and server will be used. A warning message is printed when this occurs. From there, all shell commands take the form:: cinder [arguments...] Run :program:`cinder help` to get a full list of all possible commands, and run :program:`cinder help ` to get detailed help for that command. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/functional_creds.conf.sample0000664000175000017500000000020200000000000023172 0ustar00zuulzuul00000000000000# Credentials for functional testing [auth] uri = http://10.42.0.50:5000/v2.0 [admin] user = admin tenant = admin pass = secrete ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/pylintrc0000664000175000017500000000207300000000000017320 0ustar00zuulzuul00000000000000# The format of this file isn't really documented; just use --generate-rcfile [Messages Control] # C0111: Don't require docstrings on every method # W0511: TODOs in code comments are fine. # W0142: *args and **kwargs are fine. # W0622: Redefining id is fine. disable=C0111,W0511,W0142,W0622 [Basic] # Variable names can be 1 to 31 characters long, with lowercase and underscores variable-rgx=[a-z_][a-z0-9_]{0,30}$ # Argument names can be 2 to 31 characters long, with lowercase and underscores argument-rgx=[a-z_][a-z0-9_]{1,30}$ # Method names should be at least 3 characters long # and be lowercased with underscores method-rgx=([a-z_][a-z0-9_]{2,50}|setUp|tearDown)$ # Don't require docstrings on tests. no-docstring-rgx=((__.*__)|([tT]est.*)|setUp|tearDown)$ [Design] max-public-methods=100 min-public-methods=0 max-args=6 [Variables] dummy-variables-rgx=_ [Typecheck] # Disable warnings on the HTTPSConnection classes because pylint doesn't # support importing from six.moves yet, see: # https://bitbucket.org/logilab/pylint/issue/550/ ignored-classes=HTTPSConnection ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1645793255.289896 python-cinderclient-8.3.0/python_cinderclient.egg-info/0000775000175000017500000000000000000000000023265 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793255.0 python-cinderclient-8.3.0/python_cinderclient.egg-info/PKG-INFO0000664000175000017500000005243500000000000024373 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: python-cinderclient Version: 8.3.0 Summary: OpenStack Block Storage API Client Library Home-page: https://docs.openstack.org/python-cinderclient/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: UNKNOWN Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-cinderclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on Python bindings to the OpenStack Cinder API =========================================== .. image:: https://img.shields.io/pypi/v/python-cinderclient.svg :target: https://pypi.org/project/python-cinderclient/ :alt: Latest Version This is a client for the OpenStack Cinder API. There's a Python API (the ``cinderclient`` module), and a command-line script (``cinder``). Each implements 100% of the OpenStack Cinder API. See the `OpenStack CLI Reference`_ for information on how to use the ``cinder`` command-line tool. You may also want to look at the `OpenStack API documentation`_. .. _OpenStack CLI Reference: https://docs.openstack.org/python-openstackclient/latest/cli/ .. _OpenStack API documentation: https://docs.openstack.org/api-quick-start/ The project is hosted on `Launchpad`_, where bugs can be filed. The code is hosted on `OpenStack`_. Patches must be submitted using `Gerrit`_. .. _OpenStack: https://opendev.org/openstack/python-cinderclient .. _Launchpad: https://launchpad.net/python-cinderclient .. _Gerrit: https://docs.openstack.org/infra/manual/developers.html#development-workflow * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.org/project/python-cinderclient .. _Online Documentation: https://docs.openstack.org/python-cinderclient/latest/ .. _Blueprints: https://blueprints.launchpad.net/python-cinderclient .. _Bugs: https://bugs.launchpad.net/python-cinderclient .. _Source: https://opendev.org/openstack/python-cinderclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/cinder-specs/ .. contents:: Contents: :local: Command-line API ---------------- Installing this package gets you a shell command, ``cinder``, that you can use to interact with any Rackspace compatible API (including OpenStack). You'll need to provide your OpenStack username and password. You can do this with the ``--os-username``, ``--os-password`` and ``--os-tenant-name`` params, but it's easier to just set them as environment variables:: export OS_USERNAME=openstack export OS_PASSWORD=yadayada export OS_TENANT_NAME=myproject You will also need to define the authentication url with ``--os-auth-url`` and the version of the API with ``--os-volume-api-version``. Or set them as environment variables as well. Since Block Storage API V2 is officially deprecated, you are encouraged to set ``OS_VOLUME_API_VERSION=3``. If you are using Keystone, you need to set the ``OS_AUTH_URL`` to the keystone endpoint:: export OS_AUTH_URL=http://controller:5000/v3 export OS_VOLUME_API_VERSION=3 Since Keystone can return multiple regions in the Service Catalog, you can specify the one you want with ``--os-region-name`` (or ``export OS_REGION_NAME``). It defaults to the first in the list returned. You'll find complete documentation on the shell by running ``cinder help``:: usage: cinder [--version] [-d] [--os-auth-system ] [--service-type ] [--service-name ] [--volume-service-name ] [--os-endpoint-type ] [--endpoint-type ] [--os-volume-api-version ] [--retries ] [--profile HMAC_KEY] [--os-auth-strategy ] [--os-username ] [--os-password ] [--os-tenant-name ] [--os-tenant-id ] [--os-auth-url ] [--os-user-id ] [--os-user-domain-id ] [--os-user-domain-name ] [--os-project-id ] [--os-project-name ] [--os-project-domain-id ] [--os-project-domain-name ] [--os-region-name ] [--os-token ] [--os-url ] [--insecure] [--os-cacert ] [--os-cert ] [--os-key ] [--timeout ] ... Command-line interface to the OpenStack Cinder API. Positional arguments: absolute-limits Lists absolute limits for a user. api-version Display the server API version information. (Supported by API versions 3.0 - 3.latest) availability-zone-list Lists all availability zones. backup-create Creates a volume backup. backup-delete Removes one or more backups. backup-export Export backup metadata record. backup-import Import backup metadata record. backup-list Lists all backups. backup-reset-state Explicitly updates the backup state. backup-restore Restores a backup. backup-show Shows backup details. cgsnapshot-create Creates a cgsnapshot. cgsnapshot-delete Removes one or more cgsnapshots. cgsnapshot-list Lists all cgsnapshots. cgsnapshot-show Shows cgsnapshot details. consisgroup-create Creates a consistency group. consisgroup-create-from-src Creates a consistency group from a cgsnapshot or a source CG. consisgroup-delete Removes one or more consistency groups. consisgroup-list Lists all consistency groups. consisgroup-show Shows details of a consistency group. consisgroup-update Updates a consistency group. create Creates a volume. credentials Shows user credentials returned from auth. delete Removes one or more volumes. encryption-type-create Creates encryption type for a volume type. Admin only. encryption-type-delete Deletes encryption type for a volume type. Admin only. encryption-type-list Shows encryption type details for volume types. Admin only. encryption-type-show Shows encryption type details for a volume type. Admin only. encryption-type-update Update encryption type information for a volume type (Admin Only). endpoints Discovers endpoints registered by authentication service. extend Attempts to extend size of an existing volume. extra-specs-list Lists current volume types and extra specs. failover-host Failover a replicating cinder-volume host. force-delete Attempts force-delete of volume, regardless of state. freeze-host Freeze and disable the specified cinder-volume host. get-capabilities Show backend volume stats and properties. Admin only. get-pools Show pool information for backends. Admin only. image-metadata Sets or deletes volume image metadata. image-metadata-show Shows volume image metadata. list Lists all volumes. manage Manage an existing volume. metadata Sets or deletes volume metadata. metadata-show Shows volume metadata. metadata-update-all Updates volume metadata. migrate Migrates volume to a new host. qos-associate Associates qos specs with specified volume type. qos-create Creates a qos specs. qos-delete Deletes a specified qos specs. qos-disassociate Disassociates qos specs from specified volume type. qos-disassociate-all Disassociates qos specs from all its associations. qos-get-association Lists all associations for specified qos specs. qos-key Sets or unsets specifications for a qos spec. qos-list Lists qos specs. qos-show Shows qos specs details. quota-class-show Lists quotas for a quota class. quota-class-update Updates quotas for a quota class. quota-defaults Lists default quotas for a tenant. quota-delete Delete the quotas for a tenant. quota-show Lists quotas for a tenant. quota-update Updates quotas for a tenant. quota-usage Lists quota usage for a tenant. rate-limits Lists rate limits for a user. readonly-mode-update Updates volume read-only access-mode flag. rename Renames a volume. reset-state Explicitly updates the volume state in the Cinder database. retype Changes the volume type for a volume. service-disable Disables the service. service-enable Enables the service. service-list Lists all services. Filter by host and service binary. (Supported by API versions 3.0 - 3.latest) set-bootable Update bootable status of a volume. show Shows volume details. snapshot-create Creates a snapshot. snapshot-delete Removes one or more snapshots. snapshot-list Lists all snapshots. snapshot-manage Manage an existing snapshot. snapshot-metadata Sets or deletes snapshot metadata. snapshot-metadata-show Shows snapshot metadata. snapshot-metadata-update-all Updates snapshot metadata. snapshot-rename Renames a snapshot. snapshot-reset-state Explicitly updates the snapshot state. snapshot-show Shows snapshot details. snapshot-unmanage Stop managing a snapshot. thaw-host Thaw and enable the specified cinder-volume host. transfer-accept Accepts a volume transfer. transfer-create Creates a volume transfer. transfer-delete Undoes a transfer. transfer-list Lists all transfers. transfer-show Shows transfer details. type-access-add Adds volume type access for the given project. type-access-list Print access information about the given volume type. type-access-remove Removes volume type access for the given project. type-create Creates a volume type. type-default List the default volume type. type-delete Deletes volume type or types. type-key Sets or unsets extra_spec for a volume type. type-list Lists available 'volume types'. type-show Show volume type details. type-update Updates volume type name, description, and/or is_public. unmanage Stop managing a volume. upload-to-image Uploads volume to Image Service as an image. version-list List all API versions. (Supported by API versions 3.0 - 3.latest) bash-completion Prints arguments for bash_completion. help Shows help about this program or one of its subcommands. list-extensions Optional arguments: --version show program's version number and exit -d, --debug Shows debugging output. --os-auth-system Defaults to env[OS_AUTH_SYSTEM]. --service-type Service type. For most actions, default is volume. --service-name Service name. Default=env[CINDER_SERVICE_NAME]. --volume-service-name Volume service name. Default=env[CINDER_VOLUME_SERVICE_NAME]. --os-endpoint Use this API endpoint instead of the Service Catalog. Default=env[CINDER_ENDPOINT] --os-endpoint-type Endpoint type, which is publicURL or internalURL. Default=env[OS_ENDPOINT_TYPE] or nova env[CINDER_ENDPOINT_TYPE] or publicURL. --endpoint-type DEPRECATED! Use --os-endpoint-type. --os-volume-api-version Block Storage API version. Accepts X, X.Y (where X is major and Y is minor part).Default=env[OS_VOLUME_API_VERSION]. --retries Number of retries. --profile HMAC_KEY HMAC key to use for encrypting context data for performance profiling of operation. This key needs to match the one configured on the cinder api server. Without key the profiling will not be triggered even if osprofiler is enabled on server side. Defaults to env[OS_PROFILE]. --os-auth-strategy Authentication strategy (Env: OS_AUTH_STRATEGY, default keystone). For now, any other value will disable the authentication. --os-username OpenStack user name. Default=env[OS_USERNAME]. --os-password Password for OpenStack user. Default=env[OS_PASSWORD]. --os-tenant-name Tenant name. Default=env[OS_TENANT_NAME]. --os-tenant-id ID for the tenant. Default=env[OS_TENANT_ID]. --os-auth-url URL for the authentication service. Default=env[OS_AUTH_URL]. --os-user-id Authentication user ID (Env: OS_USER_ID). --os-user-domain-id OpenStack user domain ID. Defaults to env[OS_USER_DOMAIN_ID]. --os-user-domain-name OpenStack user domain name. Defaults to env[OS_USER_DOMAIN_NAME]. --os-project-id Another way to specify tenant ID. This option is mutually exclusive with --os-tenant-id. Defaults to env[OS_PROJECT_ID]. --os-project-name Another way to specify tenant name. This option is mutually exclusive with --os-tenant-name. Defaults to env[OS_PROJECT_NAME]. --os-project-domain-id Defaults to env[OS_PROJECT_DOMAIN_ID]. --os-project-domain-name Defaults to env[OS_PROJECT_DOMAIN_NAME]. --os-region-name Region name. Default=env[OS_REGION_NAME]. --os-token Defaults to env[OS_TOKEN]. --os-url Defaults to env[OS_URL]. API Connection Options: Options controlling the HTTP API Connections --insecure Explicitly allow client to perform "insecure" TLS (https) requests. The server's certificate will not be verified against any certificate authorities. This option should be used with caution. --os-cacert Specify a CA bundle file to use in verifying a TLS (https) server certificate. Defaults to env[OS_CACERT]. --os-cert Defaults to env[OS_CERT]. --os-key Defaults to env[OS_KEY]. --timeout Set request timeout (in seconds). Run "cinder help SUBCOMMAND" for help on a subcommand. If you want to get a particular version API help message, you can add ``--os-volume-api-version `` in help command, like this:: cinder --os-volume-api-version 3.28 help Python API ---------- There's also a complete Python API, but it has not yet been documented. Quick-start using keystone:: # use v3 auth with http://controller:5000/v3 >>> from cinderclient.v3 import client >>> nt = client.Client(USERNAME, PASSWORD, PROJECT_ID, AUTH_URL) >>> nt.volumes.list() [...] See release notes and more at ``_. Platform: UNKNOWN Classifier: Development Status :: 5 - Production/Stable Classifier: Environment :: Console Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Requires-Python: >=3.6 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793255.0 python-cinderclient-8.3.0/python_cinderclient.egg-info/SOURCES.txt0000664000175000017500000002300600000000000025152 0ustar00zuulzuul00000000000000.coveragerc .mailmap .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst bindep.txt functional_creds.conf.sample pylintrc requirements.txt setup.cfg setup.py test-requirements.txt tox.ini cinderclient/__init__.py cinderclient/_i18n.py cinderclient/api_versions.py cinderclient/base.py cinderclient/client.py cinderclient/exceptions.py cinderclient/extension.py cinderclient/shell.py cinderclient/shell_utils.py cinderclient/utils.py cinderclient/version.py cinderclient/apiclient/__init__.py cinderclient/apiclient/base.py cinderclient/apiclient/exceptions.py cinderclient/contrib/__init__.py cinderclient/contrib/noauth.py cinderclient/tests/__init__.py cinderclient/tests/functional/__init__.py cinderclient/tests/functional/base.py cinderclient/tests/functional/test_cli.py cinderclient/tests/functional/test_readonly_cli.py cinderclient/tests/functional/test_snapshot_create_cli.py cinderclient/tests/functional/test_volume_create_cli.py cinderclient/tests/functional/test_volume_extend_cli.py cinderclient/tests/unit/__init__.py cinderclient/tests/unit/fake_actions_module.py cinderclient/tests/unit/fakes.py cinderclient/tests/unit/test_api_versions.py cinderclient/tests/unit/test_auth_plugins.py cinderclient/tests/unit/test_base.py cinderclient/tests/unit/test_client.py cinderclient/tests/unit/test_exceptions.py cinderclient/tests/unit/test_http.py cinderclient/tests/unit/test_shell.py cinderclient/tests/unit/test_utils.py cinderclient/tests/unit/utils.py cinderclient/tests/unit/fixture_data/__init__.py cinderclient/tests/unit/fixture_data/availability_zones.py cinderclient/tests/unit/fixture_data/base.py cinderclient/tests/unit/fixture_data/client.py cinderclient/tests/unit/fixture_data/keystone_client.py cinderclient/tests/unit/fixture_data/snapshots.py cinderclient/tests/unit/v3/__init__.py cinderclient/tests/unit/v3/fakes.py cinderclient/tests/unit/v3/fakes_base.py cinderclient/tests/unit/v3/test_attachments.py cinderclient/tests/unit/v3/test_auth.py cinderclient/tests/unit/v3/test_availability_zone.py cinderclient/tests/unit/v3/test_capabilities.py cinderclient/tests/unit/v3/test_cgsnapshots.py cinderclient/tests/unit/v3/test_clusters.py cinderclient/tests/unit/v3/test_consistencygroups.py cinderclient/tests/unit/v3/test_default_types.py cinderclient/tests/unit/v3/test_group_snapshots.py cinderclient/tests/unit/v3/test_group_types.py cinderclient/tests/unit/v3/test_groups.py cinderclient/tests/unit/v3/test_limits.py cinderclient/tests/unit/v3/test_messages.py cinderclient/tests/unit/v3/test_pools.py cinderclient/tests/unit/v3/test_qos.py cinderclient/tests/unit/v3/test_quota_classes.py cinderclient/tests/unit/v3/test_quotas.py cinderclient/tests/unit/v3/test_resource_filters.py cinderclient/tests/unit/v3/test_services.py cinderclient/tests/unit/v3/test_services_base.py cinderclient/tests/unit/v3/test_shell.py cinderclient/tests/unit/v3/test_snapshot_actions.py cinderclient/tests/unit/v3/test_type_access.py cinderclient/tests/unit/v3/test_types.py cinderclient/tests/unit/v3/test_volume_backups.py cinderclient/tests/unit/v3/test_volume_backups_30.py cinderclient/tests/unit/v3/test_volume_encryption_types.py cinderclient/tests/unit/v3/test_volume_transfers.py cinderclient/tests/unit/v3/test_volumes.py cinderclient/tests/unit/v3/test_volumes_base.py cinderclient/tests/unit/v3/contrib/__init__.py cinderclient/tests/unit/v3/contrib/test_list_extensions.py cinderclient/v3/__init__.py cinderclient/v3/attachments.py cinderclient/v3/availability_zones.py cinderclient/v3/capabilities.py cinderclient/v3/cgsnapshots.py cinderclient/v3/client.py cinderclient/v3/clusters.py cinderclient/v3/consistencygroups.py cinderclient/v3/default_types.py cinderclient/v3/group_snapshots.py cinderclient/v3/group_types.py cinderclient/v3/groups.py cinderclient/v3/limits.py cinderclient/v3/messages.py cinderclient/v3/pools.py cinderclient/v3/qos_specs.py cinderclient/v3/quota_classes.py cinderclient/v3/quotas.py cinderclient/v3/resource_filters.py cinderclient/v3/services.py cinderclient/v3/shell.py cinderclient/v3/shell_base.py cinderclient/v3/volume_backups.py cinderclient/v3/volume_backups_restore.py cinderclient/v3/volume_encryption_types.py cinderclient/v3/volume_snapshots.py cinderclient/v3/volume_transfers.py cinderclient/v3/volume_type_access.py cinderclient/v3/volume_types.py cinderclient/v3/volumes.py cinderclient/v3/volumes_base.py cinderclient/v3/workers.py cinderclient/v3/contrib/__init__.py cinderclient/v3/contrib/list_extensions.py doc/.gitignore doc/Makefile doc/requirements.txt doc/ext/__init__.py doc/ext/cli.py doc/source/conf.py doc/source/index.rst doc/source/cli/details.rst doc/source/cli/index.rst doc/source/contributor/contributing.rst doc/source/contributor/functional_tests.rst doc/source/contributor/unit_tests.rst doc/source/user/no_auth.rst doc/source/user/shell.rst python_cinderclient.egg-info/PKG-INFO python_cinderclient.egg-info/SOURCES.txt python_cinderclient.egg-info/dependency_links.txt python_cinderclient.egg-info/entry_points.txt python_cinderclient.egg-info/not-zip-safe python_cinderclient.egg-info/pbr.json python_cinderclient.egg-info/requires.txt python_cinderclient.egg-info/top_level.txt releasenotes/notes/add-generic-reset-state-command-d83v1f3accbf5807.yaml releasenotes/notes/adding-option-is-public-to-type-list-9a16bd9c2b8eb65a.yaml releasenotes/notes/attachment-create-optional-server-id-9299d9da2b62b263.yaml releasenotes/notes/attachment-mode-8427aa6a2fa26e70.yaml releasenotes/notes/backup-user-id-059ccea871893a0b.yaml releasenotes/notes/bug-1608166-ad91a7a9f50e658a.yaml releasenotes/notes/bug-1675973-ad91a7a9f50e658a.yaml releasenotes/notes/bug-1675974-34edd5g9870e65b2.yaml releasenotes/notes/bug-1675975-ad91a7a34e0esywc.yaml releasenotes/notes/bug-1705093-9bc782d44018c27d.yaml releasenotes/notes/bug-1713082-fb9276eed70f7e3b.yaml releasenotes/notes/bug-1826286-c9b68709a0d63d06.yaml releasenotes/notes/bug-1867061-fix-py-raw-error-msg-ff3c6da0b01d5d6c.yaml releasenotes/notes/bug-1915996-3aaa5e2548eb7c93.yaml releasenotes/notes/cinder-poll-4f92694cc7eb657a.yaml releasenotes/notes/cinderclient-5-de0508ce5a221d21.yaml releasenotes/notes/cli-api-ver-negotiation-9f8fd8b77ae299fd.yaml releasenotes/notes/cluster_commands-dca50e89c9d53cd2.yaml releasenotes/notes/cluster_list_manageable-40c02489b2c95d55.yaml releasenotes/notes/cluster_migration_manage-31144d67bbfdb739.yaml releasenotes/notes/deprecate-allow-multiattach-2213a100c65a95c1.yaml releasenotes/notes/do-not-reset-volume-status-ae8e28132d7bfacd.yaml releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml releasenotes/notes/drop-v2-support-e578ca21c7c6b532.yaml releasenotes/notes/enhance-backup-restore-shell-command-0cf55df6ca4b4c55.yaml releasenotes/notes/feature-cross-az-backups-9d428ad4dfc552e1.yaml releasenotes/notes/list-with-count-78gtf45r66bf8912.yaml releasenotes/notes/log-request-id-148c74d308bcaa14.yaml releasenotes/notes/messages-v3-api-3da81f4f66bf5903.yaml releasenotes/notes/noauth-7d95e5af31a00e96.yaml releasenotes/notes/profile-as-environment-variable-2a5c666ef759e486.yaml releasenotes/notes/project-default-types-727156d1db10a24d.yaml releasenotes/notes/reimage-volume-fea3a1178662e65a.yaml releasenotes/notes/remove-cg-quota-9d4120b62f09cc5c.yaml releasenotes/notes/remove-credentials-e92b68e3bda80057.yaml releasenotes/notes/remove-deprecations-621919062f867015.yaml releasenotes/notes/remove-replv1-cabf2194edb9d963.yaml releasenotes/notes/remove-replv1-cli-61d5722438f888b6.yaml releasenotes/notes/replication-group-v3-api-022900ce6bf8feba.yaml releasenotes/notes/return-request-id-to-caller-78d27f33f0048405.yaml releasenotes/notes/revert-to-snapshot-er4598df88aq5918.yaml releasenotes/notes/service_cleanup_cmd-cac85b697bc22af1.yaml releasenotes/notes/service_dynamic_log-bd81d93c73fc1570.yaml releasenotes/notes/start-using-reno-18001103a6719c13.yaml releasenotes/notes/support---os-key-option-72ba2fd4880736ac.yaml releasenotes/notes/support-bs-mv-3.60-a65f1919b5068d17.yaml releasenotes/notes/support-bs-mv-3.66-5214deb20d164056.yaml releasenotes/notes/support-create-volume-from-backup-c4e8aac89uy18uy2.yaml releasenotes/notes/support-filter-type-7yt69ub7ccbf7419.yaml releasenotes/notes/support-filters-transfer-a1e7b728c7895a45.yaml releasenotes/notes/support-generialized-resource-filter-8yf6w23f66bf5903.yaml releasenotes/notes/support-keystone-v3-for-httpClient-d48ebb24880f5821.yaml releasenotes/notes/support-like-filter-7434w23f66bf5587.yaml releasenotes/notes/support-show-group-with-volume-ad820b8442e8a9e8.yaml releasenotes/notes/support-volume-summary-d6d5bb2acfef6ad5.yaml releasenotes/notes/transfer-snapshots-555c61477835bcf7.yaml releasenotes/notes/transfer-sort-ca622e9b8da605c1.yaml releasenotes/notes/ussuri-release-f0ebfc54cdac6680.yaml releasenotes/notes/victoria-release-0d9c2b43845c3d9e.yaml releasenotes/notes/volume-transfer-bug-23c760efb9f98a4d.yaml releasenotes/notes/wallaby-release-2535df50cc307fea.yaml releasenotes/notes/xena-release-688918a69ada3a58.yaml releasenotes/notes/yoga-release-dcd35c98f6be478e.yaml releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/mitaka.rst releasenotes/source/newton.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/wallaby.rst releasenotes/source/xena.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder tools/cinder.bash_completion tools/generate_authors.sh tools/lintstack.py tools/lintstack.sh././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793255.0 python-cinderclient-8.3.0/python_cinderclient.egg-info/dependency_links.txt0000664000175000017500000000000100000000000027333 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793255.0 python-cinderclient-8.3.0/python_cinderclient.egg-info/entry_points.txt0000664000175000017500000000020400000000000026557 0ustar00zuulzuul00000000000000[console_scripts] cinder = cinderclient.shell:main [keystoneauth1.plugin] noauth = cinderclient.contrib.noauth:CinderNoAuthLoader ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793255.0 python-cinderclient-8.3.0/python_cinderclient.egg-info/not-zip-safe0000664000175000017500000000000100000000000025513 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793255.0 python-cinderclient-8.3.0/python_cinderclient.egg-info/pbr.json0000664000175000017500000000005600000000000024744 0ustar00zuulzuul00000000000000{"git_version": "ee59b68", "is_release": true}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793255.0 python-cinderclient-8.3.0/python_cinderclient.egg-info/requires.txt0000664000175000017500000000021200000000000025660 0ustar00zuulzuul00000000000000PrettyTable>=0.7.2 keystoneauth1>=4.3.1 oslo.i18n>=5.0.1 oslo.utils>=4.8.0 pbr>=5.5.0 requests>=2.25.1 simplejson>=3.5.1 stevedore>=3.3.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793255.0 python-cinderclient-8.3.0/python_cinderclient.egg-info/top_level.txt0000664000175000017500000000001500000000000026013 0ustar00zuulzuul00000000000000cinderclient ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.2578957 python-cinderclient-8.3.0/releasenotes/0000775000175000017500000000000000000000000020220 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.3018963 python-cinderclient-8.3.0/releasenotes/notes/0000775000175000017500000000000000000000000021350 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/add-generic-reset-state-command-d83v1f3accbf5807.yaml0000664000175000017500000000040700000000000032650 0ustar00zuulzuul00000000000000--- features: - Use 'cinder reset-state' as generic resource reset state command for resource 'volume', 'snapshot', 'backup' 'group' and 'group-snapshot'. Also change volume's default status from 'available' to none when no status is specified. ././@PaxHeader0000000000000000000000000000020700000000000011454 xustar0000000000000000113 path=python-cinderclient-8.3.0/releasenotes/notes/adding-option-is-public-to-type-list-9a16bd9c2b8eb65a.yaml 22 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/adding-option-is-public-to-type-list-9a16bd9c2b8eb65a.y0000664000175000017500000000066200000000000033116 0ustar00zuulzuul00000000000000--- upgrade: - | Adding ``is_public`` support in ``--filters`` option for ``type-list`` and ``group-type-list`` command. This option is used to filter volume types and group types on the basis of visibility. This option has 3 possible values : True, False, None with details as follows : * True: List public types only * False: List private types only * None: List both public and private types ././@PaxHeader0000000000000000000000000000020700000000000011454 xustar0000000000000000113 path=python-cinderclient-8.3.0/releasenotes/notes/attachment-create-optional-server-id-9299d9da2b62b263.yaml 22 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/attachment-create-optional-server-id-9299d9da2b62b263.y0000664000175000017500000000055100000000000033022 0ustar00zuulzuul00000000000000--- fixes: - | When attaching to a host, we don't need a server id so it shouldn't be mandatory to be supplied with attachment-create operation. The server_id parameter is made optional so we can create attachments without passing it. The backward compatibility is maintained so we can pass it like how we currently do if required.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/attachment-mode-8427aa6a2fa26e70.yaml0000664000175000017500000000032500000000000027516 0ustar00zuulzuul00000000000000--- features: - | Added the ability to specify the read-write or read-only mode of an attachment starting with microversion 3.54. The command line usage is `cinder attachment-create --mode [rw|ro]`. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/backup-user-id-059ccea871893a0b.yaml0000664000175000017500000000025500000000000027267 0ustar00zuulzuul00000000000000--- features: - | Starting with API microversion 3.56, ``backup-list`` and ``backup-show`` will include the ``User ID`` denoting the user that created the backup. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/bug-1608166-ad91a7a9f50e658a.yaml0000664000175000017500000000037700000000000026153 0ustar00zuulzuul00000000000000--- deprecations: - | The ``cinder endpoints`` command has been deprecated. This command performs an identity operation, and should now be handled by ``openstack catalog list``. [Bug `1608166 `_] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/bug-1675973-ad91a7a9f50e658a.yaml0000664000175000017500000000025200000000000026155 0ustar00zuulzuul00000000000000--- fixes: - The mountpoint argument was ignored when creating an attachment and now has been fixed. [Bug `1675973 `_] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/bug-1675974-34edd5g9870e65b2.yaml0000664000175000017500000000024600000000000026110 0ustar00zuulzuul00000000000000--- fixes: - The 'tenant' argument was ignored when listing attachments, and now has been fixed. [Bug `1675974 `_] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/bug-1675975-ad91a7a34e0esywc.yaml0000664000175000017500000000040700000000000026453 0ustar00zuulzuul00000000000000--- fixes: - The 'server_id' is now a required parameter when creating an attachment, that means we should create an attachment with a command like, 'cinder attachment-create '. [Bug `1675975 `_] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/bug-1705093-9bc782d44018c27d.yaml0000664000175000017500000000064000000000000026000 0ustar00zuulzuul00000000000000--- fixes: - | Fixes `bug 1705093`_ by having the ``cinderclient.client.get_highest_client_server_version`` method return a string rather than a float. The problem with returning a float is when a user of that method would cast the float result to a str which turns 3.40, for example, into "3.4" which is wrong. .. _bug 1705093: https://bugs.launchpad.net/python-cinderclient/+bug/1705093 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/bug-1713082-fb9276eed70f7e3b.yaml0000664000175000017500000000050700000000000026227 0ustar00zuulzuul00000000000000--- fixes: - | The attachment_ids in the volume info returned by show volume were incorrect. It was showing the volume_id, not the attachment_id. This fix changes the attachment_ids returned by show volume to correctly reflect the attachment_id. [Bug `1713082 `_] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/bug-1826286-c9b68709a0d63d06.yaml0000664000175000017500000000066500000000000026017 0ustar00zuulzuul00000000000000--- fixes: - | The ``discover_version`` function in the ``cinderclient.api_versions`` module was documented to return the most recent API version supported by both the client and the target Block Storage API endpoint, but it was not taking into account the highest API version supported by the client. Its behavior has been corrected in this release. [Bug `1826286 `_] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/bug-1867061-fix-py-raw-error-msg-ff3c6da0b01d5d6c.yaml0000664000175000017500000000040200000000000032176 0ustar00zuulzuul00000000000000--- fixes: - | `Bug #1867061 `_: Fixed raw Python error message when using ``cinder`` without a subcommand while passing an optional argument, such as ``--os-volume-api-version``.././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/bug-1915996-3aaa5e2548eb7c93.yaml0000664000175000017500000000031000000000000026145 0ustar00zuulzuul00000000000000--- fixes: - | `Bug #1915996 `_: Passing client certificates for mTLS connections was not supported and now has been fixed. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/cinder-poll-4f92694cc7eb657a.yaml0000664000175000017500000000021300000000000026674 0ustar00zuulzuul00000000000000features: - | Support to wait for volume creation until it completes. The command is: ``cinder create --poll `` ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/cinderclient-5-de0508ce5a221d21.yaml0000664000175000017500000000307500000000000027243 0ustar00zuulzuul00000000000000--- prelude: > This is a major version release of python-cinderclient. Backwards compatibility has been removed for some long standing deprecations and support for the Cinder v1 API has been removed. Prior to upgrading to this release, ensure all Cinder services that need to be managed are 13.0.0 (Rocky) or later. upgrade: - | This version of the python-cinderclient no longer supports the Cinder v1 API. Ensure all mananaged services have at least the v2 API available prior to upgrading this client. - | The ``cinder endpoints`` command was deprecated and has now been removed. The command ``openstack catalog list`` should be used instead. - | The ``cinder credentials`` command was deprecated and has now been removed. The command ``openstack token issue`` should be used instead. - | The use of ``--os_tenant_name``, ``--os_tenant_id`` and the environment variables ``OS_TENANT_NAME`` and ``OS_TENANT_ID`` have been deprecated for several releases and have now been removed. After upgrading, use the equivalent ``--os_project_name``, ``--os_project_id``, ``OS_PROJECT_NAME`` and ``OS_PROJECT_ID``. - | The deprecated volume create option ``--allow-multiattach`` has now been removed. Multiattach capability is now controlled using `volume-type extra specs `_. - | Support for the deprecated ``--sort_key`` and ``--sort_dir`` arguments have now been dropped. Use the supported ``--sort`` argument instead. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/cli-api-ver-negotiation-9f8fd8b77ae299fd.yaml0000664000175000017500000000064400000000000031300 0ustar00zuulzuul00000000000000--- features: - | Automatic version negotiation for the cinderclient CLI. If an API version is not specified, the CLI will use the newest supported by the client and the server. If an API version newer than the server supports is requested, the CLI will fall back to the newest version supported by the server and issue a warning message. This does not affect cinderclient library usage. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/cluster_commands-dca50e89c9d53cd2.yaml0000664000175000017500000000074000000000000030160 0ustar00zuulzuul00000000000000--- features: - Service listings will display additional "cluster" field when working with microversion 3.7 or higher. - Add clustered services commands to list -summary and detailed- (`cluster-list`), show (`cluster-show`), and update (`cluster-enable`, `cluster-disable`). Listing supports filtering by name, binary, disabled status, number of hosts, number of hosts that are down, and up/down status. These commands require API version 3.7 or higher. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/cluster_list_manageable-40c02489b2c95d55.yaml0000664000175000017500000000044000000000000031160 0ustar00zuulzuul00000000000000--- features: - | Cinder ``manageable-list`` and ``snapshot-manageable-list`` commands now accept ``--cluster`` argument to specify the backend we want to list for microversion 3.17 and higher. This argument and the ``host`` positional argument are mutually exclusive. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/cluster_migration_manage-31144d67bbfdb739.yaml0000664000175000017500000000044600000000000031520 0ustar00zuulzuul00000000000000--- features: - | Cinder migrate and manage commands now accept ``--cluster`` argument to define the destination for Active-Active deployments on microversion 3.16 and higher. This argument and the ``host`` positional argument are mutually exclusive for the migrate command. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/deprecate-allow-multiattach-2213a100c65a95c1.yaml0000664000175000017500000000023400000000000031650 0ustar00zuulzuul00000000000000--- deprecations: - | The ``--allow-multiattach`` flag on volume creation has now been marked deprecated and will be removed in a future release. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/do-not-reset-volume-status-ae8e28132d7bfacd.yaml0000664000175000017500000000026500000000000032040 0ustar00zuulzuul00000000000000--- fixes: - Default value of reset-state ``state`` option is changed from ``available`` to ``None`` because unexpected ``state`` reset happens when resetting migration status. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/drop-python2-support-d3a1bedc75445edc.yaml0000664000175000017500000000043600000000000030747 0ustar00zuulzuul00000000000000--- upgrade: - | Python 2.7 support has been dropped. Beginning with release 6.0.0, the minimum version of Python supported by python-cinderclient is Python 3.6. The last version of python-cinderclient to support Python 2.7 is the 5.x series from the Train release. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/drop-v2-support-e578ca21c7c6b532.yaml0000664000175000017500000000024700000000000027456 0ustar00zuulzuul00000000000000--- upgrade: - | This release drops support of the Block Storage API v2. The last version of the python-cinderclient supporting that API is the 7.x series. ././@PaxHeader0000000000000000000000000000020700000000000011454 xustar0000000000000000113 path=python-cinderclient-8.3.0/releasenotes/notes/enhance-backup-restore-shell-command-0cf55df6ca4b4c55.yaml 22 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/enhance-backup-restore-shell-command-0cf55df6ca4b4c55.y0000664000175000017500000000045200000000000033164 0ustar00zuulzuul00000000000000--- features: - | Enhance the ``backup-restore`` shell command to support restoring to a new volume created with a specific volume type and/or in a different AZ. New ``--volume-type`` and ``--availability-zone`` arguments are compatible with cinder API microversion v3.47 onward. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/feature-cross-az-backups-9d428ad4dfc552e1.yaml0000664000175000017500000000020000000000000031344 0ustar00zuulzuul00000000000000--- features: - | Support cross AZ backup creation specifying desired backup service AZ (added in microversion v3.51) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/list-with-count-78gtf45r66bf8912.yaml0000664000175000017500000000015100000000000027603 0ustar00zuulzuul00000000000000--- features: - Added ``with_count`` option in volume, snapshot and backup's list commands since 3.45. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/log-request-id-148c74d308bcaa14.yaml0000664000175000017500000000031100000000000027273 0ustar00zuulzuul00000000000000--- features: - Added support to log 'x-openstack-request-id' for each api call. Please refer, https://blueprints.launchpad.net/python-cinderclient/+spec/log-request-id for more details. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/messages-v3-api-3da81f4f66bf5903.yaml0000664000175000017500000000046100000000000027364 0ustar00zuulzuul00000000000000--- features: - | Add support for /messages API GET /messages cinder --os-volume-api-version 3.3 message-list GET /messages/{id} cinder --os-volume-api-version 3.3 message-show {id} DELETE /message/{id} cinder --os-volume-api-version 3.3 message-delete {id} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/noauth-7d95e5af31a00e96.yaml0000664000175000017500000000054500000000000025756 0ustar00zuulzuul00000000000000--- features: - | Cinderclient now supports noauth mode using `--os-auth-type noauth` param. Also python-cinderclient now supports keystoneauth1 plugins. deprecations: - | --bypass-url param is now deprecated. Please use --os-endpoint instead of it. --os-auth-system param is now deprecated. Please --os-auth-type instead of it. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/profile-as-environment-variable-2a5c666ef759e486.yaml0000664000175000017500000000024600000000000032605 0ustar00zuulzuul00000000000000--- features: - | ``--profile`` argument can be loaded from ``OS_PROFILE`` environment variable to avoid repeating ``--profile`` in openstack commands. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/project-default-types-727156d1db10a24d.yaml0000664000175000017500000000022700000000000030607 0ustar00zuulzuul00000000000000--- features: - | Added support to set, get, and unset the default volume type for projects with Block Storage API version 3.62 and higher. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/reimage-volume-fea3a1178662e65a.yaml0000664000175000017500000000074400000000000027374 0ustar00zuulzuul00000000000000--- features: - | A new ``cinder reimage`` command and related python API binding has been added which allows a user to replace the current content of a specified volume with the data of a specified image supplied by the Image service (Glance). (Note that this is a destructive action, that is, all data currently contained in the volume is destroyed when the volume is re-imaged.) This feature requires Block Storage API microversion 3.68 or greater. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/remove-cg-quota-9d4120b62f09cc5c.yaml0000664000175000017500000000011600000000000027455 0ustar00zuulzuul00000000000000--- other: - The useless consistencygroup quota operation has been removed. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/remove-credentials-e92b68e3bda80057.yaml0000664000175000017500000000033400000000000030244 0ustar00zuulzuul00000000000000--- other: - The cinder credentials command has not worked for several releases. The preferred alternative is to us the openstack token issue command, therefore the cinder credentials command has been removed. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/remove-deprecations-621919062f867015.yaml0000664000175000017500000000056700000000000030061 0ustar00zuulzuul00000000000000--- upgrade: - | The following CLI options were deprecated for one or more releases and have now been removed: ``--endpoint-type`` This option has been replaced by ``--os-endpoint-type``. ``--bypass-url`` This option has been replaced by ``--os-endpoint``. ``--os-auth-system`` This option has been replaced by ``--os-auth-type``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/remove-replv1-cabf2194edb9d963.yaml0000664000175000017500000000047100000000000027325 0ustar00zuulzuul00000000000000--- upgrade: - | The volume creation argument ``--source-replica`` on the command line and the ``source_replica`` kwarg for the ``create()`` call when using the cinderclient library were for the replication v1 support that was removed in the Mitaka release. These options have now been removed. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/remove-replv1-cli-61d5722438f888b6.yaml0000664000175000017500000000023400000000000027521 0ustar00zuulzuul00000000000000--- prelude: > The replication v1 have been removed from cinder, the volume promote/reenable replication on the command line have now been removed. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/replication-group-v3-api-022900ce6bf8feba.yaml0000664000175000017500000000026500000000000031345 0ustar00zuulzuul00000000000000--- features: - | Added support for replication group APIs ``enable_replication``, ``disable_replication``, ``failover_replication`` and ``list_replication_targets``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/return-request-id-to-caller-78d27f33f0048405.yaml0000664000175000017500000000055600000000000031521 0ustar00zuulzuul00000000000000--- features: - | Added support to return "x-openstack-request-id" header in request_ids attribute for better tracing. For example:: >>> from cinderclient import client >>> cinder = client.Client('2', $OS_USER_NAME, $OS_PASSWORD, $OS_TENANT_NAME, $OS_AUTH_URL) >>> res = cinder.volumes.list() >>> res.request_ids././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/revert-to-snapshot-er4598df88aq5918.yaml0000664000175000017500000000010400000000000030314 0ustar00zuulzuul00000000000000--- features: - Added support for the revert-to-snapshot feature. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/service_cleanup_cmd-cac85b697bc22af1.yaml0000664000175000017500000000026600000000000030604 0ustar00zuulzuul00000000000000--- features: - | New ``work-cleanup`` command to trigger server cleanups by other nodes within a cluster on Active-Active deployments on microversion 3.24 and higher. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/service_dynamic_log-bd81d93c73fc1570.yaml0000664000175000017500000000027700000000000030466 0ustar00zuulzuul00000000000000--- features: - | Support microversion 3.32 that allows dynamically changing and querying Cinder services' log levels with ``service-set-log`` and ``service-get-log`` commands. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/start-using-reno-18001103a6719c13.yaml0000664000175000017500000000007100000000000027345 0ustar00zuulzuul00000000000000--- other: - Start using reno to manage release notes. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/support---os-key-option-72ba2fd4880736ac.yaml0000664000175000017500000000023400000000000031031 0ustar00zuulzuul00000000000000--- features: - | Support --os-key option and OS_KEY environment variable which allows to provide client cert and its private key separately. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/support-bs-mv-3.60-a65f1919b5068d17.yaml0000664000175000017500000000111300000000000027436 0ustar00zuulzuul00000000000000--- features: - | When communicating with the Block Storage API version 3.60 and higher, you can apply time comparison filtering to the volume list command on the ``created_at`` or ``updated_at`` fields. Time must be expressed in ISO 8601 format: CCYY-MM-DDThh:mm:ss±hh:mm. The ±hh:mm value, if included, returns the time zone as an offset from UTC. See the `Block Storage service (cinder) command-line client `_ documentation for usage details. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/support-bs-mv-3.66-5214deb20d164056.yaml0000664000175000017500000000067500000000000027434 0ustar00zuulzuul00000000000000--- features: - | Adds support for Block Storage API version 3.66, which drops the requirement of a 'force' flag to create a snapshot of an in-use volume. Although the 'force' flag is invalid for the ``snapshot-create`` call for API versions 3.66 and higher, for backward compatibility the cinderclient follows the Block Storage API in silently ignoring the flag when it is passed with a value that evaluates to True. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/support-create-volume-from-backup-c4e8aac89uy18uy2.yaml0000664000175000017500000000012300000000000033474 0ustar00zuulzuul00000000000000--- features: - | Support create volume from backup in microversion v3.47. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/support-filter-type-7yt69ub7ccbf7419.yaml0000664000175000017500000000025000000000000030670 0ustar00zuulzuul00000000000000--- features: - New command option ``--filters`` is added to ``type-list`` command to support filter types since 3.52, and it's only valid for administrator. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/support-filters-transfer-a1e7b728c7895a45.yaml0000664000175000017500000000037000000000000031410 0ustar00zuulzuul00000000000000--- features: - New command option ``--filters`` is added to ``transfer-list`` command to support filtering. The ``transfer-list`` command can be used with filters when communicating with the Block Storage API version 3.52 and higher.././@PaxHeader0000000000000000000000000000020700000000000011454 xustar0000000000000000113 path=python-cinderclient-8.3.0/releasenotes/notes/support-generialized-resource-filter-8yf6w23f66bf5903.yaml 22 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/support-generialized-resource-filter-8yf6w23f66bf5903.y0000664000175000017500000000046200000000000033331 0ustar00zuulzuul00000000000000--- features: - | Added new command ``list-filters`` to retrieve enabled resource filters, Added new option ``--filters`` to these list commands: - list - snapshot-list - backup-list - group-list - group-snapshot-list - attachment-list - message-list - get-pools ././@PaxHeader0000000000000000000000000000020500000000000011452 xustar0000000000000000111 path=python-cinderclient-8.3.0/releasenotes/notes/support-keystone-v3-for-httpClient-d48ebb24880f5821.yaml 22 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/support-keystone-v3-for-httpClient-d48ebb24880f5821.yam0000664000175000017500000000011500000000000033020 0ustar00zuulzuul00000000000000--- features: - | Support Keystone V3 authentication for httpClient. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/support-like-filter-7434w23f66bf5587.yaml0000664000175000017500000000032100000000000030267 0ustar00zuulzuul00000000000000--- features: - | Enabled like filter support in these list commands. - list - snapshot-list - backup-list - group-list - group-snapshot-list - attachment-list - message-list ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/support-show-group-with-volume-ad820b8442e8a9e8.yaml0000664000175000017500000000021600000000000032561 0ustar00zuulzuul00000000000000--- features: - | Support show group with ``list-volume`` argument. The command is : cinder group-show {group_id} --list-volume ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/support-volume-summary-d6d5bb2acfef6ad5.yaml0000664000175000017500000000010700000000000031541 0ustar00zuulzuul00000000000000--- features: - | Support get volume summary command in V3.12. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/transfer-snapshots-555c61477835bcf7.yaml0000664000175000017500000000040300000000000030167 0ustar00zuulzuul00000000000000--- features: - | Starting with microversion 3.55, the volume transfer command now has the ability to exclude a volume's snapshots when transferring a volume to another project. The new command format is `cinder transfer-create --no-snapshots`. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/transfer-sort-ca622e9b8da605c1.yaml0000664000175000017500000000056600000000000027343 0ustar00zuulzuul00000000000000--- features: - | Starting with microversion 3.59, the ``cinder transfer-list`` command now supports the ``--sort`` argument to sort the returned results. This argument takes either just the attribute to sort on, or the attribute and the sort direction. Examples include ``cinder transfer-list --sort=id`` and ``cinder transfer-list --sort=name:asc``. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/ussuri-release-f0ebfc54cdac6680.yaml0000664000175000017500000000241000000000000027634 0ustar00zuulzuul00000000000000--- prelude: | The Ussuri release of the python-cinderclient supports Block Storage API version 2 and Block Storage API version 3 through microversion 3.60. (The maximum microversion of the Block Storage API in the Ussuri release is 3.60.) In addition to the features and bugfixes described below, this release includes some documentation updates. Note that this release corresponds to a major bump in the version number. See the "Upgrade Notes" section of this document for details. Please keep in mind that the minimum version of Python supported by this release is Python 3.6. upgrade: - | The ``--bypass-url`` command line argument, having been deprecated in version 2.10, was removed in version 4.0.0. It was replaced by the command line argument ``--os-endpoint`` for consistency with other OpenStack clients. In this release, the initializer functions for client objects no longer recognize ``bypass_url`` as a parameter name. Instead, use ``os_endpoint``. This keeps the cinderclient consistent both internally and with respect to other OpenStack clients. fixes: - | Fixed an issue where the ``os_endpoint`` was not being passed to the keystone session as the ``endpoint_override`` argument. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/victoria-release-0d9c2b43845c3d9e.yaml0000664000175000017500000000066100000000000027716 0ustar00zuulzuul00000000000000--- prelude: | The Victoria release of the python-cinderclient supports Block Storage API version 2 and Block Storage API version 3 through microversion 3.62. (The maximum microversion of the Block Storage API in the Victoria release is 3.62.) features: - | Added support to display the ``cluster_name`` attribute in volume detail output for admin users with Block Storage API version 3.61 and higher. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/volume-transfer-bug-23c760efb9f98a4d.yaml0000664000175000017500000000053600000000000030454 0ustar00zuulzuul00000000000000--- fixes: - | An issue was discovered with the way API microversions were handled for the new volume-transfer with snapshot handling with microversion 3.55. This release includes a fix to keep backwards compatibility with earlier releases. See `bug #1784703 `_ for more details. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/wallaby-release-2535df50cc307fea.yaml0000664000175000017500000000115100000000000027572 0ustar00zuulzuul00000000000000--- prelude: | The Wallaby release of the python-cinderclient supports Block Storage API version 2 and Block Storage API version 3 through microversion 3.64. (The maximum microversion of the Block Storage API in the Wallaby release is 3.64.) features: - | Added support to display the ``volume_type_id`` attribute in volume detail output when used with Block Storage API microversion 3.63 and higher. - | Added support to display the ``encryption_key_id`` attribute in volume detail and backup detail output when used with Block Storage API microversion 3.64 and higher. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/xena-release-688918a69ada3a58.yaml0000664000175000017500000000166300000000000026767 0ustar00zuulzuul00000000000000--- prelude: | The Xena release of the python-cinderclient supports Block Storage API version 3 through microversion 3.66. (The maximum microversion of the Block Storage API in the Xena release is 3.66.) upgrade: - | The python-cinderclient no longer supports version 2 of the Block Storage API. The last version of the python-cinderclient supporting that API is the 7.x series. features: - | Supports Block Storage API version 3.65, which displays a boolean ``consumes_quota`` field on volume and snapshot detail responses and which allows filtering volume and snapshot list responses using the standard ``--filters [ [ ...]]`` option to the ``cinder list`` or ``cinder snapshot-list`` commands. Filtering by this field may not always be possible in a cloud. Use the ``cinder list-filters`` command to see what filters are available in the cloud you are using. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/notes/yoga-release-dcd35c98f6be478e.yaml0000664000175000017500000000033100000000000027205 0ustar00zuulzuul00000000000000--- prelude: | The Yoga release of the python-cinderclient supports Block Storage API version 3 through microversion 3.68. (The maximum microversion of the Block Storage API in the Yoga release is 3.68.) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.3018963 python-cinderclient-8.3.0/releasenotes/source/0000775000175000017500000000000000000000000021520 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.3018963 python-cinderclient-8.3.0/releasenotes/source/_static/0000775000175000017500000000000000000000000023146 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/_static/.placeholder0000664000175000017500000000000000000000000025417 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.3018963 python-cinderclient-8.3.0/releasenotes/source/_templates/0000775000175000017500000000000000000000000023655 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/_templates/.placeholder0000664000175000017500000000000000000000000026126 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/conf.py0000664000175000017500000002146400000000000023026 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # Cinder Client Release Notes documentation build configuration file, # created by sphinx-quickstart on Tue Nov 4 17:02:44 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Cinder Client Release Notes' openstackdocs_auto_name = False copyright = '2015, Cinder Developers' # Release notes are version independent, no need to set version and release release = '' version = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # There are two options for replacing |today|: either, you set today to some # non-false value, then it is used: # today = '' # Else, today_fmt is used as the format for a strftime call. # today_fmt = '%B %d, %Y' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'CinderClientReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, # author, documentclass [howto, manual, or own class]). latex_documents = [ ('index', 'CinderClientReleaseNotes.tex', 'Cinder Client Release Notes Documentation', 'Cinder Developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output --------------------------------------- # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'cinderclientreleasenotes', 'Cinder Client Release Notes Documentation', ['Cinder Developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'CinderClientReleaseNotes', 'Cinder Client Release Notes Documentation', 'Cinder Developers', 'CinderClientReleaseNotes', 'Block Storage Service client.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] # -- Options for openstackdocstheme ------------------------------------------- openstackdocs_repo_name = 'openstack/python-cinderclient' openstackdocs_bug_project = 'cinderclient' openstackdocs_bug_tag = '' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/index.rst0000664000175000017500000000037100000000000023362 0ustar00zuulzuul00000000000000============================= Cinder Client Release Notes ============================= .. toctree:: :maxdepth: 1 unreleased xena wallaby victoria ussuri train stein rocky queens pike ocata newton mitaka ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/mitaka.rst0000664000175000017500000000023200000000000023515 0ustar00zuulzuul00000000000000=================================== Mitaka Series Release Notes =================================== .. release-notes:: :branch: origin/stable/mitaka ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/newton.rst0000664000175000017500000000023200000000000023561 0ustar00zuulzuul00000000000000=================================== Newton Series Release Notes =================================== .. release-notes:: :branch: origin/stable/newton ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/ocata.rst0000664000175000017500000000023000000000000023334 0ustar00zuulzuul00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/pike.rst0000664000175000017500000000021700000000000023202 0ustar00zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/queens.rst0000664000175000017500000000022300000000000023547 0ustar00zuulzuul00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/rocky.rst0000664000175000017500000000022100000000000023374 0ustar00zuulzuul00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/stein.rst0000664000175000017500000000022100000000000023367 0ustar00zuulzuul00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/train.rst0000664000175000017500000000020300000000000023362 0ustar00zuulzuul00000000000000============================ Train Series Release Notes ============================ .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/unreleased.rst0000664000175000017500000000016000000000000024376 0ustar00zuulzuul00000000000000============================== Current Series Release Notes ============================== .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/ussuri.rst0000664000175000017500000000020200000000000023576 0ustar00zuulzuul00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/victoria.rst0000664000175000017500000000021200000000000024065 0ustar00zuulzuul00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: stable/victoria ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/wallaby.rst0000664000175000017500000000020600000000000023703 0ustar00zuulzuul00000000000000============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: stable/wallaby ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/releasenotes/source/xena.rst0000664000175000017500000000017200000000000023205 0ustar00zuulzuul00000000000000========================= Xena Series Release Notes ========================= .. release-notes:: :branch: stable/xena ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/requirements.txt0000664000175000017500000000066600000000000021023 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. pbr>=5.5.0 # Apache-2.0 PrettyTable>=0.7.2 # BSD keystoneauth1>=4.3.1 # Apache-2.0 simplejson>=3.5.1 # MIT oslo.i18n>=5.0.1 # Apache-2.0 oslo.utils>=4.8.0 # Apache-2.0 requests>=2.25.1 # Apache-2.0 stevedore>=3.3.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.3058963 python-cinderclient-8.3.0/setup.cfg0000664000175000017500000000205200000000000017347 0ustar00zuulzuul00000000000000[metadata] name = python-cinderclient summary = OpenStack Block Storage API Client Library description_file = README.rst author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/python-cinderclient/latest/ python_requires = >=3.6 classifier = Development Status :: 5 - Production/Stable Environment :: Console 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 :: Only Programming Language :: Python :: 3 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [files] packages = cinderclient [entry_points] console_scripts = cinder = cinderclient.shell:main keystoneauth1.plugin = noauth = cinderclient.contrib.noauth:CinderNoAuthLoader [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/setup.py0000664000175000017500000000127100000000000017242 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import setuptools setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/test-requirements.txt0000664000175000017500000000107100000000000021767 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. # Hacking already pins down pep8, pyflakes and flake8 hacking>=4.0.0,<4.1.0 # Apache-2.0 flake8-import-order # LGPLv3 docutils>=0.16 coverage>=5.5 # Apache-2.0 ddt>=1.4.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.2.0 # Apache-2.0 testtools>=2.4.0 # MIT stestr>=3.1.0 # Apache-2.0 oslo.serialization>=4.1.0 # Apache-2.0 doc8>=0.8.1 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1645793255.3058963 python-cinderclient-8.3.0/tools/0000775000175000017500000000000000000000000016667 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/tools/cinder.bash_completion0000664000175000017500000000171500000000000023227 0ustar00zuulzuul00000000000000_cinder_opts="" # lazy init _cinder_flags="" # lazy init _cinder_opts_exp="" # lazy init _cinder() { local cur prev cbc cflags COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" if [ "x$_cinder_opts" == "x" ] ; then cbc="`cinder bash-completion 2>/dev/null | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`" _cinder_opts="`echo "$cbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`" _cinder_flags="`echo " $cbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`" fi if [[ "$prev" != "help" ]] ; then COMPLETION_CACHE=~/.cache/cinderclient/*/*-cache cflags="$_cinder_flags $_cinder_opts "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) else COMPREPLY=($(compgen -W "${_cinder_opts}" -- ${cur})) fi return 0 } complete -F _cinder cinder ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/tools/generate_authors.sh0000775000175000017500000000005100000000000022561 0ustar00zuulzuul00000000000000#!/bin/bash git shortlog -se | cut -c8- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/tools/lintstack.py0000775000175000017500000001561400000000000021247 0ustar00zuulzuul00000000000000#!/usr/bin/env python3 # Copyright (c) 2013, AT&T Labs, Yun Mao # 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. """pylint error checking.""" from io import StringIO import json import re import sys from pylint import lint from pylint.reporters import text ignore_codes = [ # Note(maoy): E1103 is error code related to partial type inference "E1103" ] ignore_messages = [ # Note(fengqian): this message is the pattern of [E0611]. "No name 'urllib' in module '_MovedItems'", # Note(xyang): these error messages are for the code [E1101]. # They should be ignored because 'sha256' and 'sha224' are functions in # 'hashlib'. "Module 'hashlib' has no 'sha256' member", "Module 'hashlib' has no 'sha224' member", # six.moves "Instance of '_MovedItems' has no 'builtins' member", # This error message is for code [E1101] "Instance of 'ResourceFilterManager' has no '_list' member", ] ignore_modules = ["cinderclient/tests/"] KNOWN_PYLINT_EXCEPTIONS_FILE = "tools/pylint_exceptions" class LintOutput(object): _cached_filename = None _cached_content = None def __init__(self, filename, lineno, line_content, code, message, lintoutput): self.filename = filename self.lineno = lineno self.line_content = line_content self.code = code self.message = message self.lintoutput = lintoutput @classmethod def from_line(cls, line): m = re.search(r"(\S+):(\d+): \[(\S+)(, \S+)?] (.*)", line) if m is None: return None matched = m.groups() filename, lineno, code, message = (matched[0], int(matched[1]), matched[2], matched[-1]) if cls._cached_filename != filename: with open(filename) as f: cls._cached_content = list(f.readlines()) cls._cached_filename = filename line_content = cls._cached_content[lineno - 1].rstrip() return cls(filename, lineno, line_content, code, message, line.rstrip()) @classmethod def from_msg_to_dict(cls, msg): """Convert pylint output to a dict. From the output of pylint msg, to a dict, where each key is a unique error identifier, value is a list of LintOutput """ result = {} for line in msg.splitlines(): obj = cls.from_line(line) if obj is None or obj.is_ignored(): continue key = obj.key() if key not in result: result[key] = [] result[key].append(obj) return result def is_ignored(self): if self.code in ignore_codes: return True if any(self.filename.startswith(name) for name in ignore_modules): return True if any(msg in self.message for msg in ignore_messages): return True return False def key(self): if self.code in ["E1101", "E1103"]: # These two types of errors are like Foo class has no member bar. # We discard the source code so that the error will be ignored # next time another Foo.bar is encountered. return self.message, "" return self.message, self.line_content.strip() def json(self): return json.dumps(self.__dict__) def review_str(self): return ("File %(filename)s\nLine %(lineno)d:%(line_content)s\n" "%(code)s: %(message)s" % {'filename': self.filename, 'lineno': self.lineno, 'line_content': self.line_content, 'code': self.code, 'message': self.message}) class ErrorKeys(object): @classmethod def print_json(cls, errors, output=sys.stdout): print("# automatically generated by tools/lintstack.py", file=output) for i in sorted(errors.keys()): print(json.dumps(i), file=output) @classmethod def from_file(cls, filename): keys = set() for line in open(filename): if line and line[0] != "#": d = json.loads(line) keys.add(tuple(d)) return keys def run_pylint(): buff = StringIO() reporter = text.TextReporter(output=buff) args = [ "--msg-template='{path}:{line}: [{msg_id}({symbol}), {obj}] {msg}'", "-E", "cinderclient"] lint.Run(args, reporter=reporter, do_exit=False) val = buff.getvalue() buff.close() return val def generate_error_keys(msg=None): print("Generating", KNOWN_PYLINT_EXCEPTIONS_FILE) if msg is None: msg = run_pylint() errors = LintOutput.from_msg_to_dict(msg) with open(KNOWN_PYLINT_EXCEPTIONS_FILE, "w") as f: ErrorKeys.print_json(errors, output=f) def validate(newmsg=None): print("Loading", KNOWN_PYLINT_EXCEPTIONS_FILE) known = ErrorKeys.from_file(KNOWN_PYLINT_EXCEPTIONS_FILE) if newmsg is None: print("Running pylint. Be patient...") newmsg = run_pylint() errors = LintOutput.from_msg_to_dict(newmsg) print("Unique errors reported by pylint: was %d, now %d." % (len(known), len(errors))) passed = True for err_key, err_list in errors.items(): for err in err_list: if err_key not in known: print(err.lintoutput) print() passed = False if passed: print("Congrats! pylint check passed.") redundant = known - set(errors.keys()) if redundant: print("Extra credit: some known pylint exceptions disappeared.") for i in sorted(redundant): print(json.dumps(i)) print("Consider regenerating the exception file if you will.") else: print("Please fix the errors above. If you believe they are false " "positives, run 'tools/lintstack.py generate' to overwrite.") sys.exit(1) def usage(): print("""Usage: tools/lintstack.py [generate|validate] To generate pylint_exceptions file: tools/lintstack.py generate To validate the current commit: tools/lintstack.py """) def main(): option = "validate" if len(sys.argv) > 1: option = sys.argv[1] if option == "generate": generate_error_keys() elif option == "validate": validate() else: usage() if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/tools/lintstack.sh0000775000175000017500000000420600000000000021224 0ustar00zuulzuul00000000000000#!/usr/bin/env bash # Copyright (c) 2012-2013, AT&T Labs, Yun Mao # 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. # Use lintstack.py to compare pylint errors. # We run pylint twice, once on HEAD, once on the code before the latest # commit for review. set -e TOOLS_DIR=$(cd $(dirname "$0") && pwd) # Get the current branch name. GITHEAD=`git rev-parse --abbrev-ref HEAD` if [[ "$GITHEAD" == "HEAD" ]]; then # In detached head mode, get revision number instead GITHEAD=`git rev-parse HEAD` echo "Currently we are at commit $GITHEAD" else echo "Currently we are at branch $GITHEAD" fi cp -f $TOOLS_DIR/lintstack.py $TOOLS_DIR/lintstack.head.py if git rev-parse HEAD^2 2>/dev/null; then # The HEAD is a Merge commit. Here, the patch to review is # HEAD^2, the master branch is at HEAD^1, and the patch was # written based on HEAD^2~1. PREV_COMMIT=`git rev-parse HEAD^2~1` git checkout HEAD~1 # The git merge is necessary for reviews with a series of patches. # If not, this is a no-op so won't hurt either. git merge $PREV_COMMIT else # The HEAD is not a merge commit. This won't happen on gerrit. # Most likely you are running against your own patch locally. # We assume the patch to examine is HEAD, and we compare it against # HEAD~1 git checkout HEAD~1 fi # First generate tools/pylint_exceptions from HEAD~1 $TOOLS_DIR/lintstack.head.py generate # Then use that as a reference to compare against HEAD git checkout $GITHEAD $TOOLS_DIR/lintstack.head.py echo "Check passed. FYI: the pylint exceptions are:" cat $TOOLS_DIR/pylint_exceptions ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1645793225.0 python-cinderclient-8.3.0/tox.ini0000664000175000017500000000664300000000000017053 0ustar00zuulzuul00000000000000[tox] distribute = False envlist = py3,pep8 minversion = 3.18.0 skipsdist = True # this allows tox to infer the base python from the environment name # and override any basepython configured in this file ignore_basepython_conflict=true [testenv] basepython = python3 usedevelop = True setenv = VIRTUAL_ENV={envdir} OS_TEST_PATH=./cinderclient/tests/unit OS_STDOUT_CAPTURE=1 OS_STDERR_CAPTURE=1 OS_TEST_TIMEOUT=60 passenv = *_proxy *_PROXY deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = find . -type f -name "*.pyc" -delete stestr run {posargs} stestr slowest allowlist_externals = find [testenv:pep8] commands = flake8 doc8 [testenv:pylint] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt pylint==2.6.0 commands = bash tools/lintstack.sh allowlist_externals = bash [testenv:venv] commands = {posargs} [testenv:cover] setenv = {[testenv]setenv} PYTHON=coverage run --source cinderclient --parallel-mode commands = stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml [testenv:docs] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html [testenv:pdf-docs] deps = {[testenv:docs]deps} commands = {[testenv:docs]commands} sphinx-build -W -b latex doc/source doc/build/pdf make -C doc/build/pdf allowlist_externals = make cp [testenv:releasenotes] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html [testenv:functional] deps = {[testenv]deps} tempest>=26.0.0 commands = stestr run {posargs} setenv = {[testenv]setenv} OS_TEST_PATH = ./cinderclient/tests/functional OS_VOLUME_API_VERSION = 3 # must define this here so it can be inherited by the -py3* environments OS_CINDERCLIENT_EXEC_DIR = {envdir}/bin # Our functional tests contain their own timeout handling, so # turn off the timeout handling provided by the # tempest.lib.base.BaseTestCase that our ClientTestBase class # inherits from. OS_TEST_TIMEOUT=0 # The OS_CACERT environment variable should be passed to the test # environments to specify a CA bundle file to use in verifying a # TLS (https) server certificate. passenv = OS_* [testenv:functional-py36] deps = {[testenv:functional]deps} setenv = {[testenv:functional]setenv} passenv = {[testenv:functional]passenv} commands = {[testenv:functional]commands} [testenv:functional-py39] deps = {[testenv:functional]deps} setenv = {[testenv:functional]setenv} passenv = {[testenv:functional]passenv} commands = {[testenv:functional]commands} [flake8] show-source = True ignore = H404,H405,E122,E123,E128,E251,W503,W504 exclude = .venv,.git,.tox,dist,doc,*lib/python*,*egg,build application-import-names = cinderclient import-order-style = pep8 [doc8] ignore-path=.tox,*.egg-info,doc/src/api,doc/source/drivers.rst,doc/build,.eggs/*/EGG-INFO/*.txt,doc/source/configuration/tables,./*.txt,releasenotes/build,doc/source/cli/details.rst extension=.txt,.rst,.inc