././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1688740210.094164 python-glanceclient-4.4.0/0000775000175000017500000000000000000000000015511 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/.coveragerc0000664000175000017500000000011100000000000017623 0ustar00zuulzuul00000000000000[run] branch = True source = glanceclient [report] ignore_errors = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/.mailmap0000664000175000017500000000030100000000000017124 0ustar00zuulzuul00000000000000# "man git-shortlog" for reference David Koo ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/.stestr.conf0000664000175000017500000000011300000000000017755 0ustar00zuulzuul00000000000000[DEFAULT] test_path=${OS_TEST_PATH:-./glanceclient/tests/unit} top_path=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/.zuul.yaml0000664000175000017500000000655100000000000017461 0ustar00zuulzuul00000000000000- job: name: glanceclient-dsvm-functional parent: devstack-tox-functional description: | Devstack-based functional tests for glanceclient. These test glanceclient against Image API v2 only. required-projects: - openstack/python-glanceclient timeout: 4200 vars: devstack_localrc: LIBS_FROM_GIT: python-glanceclient devstack_services: # turn off ceilometer ceilometer-acentral: false ceilometer-acompute: false ceilometer-alarm-evaluator: false ceilometer-alarm-notifier: false ceilometer-anotification: false ceilometer-api: false ceilometer-collector: false # turn on swift s-account: true s-container: true s-object: true s-proxy: true # Hardcode glanceclient path so the job can be run on glance patches zuul_work_dir: src/opendev.org/openstack/python-glanceclient irrelevant-files: - ^doc/.*$ - ^releasenotes/.*$ - ^.*\.rst$ - ^(test-|)requirements.txt$ - ^setup.cfg$ - job: name: glanceclient-tox-keystone-tips-base parent: tox abstract: true description: Abstract job for glanceclient vs. keystone required-projects: - name: openstack/keystoneauth - job: name: glanceclient-tox-py3-keystone-tips parent: glanceclient-tox-keystone-tips-base description: | glanceclient py3 unit tests vs. keystone masters vars: tox_envlist: py3 - job: name: glanceclient-tox-oslo-tips-base parent: tox abstract: true description: Abstract job for glanceclient vs. oslo required-projects: - name: openstack/oslo.i18n - name: openstack/oslo.utils - job: name: glanceclient-tox-py3-oslo-tips parent: glanceclient-tox-oslo-tips-base description: | glanceclient py3 unit tests vs. oslo masters vars: tox_envlist: py3 - job: name: glanceclient-dsvm-functional-py3 parent: glanceclient-dsvm-functional vars: devstack_localrc: USE_PYTHON3: true - project: templates: - check-requirements - lib-forward-testing-python3 - openstack-cover-jobs - openstack-python3-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: jobs: - glanceclient-dsvm-functional gate: jobs: - glanceclient-dsvm-functional periodic: jobs: # NOTE(rosmaita): we only want the "tips" jobs to be run against # master, hence the 'branches' qualifiers below. Without them, when # a stable branch is cut, the tests would be run against the stable # branch as well, which is pointless because these libraries are # frozen (more or less) in the stable branches. # # The "tips" jobs can be removed from the stable branch .zuul.yaml # files if someone is so inclined, but that would require manual # maintenance, so we do not do it by default. Another option is # to define these jobs in the openstack/project-config repo. # That would make us less agile in adjusting these tests, so we # aren't doing that either. - glanceclient-tox-py3-keystone-tips: branches: master - glanceclient-tox-py3-oslo-tips: branches: master experimental: jobs: - glanceclient-dsvm-functional-py3 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740209.0 python-glanceclient-4.4.0/AUTHORS0000664000175000017500000002527300000000000016572 0ustar00zuulzuul0000000000000098k <18552437190@163.com> Abhishek Kekane Abhishek Kekane Abhishek Talwar Abijitha Nadagouda Adam Gandelman Alan Meadows Alessandro Pilotti Alessio Ababilov Alex Gaynor Alex Meade Alex Schultz Alexander Bashmakov Alexander Tivelkov Alexey Galkin AmalaBasha Andre Naehring Andreas Jaeger Andreas Jaeger Andrew Laski Andrey Kurilin Andy Botting Andy McCrae Anita Kuno Ankit Agrawal Atsushi SAKAI Benedikt Loeffler Bhuvan Arumugam Bob Thyne Boris Pavlovic Brad Pokorny Brian Lamar Brian Rosmaita Brian Rosmaita Brian Waldon Cao ShuFeng Cao Xuan Hoang Chang Bo Guo ChangBo Guo(gcb) Chaozhe.Chen Chen Chen Hanxiao Chris Behrens Chris Buccella Chris Yeoh Christian Berendt Chuck Short Cindy Pallares Clark Boylan Corey Bryant Cyril Roelandt Cyril Roelandt Cyril Roelandt Dan Prince Dan Smith Daniel Bengtsson Danny Al-Gaaf Dao Cong Tien Darja Shakhray Davanum Srinivas Davanum Srinivas David Edery David Koo David Peraza David Sariel David Wittman Dazhao Dean Troyer Diego Parrilla Dirk Mueller Dominik Heidler Doug Hellmann Doug Hellmann Dougal Matthews Edward Hope-Morley Eiichi Aikawa Eric Fried Eric Harney Erno Kuvaja Erno Kuvaja Evgeny Antyshev Fei Long Wang Fei Long Wang Flaper Fesp Flavio Percoco Flavio Percoco Flavio Percoco Florian Haas Frederic Lepied Frode Nordahl Gabe Westmaas Gabriel Hurley Georges Dubus Ghanshyam Ghanshyam Mann Ghe Rivero Gorka Eguileor Haikel Guemar Hangdong Zhang Hervé Beraud Hugh Saunders Ian Cordasco Ian Cordasco Ian Wienand Ihar Hrachyshka Itisha Dewan Jack Ding Jake Yip Jakub Ruzicka James E. Blair James E. Blair James Li James Page Jamie Lennox Jared Culp Javeme Javier Pena Jay Pipes Jeremy Stanley Jimmy McCrory Joe Gordon John Bresnahan John Trowbridge Jon Bernard Juan Manuel Olle Justin Santa Barbara KATO Tomoyuki Kamil Rykowski Ken'ichi Ohmichi Kevin McDonald Kirill Kyrylo Romanenko Lakshmi N Sampath Lars Gellrich Le Tian Ren Li Wei Liang Fang LiuNanke Long Quan Sha Longgeek Louis Taylor Louis Taylor Lucian Petrut Luong Anh Tuan M V P Nitesh Maithem Manuel Desbonnet Mark J. Washenberger Mark McLoughlin Markus Zoeller Matt Riedemann Matthew Booth MattieuPuel Michael Basnight Michael Still Michal Dulko Mike Fedosin Monty Taylor Mridula Joshi Nguyen Hai Niall Bunting NiallBunting NiallBunting Nicolas Simonds Nikhil Komawar Nikhil Komawar Noboru arai Nobuto Murata Oleksii Chuprykov Ondřej Nový OpenStack Release Bot Pawel Koniszewski Pranali Deore PranaliD Rajat Dhasmana Rakesh H S Ravi Jethani Ravi Shekhar Jethani Rob Crittenden Robert Collins Rui Chen Rui Yuan Dou Russell Bryant Sabari Kumar Murugesan Sascha Peilicke Sean Dague Sean Dague Sean Dague Sean McGinnis Sean McGinnis Shane Wang Shu Muto Shuquan Huang Sirushti Murugesan Stanislaw Pitucha Stephen Finucane Steve Lewis Steve Martinelli Steve Martinelli Stuart McLaren Sudipta Biswas Sulochan Acharya Sushil Kumar Swapnil Kulkarni (coolsvap) Takashi Kajinami Takashi NATSUME Takeaki Matsumoto Tatyana Leontovich Thierry Carrez Thomas Goirand Thomas Leaman Tin Lam Tom Cocozzello Tom Leaman Tovin Seven Travis Tripp Unmesh Gurjar Van Hung Pham Venkatesh Sampath Victor Morales Vincent Untz Vishvananda Ishaya Wander Way Wayne Okuma Wu Wenxiang XinxinShen Yamini Sardana Yang Yu Yassine Lamgarchal Yvonne Stachowski Zhenguo Niu Zhi Yan Liu ZhiQiang Fan Zhiqiang Fan amalaba bhagyashris caishan ckonstanski d34dh0r53 dineshbhor eddie-sheffield fuzihao haobing1 huang.zhiping iccha-sethi iccha.sethi imacdonn isethi jacky06 jayonlau jaypipes ji-xuepeng jinyuanliu kairat_kushaev lijunbo liuqing llg8212 lrqrun lvxianguo m.benchchaoui@cloudbau.de mouad benchchaoui pawnesh.kumar pengyuesheng qingszhao rahulram rico.lin ricolin ricolin root shu-mutou sonu.kumar sridhargaddam sunjia venkatamahesh vihithad23 wanghong wangqi wangxiyuan wangxiyuan wangzhenyu whoami-rajat ya.wang yangds yatin yatin karel zhangbailin zheng yin zhengyao1 zwei 翟小君 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/CONTRIBUTING.rst0000664000175000017500000000107600000000000020156 0ustar00zuulzuul00000000000000If you would like to contribute to the development of OpenStack, you must follow the steps documented at: https://docs.openstack.org/infra/manual/developers.html#development-workflow Once those steps have been completed, changes to OpenStack should be submitted for review via the Gerrit tool, following the workflow documented at: https://docs.openstack.org/infra/manual/developers.html#development-workflow Pull requests submitted through GitHub will be ignored. Bugs should be filed on Launchpad, not GitHub: https://bugs.launchpad.net/python-glanceclient ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740209.0 python-glanceclient-4.4.0/ChangeLog0000664000175000017500000011452500000000000017273 0ustar00zuulzuul00000000000000CHANGES ======= 4.4.0 ----- * Release notes for 4.4.0 * Docs generation: mock the six module for autodoc * Bump the CHUNKSIZE to use CPU more efficiently * do\_image\_import: always pass remote\_\* to gc.images.image\_import * Tox4: remove skipsdist * do\_image\_import: fix argument retrieval * Update master for stable/2023.1 4.3.0 ----- * Release notes for 4.3.0 * Fix functional tests and docs generation * Boolean options: use strict checking * Unhardcode the value of DEFAULT\_PAGE\_SIZE from the tests * Remove unicode-related Python2-only code 4.2.0 ----- * schema\_args: Do not generate option for read-only properties * Replace osc with glance commands * Switch to 2023.1 Python3 unit tests and generic template name * md-property-create: add a mandatory "--type" option * Update master for stable/zed 4.1.0 ----- * Add support for glance-download import method * Bump default pagesize * Delete python bytecode before every test run 4.0.1 ----- * Remove incorrect note from requirements files * Uncap warlock * Check if stdin has isatty attribute * Bump tox minversion to 3.18.0 4.0.0 ----- * Update python testing as per zed cycle teting runtime * Add doc and test for verbose parameter * Fix functional CI job * Update master for stable/yoga 3.6.0 ----- * Release notes for 3.6.0 * Add support for Cache API * Add an optional parameter --append * Add an optional parameter --detail * Add support for usage API * Documentation: Add options for "glance image-import" * glance help : Clearly specify which options are mandatory * Updating python testing classifier as per Yoga testing runtime * Replace deprecated UPPER\_CONSTRAINTS\_FILE variable * Remove lower-constraints.txt * Make "tox -edocs" generate the manpage * Correct releasenote path for member-get command * Clean up extra spaces * Add Python3 yoga unit tests * Update master for stable/xena 3.5.0 ----- * Fix undesirable raw Python error * Add member-get command * Clean up extra spaces 3.4.0 ----- * Uncap PrettyTable * setup.cfg: Replace dashes with underscores * Add Python3 xena unit tests * Update master for stable/wallaby 3.3.0 ----- * Get tasks associated with image * bump mccabe in lc to unblock the gate * Add Python3 wallaby unit tests * Update master for stable/victoria 3.2.2 ----- * [goal] Migrate testing to ubuntu focal * Update test certificates to use strong signing 3.2.1 ----- * Fixes "stores" property added to the image * Remove F403, F812 and F821 from the ignorelist in tox.ini * Pass Global Request ID on with session client * Do not use the six library * Do not use the six library in the tests * drop mock from lower-constraints * Stop to use the \_\_future\_\_ module 3.2.0 ----- * Switch to newer openstackdocstheme and reno versions * Fix pygments style * Fix hacking min version to 3.0.1 * Fail gracefully when MD5 is unavailable * Bump default tox env from py37 to py38 * Add py38 package metadata * Use unittest.mock instead of third party mock * Add Python3 victoria unit tests * Pass --all-stores, --allow-failure as bool to API * Update master for stable/ussuri 3.1.0 ----- * Rename releasenotes to reflect correct version * Add release note for glanceclient 3.0.0 * Cleanup py27 support * Update hacking for Python3 * Fix SyntaxWarning with Python 3.8 * Delete image from specific store * Remove .zuul.yaml from the list of irrelevant files 3.0.0 ----- * Add support for copy-image import method * Add support for multi-store import * setup.cfg: Use better Python 3 hinting * Remove v1 tests * Drop support for tempest-full * Move py35 jobs to latest python3 * Drop python 2.7 support and testing * Stop configuring install\_command in tox * Update master for stable/train 2.17.0 ------ * Add release note for glanceclient 2.17.0 * Replace git.openstack.org URLs with opendev.org URLs * Cleanup session object * Trivial: fix image format typo * Correcting typo in shell.py - enviroment to environment * Bump openstackdocstheme to 1.20.0 * Add Python 3 Train unit tests * Modify the url of upper\_constraints\_file * Blacklist sphinx 2.1.0 (autodoc bug) * Update sphinx dependency * Rename --backend to --store * OpenDev Migration Patch * HTTPClient: actually set a timeout for requests * Update hacking version * Update master for stable/stein 2.16.0 ------ * Release notes for 2.16.0 * Remove redundant information from error message * Update min tox version to 2.0 * Update irrelevant-files for dsvm-functional tests * add python 3.7 unit test job * Add image-list filter for multihash * Add upper-constraints.txt to releasenotes tox environment * Change openstack-dev to openstack-discuss * Add Python 3.6 classifier to setup.cfg 2.15.0 ------ * Show the backend store info * Don't quote colon in HTTP headers * Embed validation data when adding location 2.14.0 ------ * Refactor periodic "tips" jobs * Cleanup .zuul.yaml * Use "multihash" for data download validation * 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 * update doc url to new * Remove team diversity tags note in README * Update reno for stable/rocky * Skip quote '=' for token header 2.12.0 ------ * Correct typo in releasenote * Releasenotes for bugfixes in 2.12.0 * Add support for hide old images * Do not quote '+' for token header * Add release note for hidden images support * Add release note for multi-store support * Add multihash release note * Unit tests for multi-store support * Add multi-store support * image-list: add checksum algorithm description * Add support for multihash * Replace 'raise StopIteration' with 'return' * Add release note link in README * Add experimental python3 functional test gate * fix tox python3 overrides * update shell tests to not rely on the serialization order of a dict * Remove PyPI downloads 2.11.1 ------ * Add release note for HTTP headers fix * Remove functional-identity-v3-only job * Switch to using stestr * Image show: print human readable string when the virtual size is unknown * Add periodic tips jobs * Handle HTTP headers per RFC 8187 * Update property keys document * Trivial: Update pypi url to new url * Follow the new PTI for document build 2.11.0 ------ * Add releasenotes for 2.11.0 * add lower-constraints job * Update local copy of image schema for 2.6 * Make image-import fail faster * Make image-create-via-import fail faster * Fix intermittent v2 shell unit test failures * Split glanceclient functional tests * Updated from global requirements * Check for container,disk\_format on web-download * Update properties URL * Add support for web-download import method * Fix docs cli authorize environment variables * Remove usage of ordereddict * Updated from global requirements * Updated from global requirements * Update 'doc/source/reference/apiv2.rst' * Remove usage of ordereddict * Updated from global requirements * Removes unicode 'u' response from "glance image-tag-update" * Zuul: Remove project name * Update reno for stable/queens * Updated from global requirements * Revert "Remove team:diverse-affiliation from tags" 2.9.1 ----- * Updated from global requirements * Remove deprecated ssl options * Avoid tox\_install.sh for constraints support 2.9.0 ----- * Compare against 'RequestIdProxy.wrapped' * Update Image service property keys doc * Update Image service property keys docs * Updated from global requirements * Revise functional testing README file * Restore functional testing under ssl * Migrate dsvm functional test jobs to project repo * Add domain info to functional test clients * Remove setting of version/release from releasenotes * Updated from global requirements * Updated from global requirements * Enable Python hash seed randomization in tests * Updated from global requirements * Remove Babel as a runtime requirement * image-create-via-import fails with ValueError * Fix image-import call * Updated from global requirements * stage call fails with TypeError * Fix python 3.6 escape char warning * Update reno for stable/pike * Updated from global requirements 2.8.0 ----- * Add missing docstring * Add image import features to client * Add documentation for image import commands * Add release note for Pike * Update glanceclient version ref * Removed the --no-ssl-compression parameter which is deprecated * Validate input args before trying image download * Make --profile load from environment variables * Updated from global requirements * Remove team:diverse-affiliation from tags * Updated from global requirements * Update and optimize documentation links * Updated from global requirements * help text for container\_format, disk\_format * Updated from global requirements * Fix man page build * turn on warning-is-error in sphinx build * use openstackdocstheme html context * update the doc URLs in the readme * switch to openstackdocstheme * import content from cli-reference in openstack-manuals * move existing content into the new standard structure * add explicit dependency on pyopenssl * Updated from global requirements * allow unhandled exceptions to cause test errors * move old release notes into the releasenotes doc tree * Enable code coverage report in console output * Replace assertTrue(isinstance()) with assertIsInstance() * Updated from global requirements * Updated from global requirements 2.7.0 ----- * Allow global\_request\_id in Client constructor * Downloading image with --progress fails * Updated from global requirements * v2: Content-Type: application/octet-stream header always added * Convert IOError from requests * Downloading image with --progress fails for python3 * Updated from global requirements * Updated from global requirements * gitignore: Ignore auto-generated docs * doc: Remove cruft from conf.py * Use Sphinx 1.5 warning-is-error * Explicitly set 'builders' option * Updated from global requirements * Updated from global requirements * Remove references to Python 3.4 * Remove log translations * Updated from global requirements * Updated from global requirements * Update test requirement * Updated from global requirements * x-openstack-request-id logged twice in logs * Updated from global requirements * Updated from global requirements * Replace six.iteritems() with .items() * Replace functions 'dict.get' and 'del' with 'dict.pop' * Update reno for stable/ocata * Fix 'UnicodeEncodeError' for unicode values in url 2.6.0 ----- * Updated from global requirements * Add request id to returned objects * Add ploop in disk\_format * Updated from global requirements * Handle formatting of subcommand name in error output * Add vhdx in disk\_format * Replace dict.iteritems() with dict.items() * Updated from global requirements * Use import\_versioned\_module from oslo.utils * Updated from global requirements * Add alt text for badges * Show team and repo badges on README * Add support for community images * Updated from global requirements * Move old oslo-incubator code out of openstack/common * Updated from global requirements * Enable release notes translation * Add Apache 2.0 license to source file * Updated from global requirements * Remove unused \_i18n.py shim * Updated from global requirements * Replace 'assertTrue(a not in b)' with 'assertNotIn(a, b)' * Updated from global requirements * Updated from global requirements * standardize release note page ordering * Updated from global requirements * switch from keystoneclient to keystoneauth * Improve tools/tox\_install.sh * Use constraints everywhere * Update reno for stable/newton 2.5.0 ----- * Updated from global requirements * Revert "Don't update tags every time" * Update doc URL 2.4.0 ----- * Updated from global requirements * Updated from global requirements 2.3.0 ----- * Updated from global requirements * Remove unused openstack/common/apiclient/client * Replace OpenStack LLC with OpenStack Foundation * py3: Fix encoding and use sys.stdin.buffer * Remove discover from test-requirements * Updated from global requirements * Log request-id before exceptions raised * Fix string interpolation to delayed to be handled by the logging code 2.2.0 ----- * Add comment about workaround for py3 * Properly build releasenotes * Update docs URL * Add Python 3.5 classifier and venv * Fix warlock model creation * Updated from global requirements * image-download: tests to catch stray output * Use correct order of arguments to assertEqual * Updated from global requirements * Update outdated image shema * Replace tempest\_lib with tempest.lib * Log request-id for each api call * Updated from global requirements * Updated from global requirements 2.1.0 ----- * Updated from global requirements * Remove deprecated construct method from session init * Add upper constraints to glanceclient * Remove unused skip\_authentication decorator * Don't update tags every time * Updated from global requirements * Fixed grammar in image-download command description * Updated from global requirements * Updated from global requirements * Updated from global requirements * Revert "Add last\_request\_id member to HTTPClient and SessionClient" * [Trivial] Remove unnecessary executable privilege * Fix "Codec can't encode characters" * Updated from global requirements * Update the home-page with developer documentation * Get endpoint if os\_image\_url is not set * Updated from global requirements * Corrected wrong parameter in docstring * Updated from global requirements * Enable hacking checks * Fix typos in docstrings and comments * Updated from global requirements * Re-enable stacktracing when --debug is used * Add last\_request\_id member to HTTPClient and SessionClient * Fix v2 so that you can see the default help info * Update auth\_token before sending request * Fix missing of debug info after we use session * Ship the default metadata schema in the client * Docs are generated incorrectly * Fix location update * Catch InUseByStore case in do\_image\_delete * Update reno for stable/mitaka 2.0.0 ----- * Handle 403 forbidden on download * Test: use assert\_has\_calls() instead * Updated from global requirements * trival: fix a typo in comment * Auto-generated squash commit * Add reno to glanceclient * Updated from global requirements * v2 - "readOnly" key should be used in schemas * Fix client initialization in shell tests * Remove code needed for python2.5 * Fix warnings in glanceclient README * Updated from global requirements * Remove argparse from requirements * Change metavar for location commands in V2 * Updated from global requirements * Updated from global requirements * Enhance description of instance-uuid option for image-create * Updated from global requirements * Remove monkey-patching for getsockopt * Fixed TestHTTPSVerifyCert failure messages * Remove location check from V2 client * Use session when not specified token or endpoint * Updated from global requirements * Skip schema validation on GET /v2/images/%s * Trival: Remove 'MANIFEST.in' * Remove openstack-common.conf * Updated from global requirements * Drop py33 support * Change assertTrue(isinstance()) by optimal assert * Add help the ability to sort images with multiple keys * use keystoneclient exceptions instead of oslo-incubator code * Add docker to image\_schema on glance v2 cli * remove python 2.6 trove classifier * Fix image-download to stdout on Python 3.x * Updated from global requirements * replace the deprecated keystoneclient...apiclient * Replace assertEqual(None, \*) with assertIsNone in tests * Removes MANIFEST.in as it is not needed explicitely by PBR * Updated from global requirements * Remove broken try/except workaround for old requests * Remove py26 support * Deprecated tox -downloadcache option removed * Updated from global requirements * Disable suggestion of v1 help for v2 commands * Remove pypy env from tox * Fix the download error when the image locations are blank * Updated from global requirements * Run py34 env first when launching tests * Updated from global requirements * Fix help for image-create * Fix tests for image-create * Fix Resource.\_\_eq\_\_ mismatch semantics of object equal * Update set of wanted commands in read-only test * Fix typo in 'help' in python-glanceclient * Add ordereddict requirement for py26 tests * Updated from global requirements * Ensure that identity token in header is not an unicode string * Change man page examples to correlate default v2 1.2.0 ----- * Release Notes for version 1.2.0 * Fix 'test\_help' for shell client * Add versions list function * Added reactivate/deactivate image using CLI * Fix the missing help descripiton of "image-create" * Add documentation for running the functional tests * Updated from global requirements * Remove unused sphinx Makefile * Update docs to recommend KSA instead of KSC * Use clouds.yaml from devstack for functional tests * Add translation to v2 shell * Import i18n functions directly * Updated from global requirements * improve readme contents * Add support for setting Accept-Language header * Updated from global requirements * Use the subcomand parsed args instead of the base * Stop trying to send image\_size to the server * Support image deletion in batches in v2 * print usage when no argument is specified for python3 * Updated from global requirements * Remove self from image-create/image-update * Updated from global requirements * Do not use openstack.common.i18n in glance client * Added unit tests for 'Unicode support shell client' * Use common identity parameters fro keystone client * No auth when token and endpoint are passed * Use dictionary literal for dictionary creation * Replace exception\_to\_str with oslo.utils function * Change ignore-errors to ignore\_errors * Add period in help message * Don't get the image before deleting it * Fix human readable when size is None 1.1.0 ----- * Change next version in docs * 1.0.1 Release notes * Fixes CLI client called without subcommands * Print the reverting back to v1 to stderr * Updates default --sort behaviour * Invalid output running the command 'glance image-show ' * Don't make \`help\` require auth parameters * Add parsing the endpoint URL * check for None value in utils.safe\_header * Updated from global requirements * Consider \`--os-token\` when using v2 * Check if v2 is available and fallback * Update path to subunit2html in post\_test\_hook * Fix the remove property logic in V2 * Password should be prompted once 1.0.0 ----- * 1.0.0 release notes * Fix Typos in comments * Remove custom SSL compression handling * Add more information show in v2 * Require disk and container format on image-create * Ship the default image schema in the client * Use API v2 as default * V2: Do not validate image schema when listing * Updated from global requirements * Add unicode support for properties values in v2 shell * Fix failure to create glance https connection pool * Add check Identity validate when get schemas * Enable flake8 checks * Extend unittests coverage for v2 tasks module * Support for Metadata Definition Catalog for Tags * Fix exception message in Http.py * Fix an issue with broken test on ci * Remove usage of assert\_called\_once on Mock objects * Add .eggs/\* to .gitignore * Updated from global requirements * Account for dictionary order in test\_shell.py * Do not fall back to namespaced oslo.i18n * Updated from global requirements * Add v2 support for the marker attribute * Import only modules and update tox.ini * Updated from global requirements * Close iterables at the end of iteration * Make glanceclient accept a session object * cleanup openstack-common.conf and sync updated files 0.19.0 ------ * Add release notes for 0.19.0 * Updated from global requirements * Include owner and status option in v2 image list * Fix Metadef Object update issue with python-glanceclient * Fix functional tests in gate * Do not crash on homedir mkdir * Improve import related error handling * Add parameter 'changes-since' for image-list of v1 * Check image-download for redirection * Add some basic CLI functional tests * Use assertIn instead of assertTrue in tests * Unorder compare in tests * Add release notes for 0.18.0 * Update README to work with release tools * Create functional test base * Move unit tests to standard directory 0.18.0 ------ * Uncap library requirements for liberty * Add unit tests for log\_curl\_request * Fix https stack trace on python 3.4 client * Fix client when using no ssl compression * Add SSL cert verification regression tests * Omit 'locations' as image-create parameter * Creating task with invalid property crashes in py3 * Don't accept \*args for client * Stub authentication requests rather than plugins * Remove keystoneclient mocks * Replace mox in tests with requests-mock * Expose 'is\_base' schema property attribute * Validate tag name when filtering for images * Remove redundant FakeSchemaAPI \_\_init\_\_ method * glance image-show now have --human-readable option * Test unit for checking update active images * Correct help messages for image-update command 0.17.0 ------ * Add release notes for 0.17.0 * Updated help for v2 member-update api * Extend images CLI v2 with new sorting syntax * Add the ability to specify the sort dir for each key * Import sys module * Adds the ability to sort images with multiple keys * add examples for properties and doc build script * Apply expected patch format when updating tags in v2.images * v2: read limit for list from --limit in shell * Fix leaking sockets after v2 list operation * Fix leaking sockets after v1 list operation 0.16.1 ------ * Add release notes for 0.16.1 * removed excessive call to os.path.exists * Fix tests failing if keystone is running locally * Unify using six.moves.range rename everywhere 0.16.0 ------ * Add release notes for 0.16.0 * Show error on trying to upload to non-queued image * https: Prevent leaking sockets for some operations * Glance image delete output * Strip json and html from error messages * Generate API documentation * Unit tests covering missing username or password * Register our own ConnectionPool without globals * Updated from global requirements * Change oslo.utils to oslo\_utils * Return 130 for keyboard interrupt * Ignore NoneType when encoding headers * Remove graduated gettextutils from openstack/common * Use utils.exit rather than print+sys.exit * Remove uuidutils from openstack-common * Add a \`--limit\` parameter to list operations * Fixed CLI help for bash-completion * Remove openstack.common.importutils * Remove openstack.common.strutils * Adds basic examples of v2 API usage * Sync latest apiclient from oslo-inc * Remove duplicate 'a' in the help string of --os-image-url * Close streamed requests explicitly * Handle HTTP byte returns in python 3 * Updated from global requirements * Add validation to --property-filter in v1 shell * v2: Allow upload from stdin on image-create * Fix v2 image create --file documentation * Make non-boolean check strict * Disable progress bar if image is piped into client * Fix Requests breaking download progress bar * Fix broken-pipe seen in glance-api * Update HTTPS certificate handling for pep-0476 0.15.0 ------ * Add release notes for 0.15.0 * Support Pagination for namespace list * Output clear error message on invalid api version * Support schema types with non-str value * Don't require version to create Client instance * Add os\_ prefix to project\_domain\_name/id * Workflow documentation is now in infra-manual * Use any instead of False in generator * Allow --file in image-create with v2 Image API * Add useful error on invalid --os-image-api-version * Add release notes for 0.14.0 - 0.14.2 * Fix minor typo in version error message * Send \`identity\_headers\` through the wire * Curl statements to include globoff for IPv6 URLs * Remove readonly options from v2 shell commands * Add --property-filter option to v2 image-list * Fix py34 failure for glance client 0.14.2 ------ * Don't set X-Auth-Token key in http session header if no token provided * Don't replace the https handler in the poolmanager * Refactor method of constructing dicts in some tests * Adds tty password entry for glanceclient * Fixed doc example * '--public' ignored on image create * Remove network\_utils * Skip non-base properties in patch method * Adds support for Glance Tasks calls * Reduce the set of supported client SSL ciphers * Fix the ordering of assertEqual arguments 0.14.1 ------ * Update how tokens are redacted * Handle UnicodeDecodeError in log\_http\_response * Print traceback to stderr if --debug is set * Stop using intersphinx * Updated from global requirements * Fix v2 requests to non-bleeding edge servers * Fix to ensure endpoint\_type is used by \_get\_endpoint() * Work toward Python 3.4 support and testing 0.14.0 ------ * Support for Metadata Definitions Catalog API * Catch new urllib3 exception: ProtocolError * Default to system CA bundle if no CA certificate is provided * Import missing gettextutils.\_ in shell.py * Fix error when logging http response with python 3 * Fix indentation in tox.ini * Add bash completion to glance client * Ensure server's SSL cert is validated * Enable osprofiler interface in glanceclient shell * Hide stderr noise in test output * Remove deprecated commands from shell * Normalize glanceclient requested service url * Fix glance-client to work with IPv6 controllers * Add support for Keystone v3 * Downgrade log message for http request failures * Update theme for docs * Add a tox job for generating docs * Don't stream non-binary requests * Use a correctly formatted example location in help * Replace old httpclient with requests * CLI image-update gives a wrong help on '--tags' param * Enable F841 * Resolving the performance issue for image listing of v2 API * Add profiling support to glanceclinet * Use immutable arg rather mutable arg * Add CONTRIBUTING.rst 0.13.1 ------ * Added release notes for 0.13.0 * Add wheels section to the setup.cfg * Add missing classifiers * Add license to setup.cfg * Fix CA certificate handling * Add the six module dependency * Prepend '/' to the delete url for the v2 client * Set purge-props header correctly in image update * Updated from global requirements * Change a debug line to prevent UnicodeDecodeError issue * Add support for location parameters in v2 commands * Convert passed integer values into int in v1 shell * Reuse class Manager from common code * Fix help text in image-create * Python 3: use next(foo) instead of foo.next() * Remove auth token from http logging * Finalize Python3 support * fixed typos found by RETF rules * Updated from global requirements * Remove py3k module * Return request ID to callers * progress flag not supported in v2 API * Fix for invalid literal ValueError parsing ipv6 url(s) * Adding network\_utils module from oslo-incubator * Sync with oslo-incubator * Fix the parameter order of assertEqual in glanceclient v1 test * Sync with Oslo * Python 3: do not use \_\_builtin\_\_ * Change assertTrue(isinstance()) by optimal assert * Updated from global requirements * Python3: do not use the 'file' type * Python 3: do not use the unicode() function * Fix the parameter order of assertEqual in glanceclient v2 test * Improve help strings * Fix the parameter order of assertEqual in glanceclient test * Python3: define a \_\_next\_\_() method for VerboseIteratorWrapper * test\_shell: remove a deprecated keyword argument * Python 3: Fix JsonPatch-related issues * Pass bytes to tempfile.NamedTemporaryFile().write() * Replace file with open, which is Python 3 compatible * Remove tox locale overrides * Fix misspellings in python-glanceclient * Update my mailmap * Add support for image size in v2 api upload * Only show progress bar for local image files * Using common method 'bool\_from\_string' from oslo strutils * Handle endpoints with versions consistently * Allow updating empty created v2 images from v1 * server 500 should not be a client error * It was removed urllib, urllib2 & urlparse modules * python3: Switch to mox3 instead of mox * Remove vim header * Python 3: use six.iteritems and six.string\_types * Python3: use six.StringIO rather than StringIO.StringIO * Python3: use six.StringIO rather than StringIO.StringIO * Replace file.write and os.path.exists by mock * Python 3: use six.iteritems() instead of iteritems() * Python 3: use six.iteritems() instead of iteritems() * Fix glanceclient http.py string formatting error * Reuse Resource from oslo * Get better format for long lines with PrettyTable * Remove unused imports * Sync apiclient and py3kcompat from oslo * Fix and enable gating on H306 * SSL: Handle wildcards in Subject Alternative Names * Updated from global requirements * Replace inheritance hierarchy with composition * Updates tox.ini to use new features * Updates .gitignore * Readd missing Babel dependency after merge from Oslo(master/bdda833) * Fix extra new line that break from progress bar 0.12.0 ------ * Add release notes for 0.12.0 * Make HACKING.rst DRYer * change assertEquals to assertEqual * Fix Pep8 errors found by Pep8 1.4.6 * python3: use six.moves for httplib imports * Sync from oslo-incubator * python3: xrange no longer exists * Fix misused assertTrue in unit tests * Add CLI for V2 image create, update, and upload * Fix regression bug after removing posixpath in http.py * Fix getting header in redirect processing * Fix default value for a header * Replace OpenStack LLC with OpenStack Foundation * Support glance client can get ipv6 image url correctly * Added support for running the tests under PyPy with tox * Enable query image by tag * Fix python 3.x related Hacking warnings * Fix glanceclient usage inconsistences for options * Add 0.11.0 doc notes * Use openstack-images-v2.1-json-patch for update method * Allow single-wildcard SSL common name matching * Revert "removed deprecated parameter --public" * \Allow removal of properties using glance v2 api * Updated from global requirements 0.10.0 ------ * Revert 02116565d358a4fa254217779fef82b14b38d8ca * Add 0.10.0 docs update * Show a pretty progressbar when uploading and downloading an image * Raise warlock requirement * Cast image\_id to string before calling urllib.quote * Don't use posixpath for URLs * Changes to allow image upload with V2 api * removed deprecated parameter --public * Encode error messages before sending them to stdout * Allow v1 client to list all users' images * Add v1 client side owner based filtering * Enable client library V2 to create an image * Provide glance CLI man page * Fix test assertions & test cases for V2 Shell Unit test * HTTPS response issues * Increase default page\_size value * Pass all identity headers received to glance * Fix SSL certificate CNAME checking * uncap python-keystoneclient version requirement * Expose checksum index image property in client * Flake8 should ignore build folder * Enable client V2 to update/delete tags for a given image * Rename invalid domain name to be RFC compliant * Start using Pyflakes and Hacking * Removes extra slash on endpoints without a path * Remove explicit distribute depend * Replace utils.ensure\_(str|unicode) with strutils.safe(decode|encode) * Do not decode headers in v1/images.py * Fix problem where image data is not read from a pipe * Add tests for encodings * python3: Introduce py33 to tox.ini * Rename requires files to standard names * Don't attempt to read stdin if it is empty * Update importutils and openstack-common.conf format * Convert non-ascii characters within image property to unicode * Migrate to pbr * Migrate to flake8 * Add test for glanceclient shells * Improve unit tests for python-glanceclient.glanceclient.common.base * Image Members for glance v2 api * Fix inconsistent --debug messages on image-update * Expand HACKING with commit message guidelines * Prevent WantReadError when using https * Improve Python 3.x compatibility * Sync with oslo-incubator copy of setup.py and version.py * bug 1166263 image-update handling for closed stdin * Test that copy\_from is used properly in old API * Fix "glance add" parsing of "copy\_from" option * Fix problem running glance --version * Improve unit tests for python-glanceclient.glanceclient.common.http 0.9.0 ----- * Add docs for 0.9.0 * Filter images list by public=True|False * Trapping KeyboardInterrupt sooner * Allow for prettytable 0.7.x as well * Implements filters: visibility, owner, member\_status. Includes tests * Add missing spaces in help msg * Control C does not cancel the CLI cleanly * Replace SchemaNotFound with HTTPNotFound * Use getattr properly in legacy shell 0.8.0 ----- * Add docs for v0.8.0 * Report name resolution errors properly * Decode input and encode output * Add library support for v2 image update * Expect minumum warlock version of 0.7.0 * Update to latest oslo-version * Update .coveragerc * Make effective ssl callback behaviour more obvious * Quote image ids before passing them to glance * Fix typo in image-update help page * Adds image-delete functionality * Change https port to be an optional parameter * Migrate to testr * Add image names to glance command arguments * Use testtools instead of unittest * Add details to stdout error message 0.7.0 ----- * Document v0.7.0 release * Support --os-cacert * Update --location help to reference swift store * Change default image sort to use name * Add --sort-key and --sort-dir to image-list * Pin pep8 to 1.3.3 * Allow setting x-image-meta-store through shell on image creation * Verify that host matches certificate 0.6.0 ----- * Document bugs/features for v0.6.0 * Hook up region\_name argument * Simplify human-readable size output * Make image sizes more readable for humans * Set useful boolean flag metavars * Unpin keystoneclient dependency * Fixes bug on Windows related to a wrong API url * Enhance --checksum help with algorithm * added --version as new parameter * Fixes setup compatibility issue on Windows * Allow deletion of multiple images through CLI * Fixes shell command for member-delete * Add OpenStack trove classifier for PyPI * Implement blueprint ssl-connect-rework * Handle create/update of images with unknown size * Display acceptable disk/container formats in help text * Simplify http(s) connection instantiation * Add happy path tests for ResponseBodyIterator * Use full URI path from Glance endpoint in HTTP requests * Typo in image-create help page * Fixes glance add / update / image-create / image-update on Windows * Fix weird "None" displayed on some errors * Make ConnectionRefused error more informative 0.5.1 ----- * Document remaining bug for v0.5.1 * Update docs for v0.5.1 release * Corrects URI to display hostname, port properly * Catches HTTP 300 while printing responses * get\_connection should raise httplib.InvalidURL * Fix PEP8 issues * Specified Content-Length in update request header * Sync importutils changes from openstack-common 0.5.0 ----- * Update release notes for v0.5.0 * Add nosehtmloutput as a test dependency * Update command descriptions * Update pip-requires with warlock<2 * Enable client V1 to download images * Simplify docs and provide 'News' on index.rst 0.4.2 ----- * Ensure v1 'limit' query parameter works correctly 0.4.1 ----- * Allow 'deleted' to be passed through image update * Cast is\_public, protected, deleted to bool * Return known int values as int, not str * Use system CA certificate file 0.4.0 ----- * socket errors and timeouts should be CommunicationErrors * Handle communication failures cleanly * Enable client V2 to download images * Refactor HTTP-related exceptions * Simplify v2 schema lookup * legacy\_shell.py shouldn't be executable * Client-side SSL Connection * SSL Certificate Validation 0.3.0 ----- * Add missing copyright headers * Add legacy compat layer to v1 shell * Allow CLI opts to override auth token and endpoint * Update python-keystoneclient version dependency * Stop looking for v2 image in container 0.2.0 ----- * Add exceptions for 500 and 503 HTTP status codes * Refactor http request/response logging * Fix --debug CLI option * Fix coverage reporting test * Honor '--insecure' commandline flag also for keystone authentication * Replace httplib2 with httplib as http driver * Clarify usage of --insecure flag * Add pagination to v1 image-list * Update README usage examples * Relax prettytable dependency to v0.6.X from v0.6 * Add pagination to v2 image-list * Prevent links from being printed in v2 CLI * Align print\_dict to the left * Convert v2 images list method to generator * Replace static v2 Image model with warlock model * Add support for viewing a single image through v2 * Rewrite link parsing for finding v2 schemas * Establish the supported importable interface * Add --is-public to image-create * Wrap image data in iterator * Translate is\_protected to protected * Change --protected to --is-protected in create * Properly map boolean-like arguments to True/False * Add ability to get version information in python * Latest setup goodness * Remove AuthorizationFailure exception * Preserve image properties on update * Add --file to image-update and correct bad name * Allow image filtering by custom properties * Expand v1 image-list filters * Add --timeout option to cli * Add size filtering to image-list action * Allow image upload from local file to v1 API * Use PyPI for keystoneclient * Switch CLI to support underscores and dashes 0.1.1 ----- * Split reading of versioninfo out into a method * Add support for tag-based version numbers * Support --os-endpoint-type in glanceclient * Hook up GET /v1/images/ 0.1.0 ----- * Add initial docs * Edit build\_sphinx options * Minimize tox.ini * Add 'explain' command to v2 that describes schemas * Stick prettytable at v0.6 * Add tests dir to pep8 command * Set pep8 dependency at v1.2 * Add minimal support for the v2 API * Auto generate AUTHORS file for glanceclient component * Include ChangeLog in tarball * Properly install from zipball * Adds support for --insecure * Fix the zipball change * Replace git url with github zipball * Refactor HTTPClient to use two request methods * Add missing files to MANIFEST.in * Add importutils from openstack-common * Adding service type as configurable shell option * Remove printt * Added condition requirement to simplejson * Use tox for running tests locally * Adds filter support to images.list() * Add '.tox' to .gitignore * Add fields to image-list * Strip version from service catalog endpoint * Fix image-create using pipelines * Allow tenant name to be used in authentication * Make tox cover output coverage.xml * Add Sphinx to test-requires * Updated depend processing to norms * Fixing pep8 errors * Add AUTHORS test case * Added gitreview file * Adding id for image members * image membership management works * Adding support for passing image data through cli * Image update works * More complete image creation * Correct keystoneclient egg name in pip-requires * Adding image-create action * Adding shared-images support * Image members bones * Basic testing * Update version to 2012.2 * Further cleanup * Basic get/list operations work * All the latest OpenStack hotness * Initial checkin for new CLI and client package ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/HACKING.rst0000664000175000017500000000035500000000000017312 0ustar00zuulzuul00000000000000Glance Style Commandments ========================= - Step 1: Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ - Step 2: Read on Glance Specific Commandments ---------------------------- None so far ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/LICENSE0000664000175000017500000002363600000000000016530 0ustar00zuulzuul00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1688740210.094164 python-glanceclient-4.4.0/PKG-INFO0000664000175000017500000000745500000000000016621 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: python-glanceclient Version: 4.4.0 Summary: OpenStack Image API Client Library Home-page: https://docs.openstack.org/python-glanceclient/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: Apache License, Version 2.0 Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-glanceclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html :alt: The following tags have been asserted for Python bindings to the OpenStack Images API: "project:official", "stable:follows-policy", "vulnerability:managed". Follow the link for an explanation of these tags. .. NOTE(rosmaita): the alt text above will have to be updated when additional tags are asserted for python-glanceclient. (The SVG in the governance repo is updated automatically.) .. Change things from this point on =========================================== Python bindings to the OpenStack Images API =========================================== .. image:: https://img.shields.io/pypi/v/python-glanceclient.svg :target: https://pypi.org/project/python-glanceclient/ :alt: Latest Version This is a client library for Glance built on the OpenStack Images API. It provides a Python API (the ``glanceclient`` module) and a command-line tool (``glance``). This library fully supports the v1 Images API, while support for the v2 API is in progress. Development takes place via the usual OpenStack processes as outlined in the `developer guide `_. The master repository is in `Git `_. See release notes and more at ``_. * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.org/project/python-glanceclient .. _Online Documentation: https://docs.openstack.org/python-glanceclient/latest/ .. _Launchpad project: https://launchpad.net/python-glanceclient .. _Blueprints: https://blueprints.launchpad.net/python-glanceclient .. _Bugs: https://bugs.launchpad.net/python-glanceclient .. _Source: https://opendev.org/openstack/python-glanceclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/glance-specs/ .. _Release notes: https://docs.openstack.org/releasenotes/python-glanceclient 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 :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Requires-Python: >=3.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/README.rst0000664000175000017500000000464100000000000017205 0ustar00zuulzuul00000000000000======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-glanceclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html :alt: The following tags have been asserted for Python bindings to the OpenStack Images API: "project:official", "stable:follows-policy", "vulnerability:managed". Follow the link for an explanation of these tags. .. NOTE(rosmaita): the alt text above will have to be updated when additional tags are asserted for python-glanceclient. (The SVG in the governance repo is updated automatically.) .. Change things from this point on =========================================== Python bindings to the OpenStack Images API =========================================== .. image:: https://img.shields.io/pypi/v/python-glanceclient.svg :target: https://pypi.org/project/python-glanceclient/ :alt: Latest Version This is a client library for Glance built on the OpenStack Images API. It provides a Python API (the ``glanceclient`` module) and a command-line tool (``glance``). This library fully supports the v1 Images API, while support for the v2 API is in progress. Development takes place via the usual OpenStack processes as outlined in the `developer guide `_. The master repository is in `Git `_. See release notes and more at ``_. * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.org/project/python-glanceclient .. _Online Documentation: https://docs.openstack.org/python-glanceclient/latest/ .. _Launchpad project: https://launchpad.net/python-glanceclient .. _Blueprints: https://blueprints.launchpad.net/python-glanceclient .. _Bugs: https://bugs.launchpad.net/python-glanceclient .. _Source: https://opendev.org/openstack/python-glanceclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/glance-specs/ .. _Release notes: https://docs.openstack.org/releasenotes/python-glanceclient ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1688740210.0781636 python-glanceclient-4.4.0/doc/0000775000175000017500000000000000000000000016256 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/doc/requirements.txt0000664000175000017500000000052100000000000021540 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. openstackdocstheme>=2.2.1 # Apache-2.0 reno>=3.1.0 # Apache-2.0 sphinx>=2.0.0,!=2.1.0 # BSD sphinxcontrib-apidoc>=0.2.0 # BSD ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1688740210.0781636 python-glanceclient-4.4.0/doc/source/0000775000175000017500000000000000000000000017556 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1688740210.0781636 python-glanceclient-4.4.0/doc/source/cli/0000775000175000017500000000000000000000000020325 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/doc/source/cli/details.rst0000664000175000017500000011705100000000000022511 0ustar00zuulzuul00000000000000========================================== Image service (glance) command-line client ========================================== The glance client is the command-line interface (CLI) for the Image service (glance) API and its extensions. This chapter documents :command:`glance` version ``2.8.0``. For help on a specific :command:`glance` command, enter: .. code-block:: console $ glance help COMMAND .. _glance_command_usage: glance usage ~~~~~~~~~~~~ .. code-block:: console usage: glance [--version] [-d] [-v] [--get-schema] [-f] [--os-image-url OS_IMAGE_URL] [--os-image-api-version OS_IMAGE_API_VERSION] [--profile HMAC_KEY] [--os-region-name OS_REGION_NAME] [--os-auth-token OS_AUTH_TOKEN] [--os-service-type OS_SERVICE_TYPE] [--os-endpoint-type OS_ENDPOINT_TYPE] [--insecure] [--os-cacert ] [--os-cert ] [--os-key ] [--timeout ] [--os-auth-type ] [--os-auth-url OS_AUTH_URL] [--os-domain-id OS_DOMAIN_ID] [--os-domain-name OS_DOMAIN_NAME] [--os-project-id OS_PROJECT_ID] [--os-project-name OS_PROJECT_NAME] [--os-project-domain-id OS_PROJECT_DOMAIN_ID] [--os-project-domain-name OS_PROJECT_DOMAIN_NAME] [--os-trust-id OS_TRUST_ID] [--os-default-domain-id OS_DEFAULT_DOMAIN_ID] [--os-default-domain-name OS_DEFAULT_DOMAIN_NAME] [--os-user-id OS_USER_ID] [--os-username OS_USERNAME] [--os-user-domain-id OS_USER_DOMAIN_ID] [--os-user-domain-name OS_USER_DOMAIN_NAME] [--os-password OS_PASSWORD] ... .. _glance_command_options: glance optional arguments ~~~~~~~~~~~~~~~~~~~~~~~~~ ``--version`` show program's version number and exit ``-d, --debug`` Defaults to ``env[GLANCECLIENT_DEBUG]``. ``-v, --verbose`` Print more verbose output. ``--get-schema`` Ignores cached copy and forces retrieval of schema that generates portions of the help text. Ignored with API version 1. ``-f, --force`` Prevent select actions from requesting user confirmation. ``--os-image-url OS_IMAGE_URL`` Defaults to ``env[OS_IMAGE_URL]``. If the provided image url contains a version number and \`--os-image-api-version\` is omitted the version of the URL will be picked as the image api version to use. ``--os-image-api-version OS_IMAGE_API_VERSION`` Defaults to ``env[OS_IMAGE_API_VERSION]`` or 2. ``--profile HMAC_KEY`` HMAC key to use for encrypting context data for performance profiling of operation. This key should be the value of HMAC key configured in osprofiler middleware in glance, it is specified in glance configuration file at /etc/glance/glance-api.conf and /etc/glance/glance-registry.conf. Without key the profiling will not be triggered even if osprofiler is enabled on server side. Defaults to ``env[OS_PROFILE]``. ``--os-region-name OS_REGION_NAME`` Defaults to ``env[OS_REGION_NAME]``. ``--os-auth-token OS_AUTH_TOKEN`` Defaults to ``env[OS_AUTH_TOKEN]``. ``--os-service-type OS_SERVICE_TYPE`` Defaults to ``env[OS_SERVICE_TYPE]``. ``--os-endpoint-type OS_ENDPOINT_TYPE`` Defaults to ``env[OS_ENDPOINT_TYPE]``. ``--os-auth-type , --os-auth-plugin `` Authentication type to use .. _glance_explain: glance explain -------------- .. code-block:: console usage: glance explain Describe a specific model. **Positional arguments:** ```` Name of model to describe. .. _glance_image-create: glance image-create ------------------- .. code-block:: console usage: glance image-create [--architecture ] [--protected [True|False]] [--name ] [--instance-uuid ] [--min-disk ] [--visibility ] [--kernel-id ] [--tags [ ...]] [--os-version ] [--disk-format ] [--os-distro ] [--id ] [--owner ] [--ramdisk-id ] [--min-ram ] [--container-format ] [--property ] [--file ] [--progress] Create a new image. **Optional arguments:** ``--architecture `` Operating system architecture as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#architecture ``--protected [True|False]`` If true, image will not be deletable. ``--name `` Descriptive name for the image ``--instance-uuid `` Metadata which can be used to record which instance this image is associated with. (Informational only, does not create an instance snapshot.) ``--min-disk `` Amount of disk space (in GB) required to boot image. ``--visibility `` Scope of image accessibility Valid values: public, private, community, shared ``--kernel-id `` ID of image stored in Glance that should be used as the kernel when booting an AMI-style image. ``--tags [ ...]`` List of strings related to the image ``--os-version `` Operating system version as specified by the distributor ``--disk-format `` Format of the disk Valid values: None, ami, ari, aki, vhd, vhdx, vmdk, raw, qcow2, vdi, iso, ploop ``--os-distro `` Common name of operating system distribution as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#os-distro ``--id `` An identifier for the image ``--owner `` Owner of the image ``--ramdisk-id `` ID of image stored in Glance that should be used as the ramdisk when booting an AMI-style image. ``--min-ram `` Amount of ram (in MB) required to boot image. ``--container-format `` Format of the container Valid values: None, ami, ari, aki, bare, ovf, ova, docker ``--property `` Arbitrary property to associate with image. May be used multiple times. ``--file `` Local file that contains disk image to be uploaded during creation. Alternatively, the image data can be passed to the client via stdin. ``--progress`` Show upload progress bar. .. _glance_image-deactivate: glance image-deactivate ----------------------- .. code-block:: console usage: glance image-deactivate Deactivate specified image. **Positional arguments:** ```` ID of image to deactivate. .. _glance_image-delete: glance image-delete ------------------- .. code-block:: console usage: glance image-delete [ ...] Delete specified image. **Positional arguments:** ```` ID of image(s) to delete. .. _glance_image-download: glance image-download --------------------- .. code-block:: console usage: glance image-download [--file ] [--progress] Download a specific image. **Positional arguments:** ```` ID of image to download. **Optional arguments:** ``--file `` Local file to save downloaded image data to. If this is not specified and there is no redirection the image data will not be saved. ``--progress`` Show download progress bar. .. _glance_image-list: glance image-list ----------------- .. code-block:: console usage: glance image-list [--limit ] [--page-size ] [--visibility ] [--member-status ] [--owner ] [--property-filter ] [--checksum ] [--tag ] [--sort-key {name,status,container_format,disk_format,size,id,created_at,updated_at}] [--sort-dir {asc,desc}] [--sort [:]] List images you can access. **Optional arguments:** ``--limit `` Maximum number of images to get. ``--page-size `` Number of images to request in each paginated request. ``--visibility `` The visibility of the images to display. ``--member-status `` The status of images to display. ``--owner `` Display images owned by . ``--property-filter `` Filter images by a user-defined image property. ``--checksum `` Displays images that match the MD5 checksum. ``--tag `` Filter images by a user-defined tag. ``--sort-key {name,status,container_format,disk_format,size,id,created_at,updated_at}`` Sort image list by specified fields. May be used multiple times. ``--sort-dir {asc,desc}`` Sort image list in specified directions. ``--sort [:]`` Comma-separated list of sort keys and directions in the form of [:]. Valid keys: name, status, container_format, disk_format, size, id, created_at, updated_at. OPTIONAL. .. _glance_image-reactivate: glance image-reactivate ----------------------- .. code-block:: console usage: glance image-reactivate Reactivate specified image. **Positional arguments:** ```` ID of image to reactivate. .. _glance_image-show: glance image-show ----------------- .. code-block:: console usage: glance image-show [--human-readable] [--max-column-width ] Describe a specific image. **Positional arguments:** ```` ID of image to describe. **Optional arguments:** ``--human-readable`` Print image size in a human-friendly format. ``--max-column-width `` The max column width of the printed table. .. _glance_image-tag-delete: glance image-tag-delete ----------------------- .. code-block:: console usage: glance image-tag-delete Delete the tag associated with the given image. **Positional arguments:** ```` ID of the image from which to delete tag. ```` Value of the tag. .. _glance_image-tag-update: glance image-tag-update ----------------------- .. code-block:: console usage: glance image-tag-update Update an image with the given tag. **Positional arguments:** ```` Image to be updated with the given tag. ```` Value of the tag. .. _glance_image-update: glance image-update ------------------- .. code-block:: console usage: glance image-update [--architecture ] [--protected [True|False]] [--name ] [--instance-uuid ] [--min-disk ] [--visibility ] [--kernel-id ] [--os-version ] [--disk-format ] [--os-distro ] [--owner ] [--ramdisk-id ] [--min-ram ] [--container-format ] [--property ] [--remove-property key] Update an existing image. **Positional arguments:** ```` ID of image to update. **Optional arguments:** ``--architecture `` Operating system architecture as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#architecture ``--protected [True|False]`` If true, image will not be deletable. ``--name `` Descriptive name for the image ``--instance-uuid `` Metadata which can be used to record which instance this image is associated with. (Informational only, does not create an instance snapshot.) ``--min-disk `` Amount of disk space (in GB) required to boot image. ``--visibility `` Scope of image accessibility Valid values: public, private, community, shared ``--kernel-id `` ID of image stored in Glance that should be used as the kernel when booting an AMI-style image. ``--os-version `` Operating system version as specified by the distributor ``--disk-format `` Format of the disk Valid values: None, ami, ari, aki, vhd, vhdx, vmdk, raw, qcow2, vdi, iso, ploop ``--os-distro `` Common name of operating system distribution as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#os-distro ``--owner `` Owner of the image ``--ramdisk-id `` ID of image stored in Glance that should be used as the ramdisk when booting an AMI-style image. ``--min-ram `` Amount of ram (in MB) required to boot image. ``--container-format `` Format of the container Valid values: None, ami, ari, aki, bare, ovf, ova, docker ``--property `` Arbitrary property to associate with image. May be used multiple times. ``--remove-property`` key Name of arbitrary property to remove from the image. .. _glance_image-upload: glance image-upload ------------------- .. code-block:: console usage: glance image-upload [--file ] [--size ] [--progress] Upload data for a specific image. **Positional arguments:** ```` ID of image to upload data to. **Optional arguments:** ``--file `` Local file that contains disk image to be uploaded. Alternatively, images can be passed to the client via stdin. ``--size `` Size in bytes of image to be uploaded. Default is to get size from provided data object but this is supported in case where size cannot be inferred. ``--progress`` Show upload progress bar. .. _glance_import-info: glance import-info ------------------ .. code-block:: console usage: glance import-info Prints the import methods available from Glance, or a message if the target Glance does not support image import. .. _glance_image-stage: glance image-stage ------------------ .. code-block:: console usage: glance image-stage [--file ] [--size ] [--progress] Upload data for a specific image to staging. **Positional arguments:** ```` ID of image to upload data to. **Optional arguments:** ``--file `` Local file that contains disk image to be uploaded. Alternatively, images can be passed to the client via stdin. ``--size `` Size in bytes of image to be uploaded. Default is to get size from provided data object but this is supported in case where size cannot be inferred. ``--progress`` Show upload progress bar. .. _glance_image-import: glance image-import ------------------- .. code-block:: console usage: glance image-import [--import-method ] [--uri ] [--store ] [--stores ] [--all-stores [True|False]] [--allow-failure [True|False]] Initiate the image import taskflow. **Positional arguments:** ```` ID of image to import. **Optional arguments:** ``--import-method `` Import method used for Image Import workflow. Valid values can be retrieved with import-info command and the default "glance-direct" is used with "image-stage". ``--uri `` URI to download the external image ``--store `` Backend store to upload image to. ``--stores `` List of comma separated stores to upload image if multi-stores are enabled in the environment. ``--all-stores [True|False]`` "all-stores" can be used instead of "--stores " to indicate that image should be imported all available stores. ``--allow-failure [True|False]`` Indicator if all stores listed (or available) must succeed. "True" by default meaning that we allow some stores to fail and the status can be monitored from the image metadata. If this is set to "False" the import will be reverted should any of the uploads fail. Only usable with "stores" or "all-stores". .. _glance_image-create-via-import: glance image-create-via-import ------------------------------ This is an **EXPERIMENTAL** command. It may be renamed or removed in future releases. .. code-block:: console usage: glance image-create-via import [--architecture ] [--protected [True|False]] [--name ] [--instance-uuid ] [--min-disk ] [--visibility ] [--kernel-id ] [--tags [ ...]] [--os-version ] [--disk-format ] [--os-distro ] [--id ] [--owner ] [--ramdisk-id ] [--min-ram ] [--container-format ] [--property ] [--file ] [--progress] Create a new image using the image import process. **NOTE** This is an EXPERIMENTAL command. It may be renamed or removed in future releases. **Optional arguments:** ``--architecture `` Operating system architecture as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#architecture ``--protected [True|False]`` If true, image will not be deletable. ``--name `` Descriptive name for the image ``--instance-uuid `` Metadata which can be used to record which instance this image is associated with. (Informational only, does not create an instance snapshot.) ``--min-disk `` Amount of disk space (in GB) required to boot image. ``--visibility `` Scope of image accessibility Valid values: public, private, community, shared ``--kernel-id `` ID of image stored in Glance that should be used as the kernel when booting an AMI-style image. ``--tags [ ...]`` List of strings related to the image ``--os-version `` Operating system version as specified by the distributor ``--disk-format `` Format of the disk Valid values: None, ami, ari, aki, vhd, vhdx, vmdk, raw, qcow2, vdi, iso, ploop ``--os-distro `` Common name of operating system distribution as specified in https://docs.openstack.org/glance/latest/user/common-image-properties.html#os-distro ``--id `` An identifier for the image ``--owner `` Owner of the image ``--ramdisk-id `` ID of image stored in Glance that should be used as the ramdisk when booting an AMI-style image. ``--min-ram `` Amount of ram (in MB) required to boot image. ``--container-format `` Format of the container Valid values: None, ami, ari, aki, bare, ovf, ova, docker ``--property `` Arbitrary property to associate with image. May be used multiple times. ``--file `` Local file that contains disk image to be uploaded during creation. Alternatively, the image data can be passed to the client via stdin. ``--progress`` Show upload progress bar. .. _glance_location-add: glance location-add ------------------- .. code-block:: console usage: glance location-add --url [--metadata ] Add a location (and related metadata) to an image. **Positional arguments:** ```` ID of image to which the location is to be added. **Optional arguments:** ``--url `` URL of location to add. ``--metadata `` Metadata associated with the location. Must be a valid JSON object (default: {}) .. _glance_location-delete: glance location-delete ---------------------- .. code-block:: console usage: glance location-delete --url Remove locations (and related metadata) from an image. **Positional arguments:** ```` ID of image whose locations are to be removed. **Optional arguments:** ``--url `` URL of location to remove. May be used multiple times. .. _glance_location-update: glance location-update ---------------------- .. code-block:: console usage: glance location-update --url [--metadata ] Update metadata of an image's location. **Positional arguments:** ```` ID of image whose location is to be updated. **Optional arguments:** ``--url `` URL of location to update. ``--metadata `` Metadata associated with the location. Must be a valid JSON object (default: {}) .. _glance_md-namespace-create: glance md-namespace-create -------------------------- .. code-block:: console usage: glance md-namespace-create [--schema ] [--created-at ] [--resource-type-associations [ ...]] [--protected [True|False]] [--self ] [--display-name ] [--owner ] [--visibility ] [--updated-at ] [--description ] Create a new metadata definitions namespace. **Positional arguments:** ```` Name of the namespace. **Optional arguments:** ``--schema `` ``--created-at `` Date and time of namespace creation. ``--resource-type-associations [...]`` ``--protected [True|False]`` If true, namespace will not be deletable. ``--self `` ``--display-name `` The user friendly name for the namespace. Used by UI if available. ``--owner `` Owner of the namespace. ``--visibility `` Scope of namespace accessibility. Valid values: public, private ``--updated-at `` Date and time of the last namespace modification. ``--description `` Provides a user friendly description of the namespace. .. _glance_md-namespace-delete: glance md-namespace-delete -------------------------- .. code-block:: console usage: glance md-namespace-delete Delete specified metadata definitions namespace with its contents. **Positional arguments:** ```` Name of namespace to delete. .. _glance_md-namespace-import: glance md-namespace-import -------------------------- .. code-block:: console usage: glance md-namespace-import [--file ] Import a metadata definitions namespace from file or standard input. **Optional arguments:** ``--file `` Path to file with namespace schema to import. Alternatively, namespaces schema can be passed to the client via stdin. .. _glance_md-namespace-list: glance md-namespace-list ------------------------ .. code-block:: console usage: glance md-namespace-list [--resource-types ] [--visibility ] [--page-size ] List metadata definitions namespaces. **Optional arguments:** ``--resource-types `` Resource type to filter namespaces. ``--visibility `` Visibility parameter to filter namespaces. ``--page-size `` Number of namespaces to request in each paginated request. .. _glance_md-namespace-objects-delete: glance md-namespace-objects-delete ---------------------------------- .. code-block:: console usage: glance md-namespace-objects-delete Delete all metadata definitions objects inside a specific namespace. **Positional arguments:** ```` Name of namespace. .. _glance_md-namespace-properties-delete: glance md-namespace-properties-delete ------------------------------------- .. code-block:: console usage: glance md-namespace-properties-delete Delete all metadata definitions property inside a specific namespace. **Positional arguments:** ```` Name of namespace. .. _glance_md-namespace-resource-type-list: glance md-namespace-resource-type-list -------------------------------------- .. code-block:: console usage: glance md-namespace-resource-type-list List resource types associated to specific namespace. **Positional arguments:** ```` Name of namespace. .. _glance_md-namespace-show: glance md-namespace-show ------------------------ .. code-block:: console usage: glance md-namespace-show [--resource-type ] [--max-column-width ] Describe a specific metadata definitions namespace. Lists also the namespace properties, objects and resource type associations. **Positional arguments:** ```` Name of namespace to describe. **Optional arguments:** ``--resource-type `` Applies prefix of given resource type associated to a namespace to all properties of a namespace. ``--max-column-width `` The max column width of the printed table. .. _glance_md-namespace-tags-delete: glance md-namespace-tags-delete ------------------------------- .. code-block:: console usage: glance md-namespace-tags-delete Delete all metadata definitions tags inside a specific namespace. **Positional arguments:** ```` Name of namespace. .. _glance_md-namespace-update: glance md-namespace-update -------------------------- .. code-block:: console usage: glance md-namespace-update [--created-at ] [--protected [True|False]] [--namespace ] [--self ] [--display-name ] [--owner ] [--visibility ] [--updated-at ] [--description ] Update an existing metadata definitions namespace. **Positional arguments:** ```` Name of namespace to update. **Optional arguments:** ``--created-at `` Date and time of namespace creation. ``--protected [True|False]`` If true, namespace will not be deletable. ``--namespace `` The unique namespace text. ``--self `` ``--display-name `` The user friendly name for the namespace. Used by UI if available. ``--owner `` Owner of the namespace. ``--visibility `` Scope of namespace accessibility. Valid values: public, private ``--updated-at `` Date and time of the last namespace modification. ``--description `` Provides a user friendly description of the namespace. .. _glance_md-object-create: glance md-object-create ----------------------- .. code-block:: console usage: glance md-object-create --name --schema Create a new metadata definitions object inside a namespace. **Positional arguments:** ```` Name of namespace the object will belong. **Optional arguments:** ``--name `` Internal name of an object. ``--schema `` Valid JSON schema of an object. .. _glance_md-object-delete: glance md-object-delete ----------------------- .. code-block:: console usage: glance md-object-delete Delete a specific metadata definitions object inside a namespace. **Positional arguments:** ```` Name of namespace the object belongs. ```` Name of an object. .. _glance_md-object-list: glance md-object-list --------------------- .. code-block:: console usage: glance md-object-list List metadata definitions objects inside a specific namespace. **Positional arguments:** ```` Name of namespace. .. _glance_md-object-property-show: glance md-object-property-show ------------------------------ .. code-block:: console usage: glance md-object-property-show [--max-column-width ] Describe a specific metadata definitions property inside an object. **Positional arguments:** ```` Name of namespace the object belongs. ```` Name of an object. ```` Name of a property. **Optional arguments:** ``--max-column-width `` The max column width of the printed table. .. _glance_md-object-show: glance md-object-show --------------------- .. code-block:: console usage: glance md-object-show [--max-column-width ] Describe a specific metadata definitions object inside a namespace. **Positional arguments:** ```` Name of namespace the object belongs. ```` Name of an object. **Optional arguments:** ``--max-column-width `` The max column width of the printed table. .. _glance_md-object-update: glance md-object-update ----------------------- .. code-block:: console usage: glance md-object-update [--name ] [--schema ] Update metadata definitions object inside a namespace. **Positional arguments:** ```` Name of namespace the object belongs. ```` Name of an object. **Optional arguments:** ``--name `` New name of an object. ``--schema `` Valid JSON schema of an object. .. _glance_md-property-create: glance md-property-create ------------------------- .. code-block:: console usage: glance md-property-create --name --title --schema <SCHEMA> <NAMESPACE> Create a new metadata definitions property inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace the property will belong. **Optional arguments:** ``--name <NAME>`` Internal name of a property. ``--title <TITLE>`` Property name displayed to the user. ``--schema <SCHEMA>`` Valid JSON schema of a property. .. _glance_md-property-delete: glance md-property-delete ------------------------- .. code-block:: console usage: glance md-property-delete <NAMESPACE> <PROPERTY> Delete a specific metadata definitions property inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace the property belongs. ``<PROPERTY>`` Name of a property. .. _glance_md-property-list: glance md-property-list ----------------------- .. code-block:: console usage: glance md-property-list <NAMESPACE> List metadata definitions properties inside a specific namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace. .. _glance_md-property-show: glance md-property-show ----------------------- .. code-block:: console usage: glance md-property-show [--max-column-width <integer>] <NAMESPACE> <PROPERTY> Describe a specific metadata definitions property inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace the property belongs. ``<PROPERTY>`` Name of a property. **Optional arguments:** ``--max-column-width <integer>`` The max column width of the printed table. .. _glance_md-property-update: glance md-property-update ------------------------- .. code-block:: console usage: glance md-property-update [--name <NAME>] [--title <TITLE>] [--schema <SCHEMA>] <NAMESPACE> <PROPERTY> Update metadata definitions property inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace the property belongs. ``<PROPERTY>`` Name of a property. **Optional arguments:** ``--name <NAME>`` New name of a property. ``--title <TITLE>`` Property name displayed to the user. ``--schema <SCHEMA>`` Valid JSON schema of a property. .. _glance_md-resource-type-associate: glance md-resource-type-associate --------------------------------- .. code-block:: console usage: glance md-resource-type-associate [--updated-at <UPDATED_AT>] [--name <NAME>] [--properties-target <PROPERTIES_TARGET>] [--prefix <PREFIX>] [--created-at <CREATED_AT>] <NAMESPACE> Associate resource type with a metadata definitions namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace. **Optional arguments:** ``--updated-at <UPDATED_AT>`` Date and time of the last resource type association modification. ``--name <NAME>`` Resource type names should be aligned with Heat resource types whenever possible: https://docs.openstack.org/heat/latest/template_guide/openstack.html ``--properties-target <PROPERTIES_TARGET>`` Some resource types allow more than one key / value pair per instance. For example, Cinder allows user and image metadata on volumes. Only the image properties metadata is evaluated by Nova (scheduling or drivers). This property allows a namespace target to remove the ambiguity. ``--prefix <PREFIX>`` Specifies the prefix to use for the given resource type. Any properties in the namespace should be prefixed with this prefix when being applied to the specified resource type. Must include prefix separator (e.g. a colon :). ``--created-at <CREATED_AT>`` Date and time of resource type association. .. _glance_md-resource-type-deassociate: glance md-resource-type-deassociate ----------------------------------- .. code-block:: console usage: glance md-resource-type-deassociate <NAMESPACE> <RESOURCE_TYPE> Deassociate resource type with a metadata definitions namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace. ``<RESOURCE_TYPE>`` Name of resource type. .. _glance_md-resource-type-list: glance md-resource-type-list ---------------------------- .. code-block:: console usage: glance md-resource-type-list List available resource type names. .. _glance_md-tag-create: glance md-tag-create -------------------- .. code-block:: console usage: glance md-tag-create --name <NAME> <NAMESPACE> Add a new metadata definitions tag inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of the namespace the tag will belong to. **Optional arguments:** ``--name <NAME>`` The name of the new tag to add. .. _glance_md-tag-create-multiple: glance md-tag-create-multiple ----------------------------- .. code-block:: console usage: glance md-tag-create-multiple --names <NAMES> [--delim <DELIM>] <NAMESPACE> Create new metadata definitions tags inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of the namespace the tags will belong to. **Optional arguments:** ``--names <NAMES>`` A comma separated list of tag names. ``--delim <DELIM>`` The delimiter used to separate the names (if none is provided then the default is a comma). .. _glance_md-tag-delete: glance md-tag-delete -------------------- .. code-block:: console usage: glance md-tag-delete <NAMESPACE> <TAG> Delete a specific metadata definitions tag inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of the namespace to which the tag belongs. ``<TAG>`` Name of the tag. .. _glance_md-tag-list: glance md-tag-list ------------------ .. code-block:: console usage: glance md-tag-list <NAMESPACE> List metadata definitions tags inside a specific namespace. **Positional arguments:** ``<NAMESPACE>`` Name of namespace. .. _glance_md-tag-show: glance md-tag-show ------------------ .. code-block:: console usage: glance md-tag-show <NAMESPACE> <TAG> Describe a specific metadata definitions tag inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of the namespace to which the tag belongs. ``<TAG>`` Name of the tag. .. _glance_md-tag-update: glance md-tag-update -------------------- .. code-block:: console usage: glance md-tag-update --name <NAME> <NAMESPACE> <TAG> Rename a metadata definitions tag inside a namespace. **Positional arguments:** ``<NAMESPACE>`` Name of the namespace to which the tag belongs. ``<TAG>`` Name of the old tag. **Optional arguments:** ``--name <NAME>`` New name of the new tag. .. _glance_member-create: glance member-create -------------------- .. code-block:: console usage: glance member-create <IMAGE_ID> <MEMBER_ID> Create member for a given image. **Positional arguments:** ``<IMAGE_ID>`` Image with which to create member. ``<MEMBER_ID>`` Tenant to add as member. .. _glance_member-delete: glance member-delete -------------------- .. code-block:: console usage: glance member-delete <IMAGE_ID> <MEMBER_ID> Delete image member. **Positional arguments:** ``<IMAGE_ID>`` Image from which to remove member. ``<MEMBER_ID>`` Tenant to remove as member. .. _glance_member-list: glance member-list ------------------ .. code-block:: console usage: glance member-list --image-id <IMAGE_ID> Describe sharing permissions by image. **Optional arguments:** ``--image-id <IMAGE_ID>`` Image to display members of. .. _glance_member-update: glance member-update -------------------- .. code-block:: console usage: glance member-update <IMAGE_ID> <MEMBER_ID> <MEMBER_STATUS> Update the status of a member for a given image. **Positional arguments:** ``<IMAGE_ID>`` Image from which to update member. ``<MEMBER_ID>`` Tenant to update. ``<MEMBER_STATUS>`` Updated status of member. Valid Values: accepted, rejected, pending .. _glance_task-create: glance task-create ------------------ .. code-block:: console usage: glance task-create [--type <TYPE>] [--input <STRING>] Create a new task. **Optional arguments:** ``--type <TYPE>`` Type of Task. Please refer to Glance schema or documentation to see which tasks are supported. ``--input <STRING>`` Parameters of the task to be launched .. _glance_task-list: glance task-list ---------------- .. code-block:: console usage: glance task-list [--sort-key {id,type,status}] [--sort-dir {asc,desc}] [--page-size <SIZE>] [--type <TYPE>] [--status <STATUS>] List tasks you can access. **Optional arguments:** ``--sort-key {id,type,status}`` Sort task list by specified field. ``--sort-dir {asc,desc}`` Sort task list in specified direction. ``--page-size <SIZE>`` Number of tasks to request in each paginated request. ``--type <TYPE>`` Filter tasks to those that have this type. ``--status <STATUS>`` Filter tasks to those that have this status. .. _glance_task-show: glance task-show ---------------- .. code-block:: console usage: glance task-show <TASK_ID> Describe a specific task. **Positional arguments:** ``<TASK_ID>`` ID of task to describe. ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/doc/source/cli/glance.rst�������������������������������������������������0000664�0001750�0001750�00000004262�00000000000�022314� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������============================== :program:`glance` CLI man page ============================== .. program:: glance .. highlight:: bash SYNOPSIS ======== :program:`glance` [options] <command> [command-options] :program:`glance help` :program:`glance help` <command> DESCRIPTION =========== The :program:`glance` command line utility interacts with OpenStack Images Service (Glance). 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-id``, and ``--os-auth-url`` or set corresponding environment variables:: export OS_USERNAME=user export OS_PASSWORD=pass export OS_PROJECT_ID=b363706f891f48019483f8bd6503c54b export OS_AUTH_URL=http://auth.example.com:5000/v2.0 The command line tool will attempt to reauthenticate using provided credentials for every request. You can override this behavior by manually supplying an auth token using ``--os-image-url`` and ``--os-auth-token`` or by setting corresponding environment variables:: export OS_IMAGE_URL=http://glance.example.org:9292/ export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 You can select an API version to use by ``--os-image-api-version`` option or by setting corresponding environment variable:: export OS_IMAGE_API_VERSION=1 Default Images API used is v2. OPTIONS ======= To get a list of available commands and options run:: glance help To get usage and options of a command:: glance help <command> EXAMPLES ======== Get information about image-create command:: glance help image-create See available images:: glance image-list To get a verbose output including more fields in the image list response:: glance --verbose image-list Create new image:: glance image-create --name foo --disk-format=qcow2 \ --container-format=bare --visibility=public \ --file /tmp/foo.img Describe a specific image:: glance image-show <Image-ID> BUGS ==== Glance client is hosted in Launchpad so you can view current bugs at https://bugs.launchpad.net/python-glanceclient/. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/doc/source/cli/index.rst��������������������������������������������������0000664�0001750�0001750�00000002127�00000000000�022170� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������============================= Command-line Tool Reference ============================= In order to use the CLI, you must provide your OpenStack username, password, tenant, and auth endpoint. Use the corresponding configuration options (``--os-username``, ``--os-password``, ``--os-project-id``, and ``--os-auth-url``) or set them in environment variables:: export OS_USERNAME=user export OS_PASSWORD=pass export OS_PROJECT_ID=b363706f891f48019483f8bd6503c54b export OS_AUTH_URL=http://auth.example.com:5000/v2.0 The command line tool will attempt to reauthenticate using your provided credentials for every request. You can override this behavior by manually supplying an auth token using ``--os-image-url`` and ``--os-auth-token``. You can alternatively set these environment variables:: export OS_IMAGE_URL=http://glance.example.org:9292/ export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 Once you've configured your authentication parameters, you can run ``glance help`` to see a complete listing of available commands. .. toctree:: details property-keys glance �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/doc/source/cli/property-keys.rst������������������������������������������0000664�0001750�0001750�00000002275�00000000000�023722� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=========================== Image service property keys =========================== You can use the glanceclient command line interface to set image properties that can be consumed by other services to affect the behavior of those other services. Properties can be set on an image at the time of image creation or they can be set on an existing image. Use the :command:`glance image-create` and :command:`glance image-update` commands respectively. For example: .. code-block:: console $ glance image-update IMG-UUID --property architecture=x86_64 For a list of image properties that can be used to affect the behavior of other services, refer to `Useful image properties <https://docs.openstack.org/glance/latest/admin/useful-image-properties.html>`_ in the Glance Administration Guide. .. note:: Behavior set using image properties overrides behavior set using flavors. For more information, refer to `Manage images <https://docs.openstack.org/glance/latest/admin/manage-images.html>`_ in the Glance Administration Guide. .. note:: Boolean properties expect one of the following values: '0', '1', 'f', 'false', 'n', 'no', 'off', 'on', 't', 'true', 'y', 'yes' (case-insensitive). �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/doc/source/conf.py��������������������������������������������������������0000664�0001750�0001750�00000005652�00000000000�021065� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2015 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 os import sys sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'openstackdocstheme', 'sphinxcontrib.apidoc', ] # sphinxcontrib.apidoc options apidoc_module_dir = '../../glanceclient' apidoc_output_dir = 'reference/api' apidoc_excluded_paths = [ 'tests/*', 'tests'] apidoc_separate_modules = True # openstackdocstheme options openstackdocs_repo_name = 'openstack/python-glanceclient' openstackdocs_bug_project = 'python-glanceclient' openstackdocs_bug_tag = '' # autodoc generation is a bit aggressive and a nuisance when doing heavy # text edit cycles. # execute "export SPHINX_DEBUG=1" in your terminal to disable # TODO: remove this once no dependency uses six anymore autodoc_mock_imports = ['six'] # 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. project = 'python-glanceclient' copyright = 'OpenStack Foundation' # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = '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' # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # -- Options for man page output ---------------------------------------------- # Grouping the document tree for man pages. # List of tuples 'sourcefile', 'target', 'title', 'Authors name', 'manual' man_pages = [ ('cli/glance', 'glance', 'Client for OpenStack Images API', ['OpenStack Foundation'], 1), ] ��������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/doc/source/index.rst������������������������������������������������������0000664�0001750�0001750�00000000641�00000000000�021420� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������============================================== Python Bindings for the OpenStack Images API ============================================== This is a client for the OpenStack Images API. There's :doc:`a Python API <reference/api/modules>` (the :mod:`glanceclient` module) and a :doc:`command-line script <cli/glance>` (installed as :program:`glance`). .. toctree:: :maxdepth: 2 reference/index cli/index �����������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0781636 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/doc/source/reference/�����������������������������������������������������0000775�0001750�0001750�00000000000�00000000000�021514� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/doc/source/reference/apiv2.rst��������������������������������������������0000664�0001750�0001750�00000004332�00000000000�023271� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Python API v2 ============= These Identity Service credentials can be used to authenticate:: * auth_url: Identity Service endpoint for authorization * username: name of user * password: user's password * project_{name|id}: name or ID of project Also the following parameters are required when using the Identity API v3:: * user_domain_{name|id}: name or ID of a domain the user belongs to * project_domain_{name|id}: name or ID for a domain the project belongs to To create a client:: from keystoneauth1 import loading from keystoneauth1 import session from glanceclient 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) session = session.Session(auth=auth) glance = Client('2', session=session) Create ------ Create a new image:: image = glance.images.create(name="myNewImage") glance.images.upload(image.id, open('/tmp/myimage.iso', 'rb')) Show ---- Describe a specific image:: glance.images.get(image.id) Update ------ Update a specific image:: # update with a list of image attribute names and their new values glance.images.update(image.id, name="myNewImageName") Custom Properties ----------------- Set a custom property on an image:: # set an arbitrary property on an image glance.images.update(image.id, my_custom_property='value') Remove a custom property from an image:: # remove the custom property 'my_custom_property' glance.images.update(image.id, remove_props=['my_custom_property']) Delete ------ Delete specified image(s):: glance.images.delete(image.id) List ---- List images you can access:: for image in glance.images.list(): print image Download -------- Download a specific image:: d = glance.images.data(image.id) Share an Image -------------- Share a specific image with a tenant:: glance.image_members.create(image_id, member_id) Remove a Share -------------- Remove a shared image from a tenant:: glance.image_members.delete(image_id, member_id) List Sharings ------------- Describe sharing permissions by image or tenant:: glance.image_members.list(image_id) ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/doc/source/reference/index.rst��������������������������������������������0000664�0001750�0001750�00000001542�00000000000�023357� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������========================== Python Library Reference ========================== 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 glanceclient import Client >>> glance = Client('1', endpoint=OS_IMAGE_ENDPOINT, token=OS_AUTH_TOKEN) >>> image = glance.images.create(name="My Test Image") >>> print image.status 'queued' >>> image.update(data=open('/tmp/myimage.iso', 'rb')) >>> print image.status 'active' >>> image.update(properties=dict(my_custom_property='value')) >>> with open('/tmp/copyimage.iso', 'wb') as f: for chunk in image.data(): f.write(chunk) >>> image.delete() .. toctree:: :maxdepth: 2 Python API Reference <api/modules> apiv2 ��������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0781636 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/�������������������������������������������������������������0000775�0001750�0001750�00000000000�00000000000�020141� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/__init__.py��������������������������������������������������0000664�0001750�0001750�00000002155�00000000000�022255� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2012 OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # NOTE(bcwaldon): this try/except block is needed to run setup.py due to # its need to import local code before installing required dependencies try: import glanceclient.client Client = glanceclient.client.Client except ImportError: import warnings warnings.warn("Could not import glanceclient.client", ImportWarning) import pbr.version version_info = pbr.version.VersionInfo('python-glanceclient') try: __version__ = version_info.version_string() except AttributeError: __version__ = None �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/_i18n.py�����������������������������������������������������0000664�0001750�0001750�00000001456�00000000000�021437� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import oslo_i18n as i18n _translators = i18n.TranslatorFactory(domain='glanceclient') # The primary translation function using the well-known name "_" _ = _translators.primary ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/client.py����������������������������������������������������0000664�0001750�0001750�00000004642�00000000000�021777� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import warnings from oslo_utils import importutils from glanceclient.common import utils def Client(version=None, endpoint=None, session=None, *args, **kwargs): """Client for the OpenStack Images API. Generic client for the OpenStack Images API. See version classes for specific details. :param string version: The version of API to use. :param session: A keystoneauth1 session that should be used for transport. :type session: keystoneauth1.session.Session """ # FIXME(jamielennox): Add a deprecation warning if no session is passed. # Leaving it as an option until we can ensure nothing break when we switch. if session: if endpoint: kwargs.setdefault('endpoint_override', endpoint) if not version: __, version = utils.strip_version(endpoint) if not version: msg = ("You must provide a client version when using session") raise RuntimeError(msg) else: if version is not None: warnings.warn(("`version` keyword is being deprecated. Please pass" " the version as part of the URL. " "http://$HOST:$PORT/v$VERSION_NUMBER"), DeprecationWarning) endpoint, url_version = utils.strip_version(endpoint) version = version or url_version if not version: msg = ("Please provide either the version or an url with the form " "http://$HOST:$PORT/v$VERSION_NUMBER") raise RuntimeError(msg) module = importutils.import_versioned_module('glanceclient', int(version), 'client') client_class = getattr(module, 'Client') return client_class(endpoint, *args, session=session, **kwargs) ����������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0781636 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/common/������������������������������������������������������0000775�0001750�0001750�00000000000�00000000000�021431� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/common/__init__.py�������������������������������������������0000664�0001750�0001750�00000000000�00000000000�023530� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/common/exceptions.py�����������������������������������������0000664�0001750�0001750�00000001341�00000000000�024163� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # This is here for compatibility purposes. Once all known OpenStack clients # are updated to use glanceclient.exc, this file should be removed from glanceclient.exc import * # noqa �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/common/http.py�����������������������������������������������0000664�0001750�0001750�00000034676�00000000000�023002� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import io import logging import socket from keystoneauth1 import adapter from keystoneauth1 import exceptions as ksa_exc import OpenSSL from oslo_utils import importutils from oslo_utils import netutils import requests import urllib.parse try: import json except ImportError: import simplejson as json from oslo_utils import encodeutils from glanceclient.common import utils from glanceclient import exc osprofiler_web = importutils.try_import("osprofiler.web") LOG = logging.getLogger(__name__) USER_AGENT = 'python-glanceclient' CHUNKSIZE = 1024 * 1024 # 1MiB REQ_ID_HEADER = 'X-OpenStack-Request-ID' TOKEN_HEADERS = ['X-Auth-Token', 'X-Service-Token'] def encode_headers(headers): """Encodes headers. Note: This should be used right before sending anything out. :param headers: Headers to encode :returns: Dictionary with encoded headers' names and values """ # NOTE(rosmaita): This function's rejection of any header name without a # corresponding value is arguably justified by RFC 7230. In any case, that # behavior was already here and there is an existing unit test for it. # Bug #1766235: According to RFC 8187, headers must be encoded as ASCII. # So we first %-encode them to get them into range < 128 and then turn # them into ASCII. encoded_dict = {} for h, v in headers.items(): if v is not None: # if the item is token, do not quote '+' as well. # NOTE(imacdonn): urllib.parse.quote() is intended for quoting the # path part of a URL, but headers like x-image-meta-location # include an entire URL. We should avoid encoding the colon in # this case (bug #1788942) safe = '=+/' if h in TOKEN_HEADERS else '/:' key = urllib.parse.quote(h, safe) value = urllib.parse.quote(v, safe) encoded_dict[key] = value return dict((encodeutils.safe_encode(h, encoding='ascii'), encodeutils.safe_encode(v, encoding='ascii')) for h, v in encoded_dict.items()) class _BaseHTTPClient(object): @staticmethod def _chunk_body(body): chunk = body while chunk: chunk = body.read(CHUNKSIZE) if not chunk: break yield chunk def _set_common_request_kwargs(self, headers, kwargs): """Handle the common parameters used to send the request.""" # Default Content-Type is octet-stream content_type = headers.get('Content-Type', 'application/octet-stream') # NOTE(jamielennox): remove this later. Managers should pass json= if # they want to send json data. data = kwargs.pop("data", None) if data is not None and not isinstance(data, str): try: data = json.dumps(data) content_type = 'application/json' except TypeError: # Here we assume it's # a file-like object # and we'll chunk it data = self._chunk_body(data) headers['Content-Type'] = content_type kwargs['stream'] = content_type == 'application/octet-stream' return data def _handle_response(self, resp): if not resp.ok: LOG.debug("Request returned failure status %s.", resp.status_code) raise exc.from_response(resp, resp.content) elif (resp.status_code == requests.codes.MULTIPLE_CHOICES and resp.request.path_url != '/versions'): # NOTE(flaper87): Eventually, we'll remove the check on `versions` # which is a bug (1491350) on the server. raise exc.from_response(resp) content_type = resp.headers.get('Content-Type') # Read body into string if it isn't obviously image data if content_type == 'application/octet-stream': # Do not read all response in memory when downloading an image. body_iter = _close_after_stream(resp, CHUNKSIZE) else: content = resp.text if content_type and content_type.startswith('application/json'): # Let's use requests json method, it should take care of # response encoding body_iter = resp.json() else: body_iter = io.StringIO(content) try: body_iter = json.loads(''.join([c for c in body_iter])) except ValueError: body_iter = None return resp, body_iter class HTTPClient(_BaseHTTPClient): def __init__(self, endpoint, **kwargs): self.endpoint = endpoint self.identity_headers = kwargs.get('identity_headers') self.auth_token = kwargs.get('token') self.language_header = kwargs.get('language_header') self.global_request_id = kwargs.get('global_request_id') if self.identity_headers: self.auth_token = self.identity_headers.pop('X-Auth-Token', self.auth_token) self.session = requests.Session() self.session.headers["User-Agent"] = USER_AGENT if self.language_header: self.session.headers["Accept-Language"] = self.language_header self.timeout = float(kwargs.get('timeout', 600)) if self.endpoint.startswith("https"): if kwargs.get('insecure', False) is True: self.session.verify = False else: if kwargs.get('cacert', None) != '': self.session.verify = kwargs.get('cacert', True) self.session.cert = (kwargs.get('cert_file'), kwargs.get('key_file')) def __del__(self): if self.session: try: self.session.close() except Exception as e: LOG.exception(e) finally: self.session = None @staticmethod def parse_endpoint(endpoint): return netutils.urlsplit(endpoint) def log_curl_request(self, method, url, headers, data, kwargs): curl = ['curl -g -i -X %s' % method] headers = copy.deepcopy(headers) headers.update(self.session.headers) for (key, value) in headers.items(): header = '-H \'%s: %s\'' % utils.safe_header(key, value) curl.append(header) if not self.session.verify: curl.append('-k') else: if isinstance(self.session.verify, str): curl.append(' --cacert %s' % self.session.verify) if self.session.cert: curl.append(' --cert %s --key %s' % self.session.cert) if data and isinstance(data, str): curl.append('-d \'%s\'' % data) curl.append(url) msg = ' '.join([encodeutils.safe_decode(item, errors='ignore') for item in curl]) LOG.debug(msg) @staticmethod def log_http_response(resp): status = (resp.raw.version / 10.0, resp.status_code, resp.reason) dump = ['\nHTTP/%.1f %s %s' % status] headers = resp.headers.items() dump.extend(['%s: %s' % utils.safe_header(k, v) for k, v in headers]) dump.append('') content_type = resp.headers.get('Content-Type') if content_type != 'application/octet-stream': dump.extend([resp.text, '']) LOG.debug('\n'.join([encodeutils.safe_decode(x, errors='ignore') for x in dump])) def _request(self, method, url, **kwargs): """Send an http request with the specified characteristics. Wrapper around httplib.HTTP(S)Connection.request to handle tasks such as setting headers and error handling. """ # Copy the kwargs so we can reuse the original in case of redirects headers = copy.deepcopy(kwargs.pop('headers', {})) if self.identity_headers: for k, v in self.identity_headers.items(): headers.setdefault(k, v) data = self._set_common_request_kwargs(headers, kwargs) # add identity header to the request if not headers.get('X-Auth-Token'): headers['X-Auth-Token'] = self.auth_token if self.global_request_id: headers.setdefault(REQ_ID_HEADER, self.global_request_id) if osprofiler_web: headers.update(osprofiler_web.get_trace_id_headers()) # Note(flaper87): Before letting headers / url fly, # they should be encoded otherwise httplib will # complain. headers = encode_headers(headers) if self.endpoint.endswith("/") or url.startswith("/"): conn_url = "%s%s" % (self.endpoint, url) else: conn_url = "%s/%s" % (self.endpoint, url) self.log_curl_request(method, conn_url, headers, data, kwargs) try: resp = self.session.request(method, conn_url, data=data, headers=headers, timeout=self.timeout, **kwargs) except requests.exceptions.Timeout as e: message = ("Error communicating with %(url)s: %(e)s" % dict(url=conn_url, e=e)) raise exc.InvalidEndpoint(message=message) except requests.exceptions.ConnectionError as e: message = ("Error finding address for %(url)s: %(e)s" % dict(url=conn_url, e=e)) raise exc.CommunicationError(message=message) except socket.gaierror as e: message = "Error finding address for %s: %s" % ( self.endpoint_hostname, e) raise exc.InvalidEndpoint(message=message) except (socket.error, socket.timeout, IOError) as e: endpoint = self.endpoint message = ("Error communicating with %(endpoint)s %(e)s" % {'endpoint': endpoint, 'e': e}) raise exc.CommunicationError(message=message) except OpenSSL.SSL.Error as e: message = ("SSL Error communicating with %(url)s: %(e)s" % {'url': conn_url, 'e': e}) raise exc.CommunicationError(message=message) # log request-id for each api call request_id = resp.headers.get('x-openstack-request-id') if request_id: LOG.debug('%(method)s call to image for ' '%(url)s used request id ' '%(response_request_id)s', {'method': resp.request.method, 'url': resp.url, 'response_request_id': request_id}) resp, body_iter = self._handle_response(resp) self.log_http_response(resp) return resp, body_iter def head(self, url, **kwargs): return self._request('HEAD', url, **kwargs) def get(self, url, **kwargs): return self._request('GET', url, **kwargs) def post(self, url, **kwargs): return self._request('POST', url, **kwargs) def put(self, url, **kwargs): return self._request('PUT', url, **kwargs) def patch(self, url, **kwargs): return self._request('PATCH', url, **kwargs) def delete(self, url, **kwargs): return self._request('DELETE', url, **kwargs) def _close_after_stream(response, chunk_size): """Iterate over the content and ensure the response is closed after.""" # Yield each chunk in the response body for chunk in response.iter_content(chunk_size=chunk_size): yield chunk # Once we're done streaming the body, ensure everything is closed. # This will return the connection to the HTTPConnectionPool in urllib3 # and ideally reduce the number of HTTPConnectionPool full warnings. response.close() class SessionClient(adapter.Adapter, _BaseHTTPClient): def __init__(self, session, **kwargs): kwargs.setdefault('user_agent', USER_AGENT) kwargs.setdefault('service_type', 'image') super(SessionClient, self).__init__(session, **kwargs) def request(self, url, method, **kwargs): headers = kwargs.pop('headers', {}) if self.global_request_id: headers.setdefault(REQ_ID_HEADER, self.global_request_id) kwargs['raise_exc'] = False data = self._set_common_request_kwargs(headers, kwargs) try: # NOTE(pumaranikar): To avoid bug #1641239, no modification of # headers should be allowed after encode_headers() is called. resp = super(SessionClient, self).request(url, method, headers=encode_headers(headers), data=data, **kwargs) except ksa_exc.ConnectTimeout as e: conn_url = self.get_endpoint(auth=kwargs.get('auth')) conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/')) message = ("Error communicating with %(url)s %(e)s" % dict(url=conn_url, e=e)) raise exc.InvalidEndpoint(message=message) except ksa_exc.ConnectFailure as e: conn_url = self.get_endpoint(auth=kwargs.get('auth')) conn_url = "%s/%s" % (conn_url.rstrip('/'), url.lstrip('/')) message = ("Error finding address for %(url)s: %(e)s" % dict(url=conn_url, e=e)) raise exc.CommunicationError(message=message) return self._handle_response(resp) def get_http_client(endpoint=None, session=None, **kwargs): if session: return SessionClient(session, **kwargs) elif endpoint: return HTTPClient(endpoint, **kwargs) else: raise AttributeError('Constructing a client must contain either an ' 'endpoint or a session') ������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/common/https.py����������������������������������������������0000664�0001750�0001750�00000022257�00000000000�023155� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2014 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 socket import ssl import struct import OpenSSL try: from eventlet import patcher # Handle case where we are running in a monkey patched environment if patcher.is_monkey_patched('socket'): from eventlet.green.httplib import HTTPSConnection from eventlet.green.OpenSSL.SSL import GreenConnection as Connection else: raise ImportError except ImportError: import http.client from OpenSSL import SSL HTTPSConnection = http.client.HTTPSConnection Connection = SSL.Connection from glanceclient import exc def verify_callback(host=None): """Provide wrapper for do_verify_callback. We use a partial around the 'real' verify_callback function so that we can stash the host value without holding a reference on the VerifiedHTTPSConnection. """ def wrapper(connection, x509, errnum, depth, preverify_ok, host=host): return do_verify_callback(connection, x509, errnum, depth, preverify_ok, host=host) return wrapper def do_verify_callback(connection, x509, errnum, depth, preverify_ok, host=None): """Verify the server's SSL certificate. This is a standalone function rather than a method to avoid issues around closing sockets if a reference is held on a VerifiedHTTPSConnection by the callback function. """ if x509.has_expired(): msg = "SSL Certificate expired on '%s'" % x509.get_notAfter() raise exc.SSLCertificateError(msg) if depth == 0 and preverify_ok: # We verify that the host matches against the last # certificate in the chain return host_matches_cert(host, x509) else: # Pass through OpenSSL's default result return preverify_ok def host_matches_cert(host, x509): """Verify the certificate identifies the host. Verify that the x509 certificate we have received from 'host' correctly identifies the server we are connecting to, ie that the certificate's Common Name or a Subject Alternative Name matches 'host'. """ def check_match(name): # Directly match the name if name == host: return True # Support single wildcard matching if name.startswith('*.') and host.find('.') > 0: if name[2:] == host.split('.', 1)[1]: return True common_name = x509.get_subject().commonName # First see if we can match the CN if check_match(common_name): return True # Also try Subject Alternative Names for a match san_list = None for i in range(x509.get_extension_count()): ext = x509.get_extension(i) if ext.get_short_name() == b'subjectAltName': san_list = str(ext) for san in ''.join(san_list.split()).split(','): if san.startswith('DNS:'): if check_match(san.split(':', 1)[1]): return True # Server certificate does not match host msg = ('Host "%s" does not match x509 certificate contents: ' 'CommonName "%s"' % (host, common_name)) if san_list is not None: msg = msg + ', subjectAltName "%s"' % san_list raise exc.SSLCertificateError(msg) def to_bytes(s): if isinstance(s, str): return bytes(s, 'latin-1') else: return s class OpenSSLConnectionDelegator(object): """An OpenSSL.SSL.Connection delegator. Supplies an additional 'makefile' method which httplib requires and is not present in OpenSSL.SSL.Connection. Note: Since it is not possible to inherit from OpenSSL.SSL.Connection a delegator must be used. """ def __init__(self, *args, **kwargs): self.connection = Connection(*args, **kwargs) def __getattr__(self, name): return getattr(self.connection, name) def makefile(self, *args, **kwargs): return socket._fileobject(self.connection, *args, **kwargs) class VerifiedHTTPSConnection(HTTPSConnection): """Extended OpenSSL HTTPSConnection for enhanced SSL support. Note: Much of this functionality can eventually be replaced with native Python 3.3 code. """ # Restrict the set of client supported cipher suites CIPHERS = 'ECDH+AESGCM:DH+AESGCM:ECDH+AES256:DH+AES256:'\ 'eCDH+AES128:DH+AES:ECDH+3DES:DH+3DES:RSA+AESGCM:'\ 'RSA+AES:RSA+3DES:!aNULL:!MD5:!DSS' def __init__(self, host, port=None, key_file=None, cert_file=None, cacert=None, timeout=None, insecure=False, ssl_compression=True): # List of exceptions reported by Python3 instead of # SSLConfigurationError excp_lst = (TypeError, FileNotFoundError, ssl.SSLError) try: HTTPSConnection.__init__(self, host, port, key_file=key_file, cert_file=cert_file) self.key_file = key_file self.cert_file = cert_file self.timeout = timeout self.insecure = insecure # NOTE(flaper87): `is_verified` is needed for # requests' urllib3. If insecure is True then # the request is not `verified`, hence `not insecure` self.is_verified = not insecure self.ssl_compression = ssl_compression self.cacert = None if cacert is None else str(cacert) self.set_context() # ssl exceptions are reported in various form in Python 3 # so to be compatible, we report the same kind as under # Python2 except excp_lst as e: raise exc.SSLConfigurationError(str(e)) def set_context(self): """Set up the OpenSSL context.""" self.context = OpenSSL.SSL.Context(OpenSSL.SSL.SSLv23_METHOD) self.context.set_cipher_list(self.CIPHERS) if self.ssl_compression is False: self.context.set_options(0x20000) # SSL_OP_NO_COMPRESSION if self.insecure is not True: self.context.set_verify(OpenSSL.SSL.VERIFY_PEER, verify_callback(host=self.host)) else: self.context.set_verify(OpenSSL.SSL.VERIFY_NONE, lambda *args: True) if self.cert_file: try: self.context.use_certificate_file(self.cert_file) except Exception as e: msg = 'Unable to load cert from "%s" %s' % (self.cert_file, e) raise exc.SSLConfigurationError(msg) if self.key_file is None: # We support having key and cert in same file try: self.context.use_privatekey_file(self.cert_file) except Exception as e: msg = ('No key file specified and unable to load key ' 'from "%s" %s' % (self.cert_file, e)) raise exc.SSLConfigurationError(msg) if self.key_file: try: self.context.use_privatekey_file(self.key_file) except Exception as e: msg = 'Unable to load key from "%s" %s' % (self.key_file, e) raise exc.SSLConfigurationError(msg) if self.cacert: try: self.context.load_verify_locations(to_bytes(self.cacert)) except Exception as e: msg = 'Unable to load CA from "%s" %s' % (self.cacert, e) raise exc.SSLConfigurationError(msg) else: self.context.set_default_verify_paths() def connect(self): """Connect to an SSL port using the OpenSSL library. This method also applies per-connection parameters to the connection. """ result = socket.getaddrinfo(self.host, self.port, 0, socket.SOCK_STREAM) if result: socket_family = result[0][0] if socket_family == socket.AF_INET6: sock = socket.socket(socket.AF_INET6, socket.SOCK_STREAM) else: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) else: # If due to some reason the address lookup fails - we still connect # to IPv4 socket. This retains the older behavior. sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) if self.timeout is not None: # '0' microseconds sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVTIMEO, struct.pack('LL', self.timeout, 0)) self.sock = OpenSSLConnectionDelegator(self.context, sock) self.sock.connect((self.host, self.port)) �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/common/progressbar.py����������������������������������������0000664�0001750�0001750�00000006342�00000000000�024341� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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. import sys class _ProgressBarBase(object): """A progress bar provider for a wrapped obect. Base abstract class used by specific class wrapper to show a progress bar when the wrapped object are consumed. :param wrapped: Object to wrap that hold data to be consumed. :param totalsize: The total size of the data in the wrapped object. :note: The progress will be displayed only if sys.stdout is a tty. """ def __init__(self, wrapped, totalsize): self._wrapped = wrapped self._totalsize = float(totalsize) self._show_progress = sys.stdout.isatty() and self._totalsize != 0 self._percent = 0 def _display_progress_bar(self, size_read): if self._show_progress: self._percent += size_read / self._totalsize # Output something like this: [==========> ] 49% sys.stdout.write('\r[{0:<30}] {1:.0%}'.format( '=' * int(round(self._percent * 29)) + '>', self._percent )) sys.stdout.flush() def __getattr__(self, attr): # Forward other attribute access to the wrapped object. return getattr(self._wrapped, attr) class VerboseFileWrapper(_ProgressBarBase): """A file wrapper with a progress bar. The file wrapper shows and advances a progress bar whenever the wrapped file's read method is called. """ def read(self, *args, **kwargs): data = self._wrapped.read(*args, **kwargs) if data: self._display_progress_bar(len(data)) else: if self._show_progress: # Break to a new line from the progress bar for incoming # output. sys.stdout.write('\n') return data class VerboseIteratorWrapper(_ProgressBarBase): """An iterator wrapper with a progress bar. The iterator wrapper shows and advances a progress bar whenever the wrapped data is consumed from the iterator. :note: Use only with iterator that yield strings. """ def __iter__(self): return self def next(self): try: data = next(self._wrapped) # NOTE(mouad): Assuming that data is a string b/c otherwise calling # len function will not make any sense. self._display_progress_bar(len(data)) return data except StopIteration: if self._show_progress: # Break to a new line from the progress bar for incoming # output. sys.stdout.write('\n') raise # In Python 3, __next__() has replaced next(). __next__ = next ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/common/utils.py����������������������������������������������0000664�0001750�0001750�00000051011�00000000000�023141� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import datetime import errno import functools import hashlib import json import os import re import sys import threading import urllib.parse import uuid if os.name == 'nt': # noqa import msvcrt # noqa else: # noqa msvcrt = None # noqa from oslo_utils import encodeutils from oslo_utils import strutils import prettytable import wrapt from glanceclient._i18n import _ from glanceclient import exc _memoized_property_lock = threading.Lock() SENSITIVE_HEADERS = ('X-Auth-Token', ) REQUIRED_FIELDS_ON_DATA = ('disk_format', 'container_format') # Decorator for cli-args def arg(*args, **kwargs): def _decorator(func): # Because of the semantics of decorator composition if we just append # to the options list positional options will appear to be backwards. func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) return func return _decorator def on_data_require_fields(data_fields, required=REQUIRED_FIELDS_ON_DATA): """Decorator to check commands' validity This decorator checks that required fields are present when image data has been supplied via command line arguments or via stdin On error throws CommandError exception with meaningful message. :param data_fields: Which fields' presence imply image data :type data_fields: iter :param required: Required fields :type required: iter :return: function decorator """ def args_decorator(func): def prepare_fields(fields): args = ('--' + x.replace('_', '-') for x in fields) return ', '.join(args) @functools.wraps(func) def func_wrapper(gc, args): # Set of arguments with data fields = set(a[0] for a in vars(args).items() if a[1]) # Fields the conditional requirements depend on present = fields.intersection(data_fields) # How many conditional requirements are missing missing = set(required) - fields # We use get_data_file to check if data is provided in stdin if (present or get_data_file(args)) and missing: msg = (_("error: Must provide %(req)s when using %(opt)s.") % {'req': prepare_fields(missing), 'opt': prepare_fields(present) or 'stdin'}) raise exc.CommandError(msg) return func(gc, args) return func_wrapper return args_decorator def schema_args(schema_getter, omit=None): omit = omit or [] typemap = { 'string': encodeutils.safe_decode, 'integer': int, 'boolean': lambda x: strutils.bool_from_string(x, strict=True), 'array': list } def _decorator(func): schema = schema_getter() if schema is None: param = '<unavailable>' kwargs = { 'help': ("Please run with connection parameters set to " "retrieve the schema for generating help for this " "command") } func.__dict__.setdefault('arguments', []).insert(0, ((param, ), kwargs)) else: properties = schema.get('properties', {}) for name, property in properties.items(): if name in omit: continue if property.get('readOnly', False): continue param = '--' + name.replace('_', '-') kwargs = {} type_str = property.get('type', 'string') if isinstance(type_str, list): # NOTE(flaper87): This means the server has # returned something like `['null', 'string']`, # therefore we use the first non-`null` type as # the valid type. for t in type_str: if t != 'null': type_str = t break if type_str == 'array': items = property.get('items') kwargs['type'] = typemap.get(items.get('type')) kwargs['nargs'] = '+' else: kwargs['type'] = typemap.get(type_str) if type_str == 'boolean': kwargs['metavar'] = '[True|False]' else: kwargs['metavar'] = '<%s>' % name.upper() description = property.get('description', "") if 'enum' in property: if len(description): description += " " # NOTE(flaper87): Make sure all values are `str/unicode` # for the `join` to succeed. Enum types can also be `None` # therefore, join's call would fail without the following # list comprehension vals = [str(val) for val in property.get('enum')] description += ('Valid values: ' + ', '.join(vals)) kwargs['help'] = description func.__dict__.setdefault('arguments', []).insert(0, ((param, ), kwargs)) return func return _decorator def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) def has_version(client, version): versions = client.get('/versions')[1].get('versions') supported = ['SUPPORTED', 'CURRENT', 'EXPERIMENTAL'] for version_struct in versions: if version_struct['id'] == version: return version_struct['status'] in supported return False def print_cached_images(cached_images): cache_pt = prettytable.PrettyTable(("ID", "State", "Last Accessed (UTC)", "Last Modified (UTC)", "Size", "Hits")) for item in cached_images: state = "queued" last_accessed = "N/A" last_modified = "N/A" size = "N/A" hits = "N/A" if item == 'cached_images': state = "cached" for image in cached_images[item]: last_accessed = image['last_accessed'] if last_accessed == 0: last_accessed = "N/A" else: last_accessed = datetime.datetime.utcfromtimestamp( last_accessed).isoformat() cache_pt.add_row((image['image_id'], state, last_accessed, datetime.datetime.utcfromtimestamp( image['last_modified']).isoformat(), image['size'], image['hits'])) else: for image in cached_images[item]: cache_pt.add_row((image, state, last_accessed, last_modified, size, hits)) print(cache_pt.get_string()) def print_dict_list(objects, fields): pt = prettytable.PrettyTable([f for f in fields], caching=False) pt.align = 'l' for o in objects: row = [] for field in fields: field_name = field.lower().replace(' ', '_') # NOTE (abhishekk) mapping field to actual name in the # structure. if field_name == 'task_id': field_name = 'id' data = o.get(field_name, '') row.append(data) pt.add_row(row) print(encodeutils.safe_decode(pt.get_string())) def print_list(objs, fields, formatters=None, field_settings=None): '''Prints a list of objects. @param objs: Objects to print @param fields: Fields on each object to be printed @param formatters: Custom field formatters ''' formatters = formatters or {} field_settings = field_settings or {} pt = prettytable.PrettyTable([f for f in fields], caching=False) pt.align = 'l' for o in objs: row = [] for field in fields: if field in field_settings: for setting, value in field_settings[field].items(): setting_dict = getattr(pt, setting) setting_dict[field] = value if field in formatters: row.append(formatters[field](o)) else: field_name = field.lower().replace(' ', '_') data = getattr(o, field_name, None) or '' row.append(data) pt.add_row(row) print(encodeutils.safe_decode(pt.get_string())) def print_dict(d, max_column_width=80): pt = prettytable.PrettyTable(['Property', 'Value'], caching=False) pt.align = 'l' pt.max_width = max_column_width for k, v in d.items(): if isinstance(v, (dict, list)): v = json.dumps(v) pt.add_row([k, v]) print(encodeutils.safe_decode(pt.get_string(sortby='Property'))) def find_resource(manager, name_or_id): """Helper for the _find_* methods.""" # first try to get entity as integer id try: if isinstance(name_or_id, int) or name_or_id.isdigit(): return manager.get(int(name_or_id)) except exc.NotFound: pass # now try to get entity as uuid try: # This must be unicode for Python 3 compatibility. # If you pass a bytestring to uuid.UUID, you will get a TypeError uuid.UUID(encodeutils.safe_decode(name_or_id)) return manager.get(name_or_id) except (ValueError, exc.NotFound): pass # finally try to find entity by name matches = list(manager.list(filters={'name': name_or_id})) num_matches = len(matches) if num_matches == 0: msg = "No %s with a name or ID of '%s' exists." % \ (manager.resource_class.__name__.lower(), name_or_id) raise exc.CommandError(msg) elif num_matches > 1: msg = ("Multiple %s matches found for '%s', use an ID to be more" " specific." % (manager.resource_class.__name__.lower(), name_or_id)) raise exc.CommandError(msg) else: return matches[0] def env(*vars, **kwargs): """Search for the first defined of possibly many env vars. Returns the first environment variable defined in vars, or returns the default defined in kwargs. """ for v in vars: value = os.environ.get(v, None) if value: return value return kwargs.get('default', '') def exit(msg='', exit_code=1): if msg: print_err(msg) sys.exit(exit_code) def print_err(msg): print(encodeutils.safe_decode(msg), file=sys.stderr) def save_image(data, path): """Save an image to the specified path. :param data: binary data of the image :param path: path to save the image to """ if path is None: # NOTE(kragniz): for py3 compatibility: sys.stdout.buffer is only # present on py3, otherwise fall back to sys.stdout image = getattr(sys.stdout, 'buffer', sys.stdout) else: image = open(path, 'wb') try: for chunk in data: image.write(chunk) finally: if path is not None: image.close() def make_size_human_readable(size): suffix = ['B', 'kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB'] base = 1024.0 index = 0 if size is None: size = 0 while size >= base: index = index + 1 size = size / base padded = '%.1f' % size stripped = padded.rstrip('0').rstrip('.') return '%s%s' % (stripped, suffix[index]) def get_file_size(file_obj): """Analyze file-like object and attempt to determine its size. :param file_obj: file-like object. :retval: The file's size or None if it cannot be determined. """ if (hasattr(file_obj, 'seek') and hasattr(file_obj, 'tell') and file_obj.seekable()): try: curr = file_obj.tell() file_obj.seek(0, os.SEEK_END) size = file_obj.tell() file_obj.seek(curr) return size except IOError as e: if e.errno == errno.ESPIPE: # Illegal seek. This means the file object # is a pipe (e.g. the user is trying # to pipe image data to the client, # echo testdata | bin/glance add blah...), or # that file object is empty, or that a file-like # object which doesn't support 'seek/tell' has # been supplied. return else: raise def get_data_file(args): if args.file: return open(args.file, 'rb') else: # distinguish cases where: # (1) stdin is not valid (as in cron jobs): # glance ... <&- # (2) image data is provided through standard input: # glance ... < /tmp/file or cat /tmp/file | glance ... # (3) no image data provided: # glance ... try: os.fstat(0) except OSError: # (1) stdin is not valid (closed...) return None if hasattr(sys.stdin, 'isatty') and not sys.stdin.isatty(): # (2) image data is provided through standard input image = sys.stdin if hasattr(sys.stdin, 'buffer'): image = sys.stdin.buffer if msvcrt: msvcrt.setmode(image.fileno(), os.O_BINARY) return image else: # (3) no image data provided return None def strip_version(endpoint): """Strip version from the last component of endpoint if present.""" # NOTE(flaper87): This shouldn't be necessary if # we make endpoint the first argument. However, we # can't do that just yet because we need to keep # backwards compatibility. if not isinstance(endpoint, str): raise ValueError("Expected endpoint") version = None # Get rid of trailing '/' if present endpoint = endpoint.rstrip('/') url_parts = urllib.parse.urlparse(endpoint) (scheme, netloc, path, __, __, __) = url_parts path = path.lstrip('/') # regex to match 'v1' or 'v2.0' etc if re.match(r'v\d+\.?\d*', path): version = float(path.lstrip('v')) endpoint = scheme + '://' + netloc return endpoint, version def print_image(image_obj, human_readable=False, max_col_width=None): ignore = ['self', 'access', 'file', 'schema'] image = dict([item for item in image_obj.items() if item[0] not in ignore]) if 'virtual_size' in image: image['virtual_size'] = image.get('virtual_size') or 'Not available' if human_readable: image['size'] = make_size_human_readable(image['size']) if str(max_col_width).isdigit(): print_dict(image, max_column_width=max_col_width) else: print_dict(image) def integrity_iter(iter, checksum): """Check image data integrity. :raises: IOError """ try: md5sum = hashlib.new('md5') except ValueError: raise IOError(errno.EPIPE, 'Corrupt image download. Expected checksum is %s ' 'but md5 algorithm is not available on the client' % checksum) for chunk in iter: yield chunk if isinstance(chunk, str): chunk = bytes(chunk, 'latin-1') md5sum.update(chunk) md5sum = md5sum.hexdigest() if md5sum != checksum: raise IOError(errno.EPIPE, 'Corrupt image download. Checksum was %s expected %s' % (md5sum, checksum)) def serious_integrity_iter(iter, hasher, hash_value): """Check image data integrity using the Glance "multihash". :param iter: iterable containing the image data :param hasher: a hashlib object :param hash_value: hexdigest of the image data :raises: IOError if the hashdigest of the data is not hash_value """ for chunk in iter: yield chunk if isinstance(chunk, str): chunk = bytes(chunk, 'latin-1') hasher.update(chunk) computed = hasher.hexdigest() if computed != hash_value: raise IOError(errno.EPIPE, 'Corrupt image download. Hash was %s expected %s' % (computed, hash_value)) def memoized_property(fn): attr_name = '_lazy_once_' + fn.__name__ @property def _memoized_property(self): if hasattr(self, attr_name): return getattr(self, attr_name) else: with _memoized_property_lock: if not hasattr(self, attr_name): setattr(self, attr_name, fn(self)) return getattr(self, attr_name) return _memoized_property def safe_header(name, value): if value is not None and name in SENSITIVE_HEADERS: h = hashlib.sha1(value) d = h.hexdigest() return name, "{SHA1}%s" % d else: return name, value def endpoint_version_from_url(endpoint, default_version=None): if endpoint: endpoint, version = strip_version(endpoint) return endpoint, version or default_version else: return None, default_version def debug_enabled(argv): if bool(env('GLANCECLIENT_DEBUG')) is True: return True if '--debug' in argv or '-d' in argv: return True return False class IterableWithLength(object): def __init__(self, iterable, length): self.iterable = iterable self.length = length def __iter__(self): try: for chunk in self.iterable: yield chunk finally: self.iterable.close() def next(self): return next(self.iterable) # In Python 3, __next__() has replaced next(). __next__ = next def __len__(self): return self.length class RequestIdProxy(wrapt.ObjectProxy): def __init__(self, wrapped): # `wrapped` is a tuple: (original_obj, response_obj) super(RequestIdProxy, self).__init__(wrapped[0]) self._self_wrapped = wrapped[0] req_id = _extract_request_id(wrapped[1]) self._self_request_ids = [req_id] @property def request_ids(self): return self._self_request_ids @property def wrapped(self): return self._self_wrapped # Overriden next method to act as iterator def next(self): return next(self._self_wrapped) # In Python 3, __next__() has replaced next(). __next__ = next class GeneratorProxy(wrapt.ObjectProxy): def __init__(self, wrapped): super(GeneratorProxy, self).__init__(wrapped) self._self_wrapped = wrapped self._self_request_ids = [] def _set_request_ids(self, resp): if self._self_request_ids == []: req_id = _extract_request_id(resp) self._self_request_ids = [req_id] def _next(self): obj, resp = next(self._self_wrapped) self._set_request_ids(resp) return obj # Override generator's next method to add # request id on each iteration def next(self): return self._next() # For Python 3 compatibility def __next__(self): return self._next() def __iter__(self): return self @property def request_ids(self): return self._self_request_ids @property def wrapped(self): return self._self_wrapped def add_req_id_to_object(): @wrapt.decorator def inner(wrapped, instance, args, kwargs): return RequestIdProxy(wrapped(*args, **kwargs)) return inner def add_req_id_to_generator(): @wrapt.decorator def inner(wrapped, instance, args, kwargs): return GeneratorProxy(wrapped(*args, **kwargs)) return inner def _extract_request_id(resp): # TODO(rsjethani): Do we need more checks here? return resp.headers.get('x-openstack-request-id') �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/exc.py�������������������������������������������������������0000664�0001750�0001750�00000011252�00000000000�021273� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re import sys class BaseException(Exception): """An error occurred.""" def __init__(self, message=None): self.message = message def __str__(self): return self.message or self.__class__.__doc__ class CommandError(BaseException): """Invalid usage of CLI.""" class InvalidEndpoint(BaseException): """The provided endpoint is invalid.""" class CommunicationError(BaseException): """Unable to communicate with server.""" class ClientException(Exception): """DEPRECATED!""" class HTTPException(ClientException): """Base exception for all HTTP-derived exceptions.""" code = 'N/A' def __init__(self, details=None): self.details = details or self.__class__.__name__ def __str__(self): return "HTTP %s" % (self.details) class HTTPMultipleChoices(HTTPException): code = 300 def __str__(self): self.details = ("Requested version of OpenStack Images API is not " "available.") return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code, self.details) class BadRequest(HTTPException): """DEPRECATED!""" code = 400 class HTTPBadRequest(BadRequest): pass class Unauthorized(HTTPException): """DEPRECATED!""" code = 401 class HTTPUnauthorized(Unauthorized): pass class Forbidden(HTTPException): """DEPRECATED!""" code = 403 class HTTPForbidden(Forbidden): pass class NotFound(HTTPException): """DEPRECATED!""" code = 404 class HTTPNotFound(NotFound): pass class HTTPMethodNotAllowed(HTTPException): code = 405 class Conflict(HTTPException): """DEPRECATED!""" code = 409 class HTTPConflict(Conflict): pass class OverLimit(HTTPException): """DEPRECATED!""" code = 413 class HTTPOverLimit(OverLimit): pass class HTTPInternalServerError(HTTPException): code = 500 class HTTPNotImplemented(HTTPException): code = 501 class HTTPBadGateway(HTTPException): code = 502 class ServiceUnavailable(HTTPException): """DEPRECATED!""" code = 503 class HTTPServiceUnavailable(ServiceUnavailable): pass # NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception # classes _code_map = {} for obj_name in dir(sys.modules[__name__]): if obj_name.startswith('HTTP'): obj = getattr(sys.modules[__name__], obj_name) _code_map[obj.code] = obj def from_response(response, body=None): """Return an instance of an HTTPException based on httplib response.""" cls = _code_map.get(response.status_code, HTTPException) if body and 'json' in response.headers['content-type']: # Iterate over the nested objects and retrieve the "message" attribute. messages = [obj.get('message') for obj in response.json().values()] # Join all of the messages together nicely and filter out any objects # that don't have a "message" attr. details = '\n'.join(i for i in messages if i is not None) return cls(details=details) elif body and 'html' in response.headers['content-type']: # Split the lines, strip whitespace and inline HTML from the response. details = [re.sub(r'<.+?>', '', i.strip()) for i in response.text.splitlines()] details = [i for i in details if i] # Remove duplicates from the list. details_seen = set() details_temp = [] for i in details: if i not in details_seen: details_temp.append(i) details_seen.add(i) # Return joined string separated by colons. details = ': '.join(details_temp) return cls(details=details) elif body: body = body.decode('utf-8') details = body.replace('\n\n', '\n') return cls(details=details) return cls() class NoTokenLookupException(Exception): """DEPRECATED!""" pass class EndpointNotFound(Exception): """DEPRECATED!""" pass class SSLConfigurationError(BaseException): pass class SSLCertificateError(BaseException): pass ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/shell.py�����������������������������������������������������0000664�0001750�0001750�00000066512�00000000000�021634� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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. """ Command-line interface to the OpenStack Images API. """ import argparse import copy import getpass import hashlib import json import logging import os import sys import traceback from oslo_utils import encodeutils from oslo_utils import importutils import urllib.parse import glanceclient from glanceclient._i18n import _ from glanceclient.common import utils from glanceclient import exc from keystoneauth1 import discover from keystoneauth1 import exceptions as ks_exc from keystoneauth1.identity import v2 as v2_auth from keystoneauth1.identity import v3 as v3_auth from keystoneauth1 import loading osprofiler_profiler = importutils.try_import("osprofiler.profiler") SUPPORTED_VERSIONS = [1, 2] class OpenStackImagesShell(object): def _append_global_identity_args(self, parser, argv): # register common identity args parser.set_defaults(os_auth_url=utils.env('OS_AUTH_URL')) parser.set_defaults(os_project_name=utils.env( 'OS_PROJECT_NAME', 'OS_TENANT_NAME')) parser.set_defaults(os_project_id=utils.env( 'OS_PROJECT_ID', 'OS_TENANT_ID')) parser.add_argument('--os_tenant_id', help=argparse.SUPPRESS) parser.add_argument('--os_tenant_name', help=argparse.SUPPRESS) parser.add_argument('--os-region-name', default=utils.env('OS_REGION_NAME'), help='Defaults to env[OS_REGION_NAME].') parser.add_argument('--os_region_name', help=argparse.SUPPRESS) parser.add_argument('--os-auth-token', default=utils.env('OS_AUTH_TOKEN'), help='Defaults to env[OS_AUTH_TOKEN].') parser.add_argument('--os_auth_token', help=argparse.SUPPRESS) parser.add_argument('--os-service-type', default=utils.env('OS_SERVICE_TYPE'), help='Defaults to env[OS_SERVICE_TYPE].') parser.add_argument('--os_service_type', help=argparse.SUPPRESS) parser.add_argument('--os-endpoint-type', default=utils.env('OS_ENDPOINT_TYPE'), help='Defaults to env[OS_ENDPOINT_TYPE].') parser.add_argument('--os_endpoint_type', help=argparse.SUPPRESS) loading.register_session_argparse_arguments(parser) # Peek into argv to see if os-auth-token (or the deprecated # os_auth_token) or the new os-token or the environment variable # OS_AUTH_TOKEN were given. In which case, the token auth plugin is # what the user wants. Else, we'll default to password. default_auth_plugin = 'password' token_opts = ['os-token', 'os-auth-token', 'os_auth-token'] if argv and any(i in token_opts for i in argv): default_auth_plugin = 'token' loading.register_auth_argparse_arguments( parser, argv, default=default_auth_plugin) def get_base_parser(self, argv): parser = argparse.ArgumentParser( prog='glance', description=__doc__.strip(), epilog='See "glance help COMMAND" ' 'for help on a specific command.', add_help=False, formatter_class=HelpFormatter, ) # Global arguments parser.add_argument('-h', '--help', action='store_true', help=argparse.SUPPRESS, ) parser.add_argument('--version', action='version', version=glanceclient.__version__) parser.add_argument('-d', '--debug', default=bool(utils.env('GLANCECLIENT_DEBUG')), action='store_true', help='Defaults to env[GLANCECLIENT_DEBUG].') parser.add_argument('-v', '--verbose', default=False, action="store_true", help="Print more verbose output.") parser.add_argument('--get-schema', default=False, action="store_true", dest='get_schema', help='Ignores cached copy and forces retrieval ' 'of schema that generates portions of the ' 'help text. Ignored with API version 1.') parser.add_argument('-f', '--force', dest='force', default=False, action='store_true', help='Prevent select actions from requesting ' 'user confirmation.') parser.add_argument('--os-image-url', default=utils.env('OS_IMAGE_URL'), help=('Defaults to env[OS_IMAGE_URL]. ' 'If the provided image url contains ' 'a version number and ' '`--os-image-api-version` is omitted ' 'the version of the URL will be picked as ' 'the image api version to use.')) parser.add_argument('--os_image_url', help=argparse.SUPPRESS) parser.add_argument('--os-image-api-version', default=utils.env('OS_IMAGE_API_VERSION', default=None), help='Defaults to env[OS_IMAGE_API_VERSION] or 2.') parser.add_argument('--os_image_api_version', help=argparse.SUPPRESS) parser.set_defaults(func=self.do_help) 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 should be the value of HMAC key ' 'configured in osprofiler middleware in ' 'glance, it is specified in glance ' 'configuration file at ' '/etc/glance/glance-api.conf and ' '/etc/glance/glance-registry.conf. 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, argv) return parser def get_subcommand_parser(self, version, argv=None): parser = self.get_base_parser(argv) self.subcommands = {} subparsers = parser.add_subparsers(metavar='<subcommand>') submodule = importutils.import_versioned_module('glanceclient', version, 'shell') self._find_actions(subparsers, submodule) self._find_actions(subparsers, self) self._add_bash_completion_subparser(subparsers) return parser def _find_actions(self, subparsers, actions_module): for attr in (a for a in dir(actions_module) if a.startswith('do_')): # Replace underscores with hyphens in the commands # displayed to the user command = attr[3:].replace('_', '-') callback = getattr(actions_module, attr) desc = callback.__doc__ or '' help = desc.strip().split('\n')[0] arguments = getattr(callback, 'arguments', []) subparser = subparsers.add_parser(command, help=help, description=desc, add_help=False, formatter_class=HelpFormatter ) subparser.add_argument('-h', '--help', action='help', help=argparse.SUPPRESS, ) self.subcommands[command] = subparser required_args = subparser.add_argument_group('Required arguments') for (args, kwargs) in arguments: if kwargs.get('required', False): required_args.add_argument(*args, **kwargs) else: subparser.add_argument(*args, **kwargs) subparser.set_defaults(func=callback) def _add_bash_completion_subparser(self, subparsers): subparser = subparsers.add_parser('bash_completion', add_help=False, formatter_class=HelpFormatter) self.subcommands['bash_completion'] = subparser subparser.set_defaults(func=self.do_bash_completion) def _get_image_url(self, args): """Translate the available url-related options into a single string. Return the endpoint that should be used to talk to Glance if a clear decision can be made. Otherwise, return None. """ if args.os_image_url: return args.os_image_url else: return None def _discover_auth_versions(self, session, auth_url): # discover the API versions the server is supporting base 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 ks_exc.ClientException as e: # Identity service may not support discover API version. # Lets trying to figure out the API version from the original URL. url_parts = urllib.parse.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: # not enough information to determine the auth version msg = ('Unable to determine the Keystone version ' 'to authenticate with using the given ' 'auth_url. Identity service may not support API ' 'version discovery. Please provide a versioned ' 'auth_url instead. error=%s') % (e) raise exc.CommandError(msg) return (v2_auth_url, v3_auth_url) def _get_keystone_auth_plugin(self, ks_session, **kwargs): # discover the supported keystone versions using the given auth url auth_url = kwargs.pop('auth_url', None) (v2_auth_url, v3_auth_url) = self._discover_auth_versions( session=ks_session, auth_url=auth_url) # Determine which authentication plugin to use. First inspect the # auth_url to see the supported version. If both v3 and v2 are # supported, then use the highest version if possible. user_id = kwargs.pop('user_id', None) username = kwargs.pop('username', None) password = kwargs.pop('password', None) user_domain_name = kwargs.pop('user_domain_name', None) user_domain_id = kwargs.pop('user_domain_id', None) # project and tenant can be used interchangeably project_id = (kwargs.pop('project_id', None) or kwargs.pop('tenant_id', None)) project_name = (kwargs.pop('project_name', None) or kwargs.pop('tenant_name', None)) project_domain_id = kwargs.pop('project_domain_id', None) project_domain_name = kwargs.pop('project_domain_name', None) auth = None use_domain = (user_domain_id or user_domain_name or project_domain_id or project_domain_name) use_v3 = v3_auth_url and (use_domain or (not v2_auth_url)) use_v2 = v2_auth_url and not use_domain if use_v3: auth = v3_auth.Password( v3_auth_url, user_id=user_id, username=username, password=password, user_domain_id=user_domain_id, user_domain_name=user_domain_name, project_id=project_id, project_name=project_name, project_domain_id=project_domain_id, project_domain_name=project_domain_name) elif use_v2: auth = v2_auth.Password( v2_auth_url, username, password, tenant_id=project_id, tenant_name=project_name) else: # if we get here it means domain information is provided # (caller meant to use Keystone V3) but the auth url is # actually Keystone V2. Obviously we can't authenticate a V3 # user using V2. exc.CommandError("Credential and auth_url mismatch. The given " "auth_url is using Keystone V2 endpoint, which " "may not able to handle Keystone V3 credentials. " "Please provide a correct Keystone V3 auth_url.") return auth def _get_kwargs_to_create_auth_plugin(self, args): if not args.os_username: raise exc.CommandError( _("You must provide a username via" " either --os-username or " "env[OS_USERNAME]")) if not args.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: args.os_password = getpass.getpass('OS Password: ') except EOFError: pass # No password because we didn't have a tty or the # user Ctl-D when prompted. if not args.os_password: raise exc.CommandError( _("You must provide a password via " "either --os-password, " "env[OS_PASSWORD], " "or prompted response")) # Validate password flow auth os_project_name = getattr( args, 'os_project_name', getattr(args, 'os_tenant_name', None)) os_project_id = getattr( args, 'os_project_id', getattr(args, 'os_tenant_id', None)) if not any([os_project_name, os_project_id]): # tenant is deprecated in Keystone v3. Use the latest # terminology instead. 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 args.os_auth_url: raise exc.CommandError( _("You must provide an auth url via" " either --os-auth-url or " "via env[OS_AUTH_URL]")) kwargs = { 'auth_url': args.os_auth_url, 'username': args.os_username, 'user_id': args.os_user_id, 'user_domain_id': args.os_user_domain_id, 'user_domain_name': args.os_user_domain_name, 'password': args.os_password, 'tenant_name': args.os_tenant_name, 'tenant_id': args.os_tenant_id, 'project_name': args.os_project_name, 'project_id': args.os_project_id, 'project_domain_name': args.os_project_domain_name, 'project_domain_id': args.os_project_domain_id, } return kwargs def _get_versioned_client(self, api_version, args): endpoint = self._get_image_url(args) auth_token = args.os_auth_token if endpoint and auth_token: kwargs = { 'token': auth_token, 'insecure': args.insecure, 'timeout': args.timeout, 'cacert': args.os_cacert, 'cert': args.os_cert, 'key': args.os_key, } else: ks_session = loading.load_session_from_argparse_arguments(args) auth_plugin_kwargs = self._get_kwargs_to_create_auth_plugin(args) ks_session.auth = self._get_keystone_auth_plugin( ks_session=ks_session, **auth_plugin_kwargs) kwargs = {'session': ks_session} if endpoint is None: endpoint_type = args.os_endpoint_type or 'public' service_type = args.os_service_type or 'image' endpoint = ks_session.get_endpoint( service_type=service_type, interface=endpoint_type, region_name=args.os_region_name) return glanceclient.Client(api_version, endpoint, **kwargs) def _cache_schemas(self, options, client, home_dir='~/.glanceclient'): homedir = os.path.expanduser(home_dir) path_prefix = homedir if options.os_auth_url: hash_host = hashlib.sha1(options.os_auth_url.encode('utf-8')) path_prefix = os.path.join(path_prefix, hash_host.hexdigest()) if not os.path.exists(path_prefix): try: os.makedirs(path_prefix) except OSError as e: # This avoids glanceclient to crash if it can't write to # ~/.glanceclient, which may happen on some env (for me, # it happens in Jenkins, as glanceclient can't write to # /var/lib/jenkins). msg = '%s' % e print(encodeutils.safe_decode(msg), file=sys.stderr) resources = ['image', 'metadefs/namespace', 'metadefs/resource_type'] schema_file_paths = [os.path.join(path_prefix, x + '_schema.json') for x in ['image', 'namespace', 'resource_type']] failed_download_schema = 0 for resource, schema_file_path in zip(resources, schema_file_paths): if (not os.path.exists(schema_file_path)) or options.get_schema: try: schema = client.schemas.get(resource) with open(schema_file_path, 'w') as f: f.write(json.dumps(schema.raw())) except exc.Unauthorized: raise exc.CommandError( "Invalid OpenStack Identity credentials.") except Exception: # NOTE(esheffield) do nothing here, we'll get a message # later if the schema is missing failed_download_schema += 1 pass return failed_download_schema >= len(resources) def main(self, argv): def _get_subparser(api_version): try: return self.get_subcommand_parser(api_version, argv) except ImportError as e: if not str(e): # Add a generic import error message if the raised # ImportError has none. raise ImportError('Unable to import module. Re-run ' 'with --debug for more info.') raise # Parse args once to find version # NOTE(flepied) Under Python3, parsed arguments are removed # from the list so make a copy for the first parsing base_argv = copy.deepcopy(argv) parser = self.get_base_parser(argv) (options, args) = parser.parse_known_args(base_argv) try: # NOTE(flaper87): Try to get the version from the # image-url first. If no version was specified, fallback # to the api-image-version arg. If both of these fail then # fallback to the minimum supported one and let keystone # do the magic. endpoint = self._get_image_url(options) endpoint, url_version = utils.strip_version(endpoint) except ValueError: # NOTE(flaper87): ValueError is raised if no endpoint is provided url_version = None # build available subcommands based on version try: api_version = int(options.os_image_api_version or url_version or 2) if api_version not in SUPPORTED_VERSIONS: raise ValueError except ValueError: msg = ("Invalid API version parameter. " "Supported values are %s" % SUPPORTED_VERSIONS) utils.exit(msg=msg) # Handle top-level --help/-h before attempting to parse # a command off the command line if options.help or not argv: parser = _get_subparser(api_version) self.do_help(options, parser=parser) return 0 # short-circuit and deal with help command right away. sub_parser = _get_subparser(api_version) args = sub_parser.parse_args(argv) if args.func == self.do_help: self.do_help(args, parser=sub_parser) return 0 elif args.func == self.do_bash_completion: self.do_bash_completion(args) return 0 if not options.os_image_api_version and api_version == 2: switch_version = True client = self._get_versioned_client('2', args) resp, body = client.http_client.get('/versions') for version in body['versions']: if version['id'].startswith('v2'): # NOTE(flaper87): We know v2 is enabled in the server, # which means we should be able to get the schemas and # move on. switch_version = self._cache_schemas(options, client) break if switch_version: print('WARNING: The client is falling back to v1 because' ' the accessing to v2 failed. This behavior will' ' be removed in future versions', file=sys.stderr) api_version = 1 sub_parser = _get_subparser(api_version) # Parse args again and call whatever callback was selected args = sub_parser.parse_args(argv) # NOTE(flaper87): Make sure we re-use the password input if we # have one. This may happen if the schemas were downloaded in # this same command. Password will be asked to download the # schemas and then for the operations below. if not args.os_password and options.os_password: args.os_password = options.os_password if args.debug: # Set up the root logger to debug so that the submodules can # print debug messages logging.basicConfig(level=logging.DEBUG) # for iso8601 < 0.1.11 logging.getLogger('iso8601').setLevel(logging.WARNING) LOG = logging.getLogger('glanceclient') LOG.addHandler(logging.StreamHandler()) LOG.setLevel(logging.DEBUG if args.debug else logging.INFO) profile = osprofiler_profiler and options.profile if profile: osprofiler_profiler.init(options.profile) client = self._get_versioned_client(api_version, args) try: args.func(client, args) except exc.Unauthorized: raise exc.CommandError("Invalid OpenStack Identity credentials.") finally: if profile: trace_id = osprofiler_profiler.get().get_base_id() print("Profiling trace ID: %s" % trace_id) print("To display trace use next command:\n" "osprofiler trace show --html %s " % trace_id) @utils.arg('command', metavar='<subcommand>', nargs='?', help='Display help for <subcommand>.') def do_help(self, args, parser): """Display help about this program or one of its subcommands.""" command = getattr(args, 'command', '') if 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: parser.print_help() if not args.os_image_api_version or args.os_image_api_version == '2': # NOTE(NiallBunting) This currently assumes that the only versions # are one and two. try: if command is None: print("\nRun `glance --os-image-api-version 1 help`" " for v1 help") else: self.get_subcommand_parser(1) if command in self.subcommands: command = ' ' + command print(("\nRun `glance --os-image-api-version 1 help%s`" " for v1 help") % (command or '')) except ImportError: pass def do_bash_completion(self, _args): """Prints arguments for bash_completion. Prints all of the commands and options to stdout so that the glance.bash_completion script doesn't have to hard code them. """ commands = set() options = set() for sc_str, sc in self.subcommands.items(): commands.add(sc_str) for option in sc._optionals._option_string_actions.keys(): options.add(option) commands.remove('bash_completion') commands.remove('bash-completion') print(' '.join(commands | options)) class HelpFormatter(argparse.HelpFormatter): def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) super(HelpFormatter, self).start_section(heading) def main(): try: argv = [encodeutils.safe_decode(a) for a in sys.argv[1:]] OpenStackImagesShell().main(argv) except KeyboardInterrupt: utils.exit('... terminating glance client', exit_code=130) except Exception as e: if utils.debug_enabled(argv) is True: traceback.print_exc() utils.exit(encodeutils.exception_to_unicode(e)) ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0781636 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/�������������������������������������������������������0000775�0001750�0001750�00000000000�00000000000�021303� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/__init__.py��������������������������������������������0000664�0001750�0001750�00000000000�00000000000�023402� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0781636 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/functional/��������������������������������������������0000775�0001750�0001750�00000000000�00000000000�023445� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/functional/README.rst����������������������������������0000664�0001750�0001750�00000003440�00000000000�025135� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������====================================== python-glanceclient functional testing ====================================== Idea ---- Run real client/server requests in the gate to catch issues which are difficult to catch with a purely unit test approach. Many projects (nova, keystone...) already have this form of testing in the gate. Testing Theory -------------- Since python-glanceclient has two uses, CLI and python API, we should have two sets of functional tests. CLI and python API. The python API tests should never use the CLI. But the CLI tests can use the python API where adding native support to the CLI for the required functionality would involve a non trivial amount of work. Functional Test Guidelines -------------------------- The functional tests require: 1) A working Glance/Keystone installation (for example, devstack) 2) A yaml file containing valid credentials If you are using devstack, a yaml file will have been created for you with the name /etc/openstack/clouds.yaml. The test code knows where to find it, so if you're using devstack, you don't need to do anything else. If you are not using devstack you should create a yaml file with the following format: clouds: devstack-admin: auth: auth_url: http://10.0.0.1:35357/v2.0 password: example project_domain_id: default project_name: admin user_domain_id: default username: admin identity_api_version: '2.0' region_name: RegionOne The tests will look for a file named 'clouds.yaml' in the following locations (in this order, first found wins): * current directory * ~/.config/openstack * /etc/openstack You may also set the environment variable OS_CLIENT_CONFIG_FILE to the absolute pathname of a file and that location will be inserted at the front of the search list. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/functional/__init__.py���������������������������������0000664�0001750�0001750�00000000000�00000000000�025544� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/functional/base.py�������������������������������������0000664�0001750�0001750�00000007030�00000000000�024731� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import glanceclient from keystoneauth1 import loading from keystoneauth1 import session import os import os_client_config from tempest.lib.cli import base def credentials(cloud='devstack-admin'): """Retrieves credentials to run functional tests Credentials are either read via os-client-config from the environment or from a config file ('clouds.yaml'). Environment variables override those from the config file. devstack produces a clouds.yaml with two named clouds - one named 'devstack' which has user privs and one named 'devstack-admin' which has admin privs. This function will default to getting the devstack-admin cloud as that is the current expected behavior. """ return os_client_config.OpenStackConfig().get_one_cloud(cloud=cloud) class ClientTestBase(base.ClientTestBase): """This is a first pass at a simple read only python-glanceclient test. This only exercises client commands that are read only. This should test commands: * as a regular user * as an admin user * with and without optional parameters * initially just check return codes, and later test command outputs """ def _get_clients(self): self.creds = credentials().get_auth_args() venv_name = os.environ.get('OS_TESTENV_NAME', 'functional') cli_dir = os.environ.get( 'OS_GLANCECLIENT_EXEC_DIR', os.path.join(os.path.abspath('.'), '.tox/%s/bin' % venv_name)) return base.CLIClient( username=self.creds['username'], password=self.creds['password'], tenant_name=self.creds['project_name'], user_domain_id=self.creds['user_domain_id'], project_domain_id=self.creds['project_domain_id'], uri=self.creds['auth_url'], cli_dir=cli_dir) def glance(self, *args, **kwargs): return self.clients.glance(*args, **kwargs) def glance_pyclient(self): ks_creds = dict( auth_url=self.creds["auth_url"], username=self.creds["username"], password=self.creds["password"], project_name=self.creds["project_name"], user_domain_id=self.creds["user_domain_id"], project_domain_id=self.creds["project_domain_id"]) keystoneclient = self.Keystone(**ks_creds) return self.Glance(keystoneclient) class Keystone(object): def __init__(self, **kwargs): loader = loading.get_plugin_loader("password") auth = loader.load_from_options(**kwargs) self.session = session.Session(auth=auth) class Glance(object): def __init__(self, keystone, version="2"): self.glance = glanceclient.Client( version, session=keystone.session) def find(self, image_name): for image in self.glance.images.list(): if image.name == image_name: return image return None ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0781636 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/functional/v1/�����������������������������������������0000775�0001750�0001750�00000000000�00000000000�023773� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/functional/v1/__init__.py������������������������������0000664�0001750�0001750�00000000000�00000000000�026072� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/functional/v1/test_readonly_glance.py������������������0000664�0001750�0001750�00000006107�00000000000�030536� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re from tempest.lib import exceptions from glanceclient.tests.functional import base class SimpleReadOnlyGlanceClientTest(base.ClientTestBase): """Read only functional python-glanceclient tests. This only exercises client commands that are read only. """ def test_list_v1(self): out = self.glance('--os-image-api-version 1 image-list') endpoints = self.parser.listing(out) self.assertTableStruct(endpoints, [ 'ID', 'Name', 'Disk Format', 'Container Format', 'Size', 'Status']) def test_fake_action(self): self.assertRaises(exceptions.CommandFailed, self.glance, 'this-does-not-exist') def test_member_list_v1(self): tenant_name = '--tenant-id %s' % self.creds['project_name'] out = self.glance('--os-image-api-version 1 member-list', params=tenant_name) endpoints = self.parser.listing(out) self.assertTableStruct(endpoints, ['Image ID', 'Member ID', 'Can Share']) def test_help(self): help_text = self.glance('--os-image-api-version 1 help') lines = help_text.split('\n') self.assertFirstLineStartsWith(lines, 'usage: glance') commands = [] cmds_start = lines.index('Positional arguments:') try: # Starting in Python 3.10, argparse displays options in the # "Options:" section... cmds_end = lines.index('Options:') except ValueError: # ... but before Python 3.10, options were displayed in the # "Optional arguments:" section. cmds_end = lines.index('Optional arguments:') command_pattern = re.compile(r'^ {4}([a-z0-9\-\_]+)') for line in lines[cmds_start:cmds_end]: match = command_pattern.match(line) if match: commands.append(match.group(1)) commands = set(commands) wanted_commands = {'bash-completion', 'help', 'image-create', 'image-delete', 'image-download', 'image-list', 'image-show', 'image-update', 'member-create', 'member-delete', 'member-list'} self.assertEqual(commands, wanted_commands) def test_version(self): self.glance('', flags='--version') def test_debug_list(self): self.glance('--os-image-api-version 1 image-list', flags='--debug') ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0821638 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/functional/v2/�����������������������������������������0000775�0001750�0001750�00000000000�00000000000�023774� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/functional/v2/__init__.py������������������������������0000664�0001750�0001750�00000000000�00000000000�026073� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/functional/v2/test_http_headers.py���������������������0000664�0001750�0001750�00000005215�00000000000�030062� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from glanceclient.tests.functional import base import time IMAGE = {"protected": False, "disk_format": "qcow2", "name": "glance_functional_test_image.img", "visibility": "private", "container_format": "bare"} class HttpHeadersTest(base.ClientTestBase): def test_encode_headers_python(self): """Test proper handling of Content-Type headers. encode_headers() must be called as late as possible before a request is sent. If this principle is violated, and if any changes are made to the headers between encode_headers() and the actual request (for instance a call to _set_common_request_kwargs()), and if you're trying to set a Content-Type that is not equal to application/octet-stream (the default), it is entirely possible that you'll end up with two Content-Type headers defined (yours plus application/octet-stream). The request will go out the door with only one of them chosen seemingly at random. This test uses a call to update() because it sets a header such as the following (this example may be subject to change): Content-Type: application/openstack-images-v2.1-json-patch This situation only occurs in python3. This test will never fail in python2. There is no test against the CLI because it swallows the error. """ # the failure is intermittent - try up to 6 times for attempt in range(0, 6): glanceclient = self.glance_pyclient() image = glanceclient.find(IMAGE["name"]) if image: glanceclient.glance.images.delete(image.id) image = glanceclient.glance.images.create(name=IMAGE["name"]) self.assertTrue(image.status == "queued") try: image = glanceclient.glance.images.update(image.id, disk_format="qcow2") except Exception as e: self.assertFalse("415 Unsupported Media Type" in e.details) time.sleep(5) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/functional/v2/test_readonly_glance.py������������������0000664�0001750�0001750�00000010725�00000000000�030540� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re from tempest.lib import exceptions from glanceclient.tests.functional import base class SimpleReadOnlyGlanceClientTest(base.ClientTestBase): """Read only functional python-glanceclient tests. This only exercises client commands that are read only. """ def test_list_v2(self): out = self.glance('--os-image-api-version 2 image-list') endpoints = self.parser.listing(out) self.assertTableStruct(endpoints, ['ID', 'Name']) def test_fake_action(self): self.assertRaises(exceptions.CommandFailed, self.glance, 'this-does-not-exist') def test_member_list_v2(self): try: # NOTE(flwang): If set disk-format and container-format, Jenkins # will raise an error said can't recognize the params, though it # works fine at local. Without the two params, Glance will # complain. So we just catch the exception can skip it. self.glance('--os-image-api-version 2 image-create --name temp') except Exception: pass out = self.glance('--os-image-api-version 2 image-list' ' --visibility private') image_list = self.parser.listing(out) # NOTE(flwang): Because the member-list command of v2 is using # image-id as required parameter, so we have to get a valid image id # based on current environment. If there is no valid image id, we will # pass in a fake one and expect a 404 error. if len(image_list) > 0: param_image_id = '--image-id %s' % image_list[0]['ID'] out = self.glance('--os-image-api-version 2 member-list', params=param_image_id) endpoints = self.parser.listing(out) self.assertTableStruct(endpoints, ['Image ID', 'Member ID', 'Status']) else: param_image_id = '--image-id fake_image_id' self.assertRaises(exceptions.CommandFailed, self.glance, '--os-image-api-version 2 member-list', params=param_image_id) def test_help(self): help_text = self.glance('--os-image-api-version 2 help') lines = help_text.split('\n') self.assertFirstLineStartsWith(lines, 'usage: glance') commands = [] cmds_start = lines.index('Positional arguments:') try: # Starting in Python 3.10, argparse displays options in the # "Options:" section... cmds_end = lines.index('Options:') except ValueError: # ... but before Python 3.10, options were displayed in the # "Optional arguments:" section. cmds_end = lines.index('Optional arguments:') command_pattern = re.compile(r'^ {4}([a-z0-9\-\_]+)') for line in lines[cmds_start:cmds_end]: match = command_pattern.match(line) if match: commands.append(match.group(1)) commands = set(commands) wanted_commands = {'bash-completion', 'help', 'image-create', 'image-deactivate', 'image-delete', 'image-download', 'image-list', 'image-reactivate', 'image-show', 'image-tag-delete', 'image-tag-update', 'image-update', 'image-upload', 'location-add', 'location-delete', 'location-update', 'member-create', 'member-delete', 'member-list', 'member-update', 'task-create', 'task-list', 'task-show'} self.assertFalse(wanted_commands - commands) def test_version(self): self.glance('', flags='--version') def test_debug_list(self): self.glance('--os-image-api-version 2 image-list', flags='--debug') �������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0821638 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/unit/��������������������������������������������������0000775�0001750�0001750�00000000000�00000000000�022262� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/unit/__init__.py���������������������������������������0000664�0001750�0001750�00000000000�00000000000�024361� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/unit/test_base.py��������������������������������������0000664�0001750�0001750�00000004740�00000000000�024612� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2013 OpenStack Foundation # Copyright (C) 2013 Yahoo! Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools from glanceclient.v1.apiclient import base class TestBase(testtools.TestCase): def test_resource_repr(self): r = base.Resource(None, dict(foo="bar", baz="spam")) self.assertEqual("<Resource baz=spam, foo=bar>", repr(r)) def test_getid(self): self.assertEqual(4, base.getid(4)) class TmpObject(object): id = 4 self.assertEqual(4, base.getid(TmpObject)) def test_two_resources_with_same_id_are_not_equal(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) def test_two_resources_with_same_id_and_info_are_equal(self): # 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) def test_two_resources_with_eq_info_are_equal(self): # 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_two_resources_with_diff_id_are_not_equal(self): # Two resources with diff ID: not equal r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) r2 = base.Resource(None, {'id': 2, 'name': 'hello'}) self.assertNotEqual(r1, r2) def test_two_resources_with_not_eq_info_are_not_equal(self): # Two resources with no ID: not equal if their info is not equal r1 = base.Resource(None, {'name': 'bill', 'age': 21}) r2 = base.Resource(None, {'name': 'joe', 'age': 12}) self.assertNotEqual(r1, r2) ��������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/unit/test_client.py������������������������������������0000664�0001750�0001750�00000005355�00000000000�025161� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2014 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 testtools from glanceclient import client from glanceclient import v1 from glanceclient import v2 class ClientTest(testtools.TestCase): def test_no_endpoint_error(self): self.assertRaises(ValueError, client.Client, None) def test_endpoint(self): gc = client.Client(1, "http://example.com") self.assertEqual("http://example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v1.client.Client) def test_versioned_endpoint(self): gc = client.Client(1, "http://example.com/v2") self.assertEqual("http://example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v1.client.Client) def test_versioned_endpoint_no_version(self): gc = client.Client(endpoint="http://example.com/v2") self.assertEqual("http://example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v2.client.Client) def test_versioned_endpoint_with_minor_revision(self): gc = client.Client(2.2, "http://example.com/v2.1") self.assertEqual("http://example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v2.client.Client) def test_endpoint_with_version_hostname(self): gc = client.Client(2, "http://v1.example.com") self.assertEqual("http://v1.example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v2.client.Client) def test_versioned_endpoint_with_version_hostname_v2(self): gc = client.Client(endpoint="http://v1.example.com/v2") self.assertEqual("http://v1.example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v2.client.Client) def test_versioned_endpoint_with_version_hostname_v1(self): gc = client.Client(endpoint="http://v2.example.com/v1") self.assertEqual("http://v2.example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v1.client.Client) def test_versioned_endpoint_with_minor_revision_and_version_hostname(self): gc = client.Client(endpoint="http://v1.example.com/v2.1") self.assertEqual("http://v1.example.com", gc.http_client.endpoint) self.assertIsInstance(gc, v2.client.Client) �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/tests/unit/test_exc.py���������������������������������������0000664�0001750�0001750�00000005217�00000000000�024457� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools from unittest import mock from glanceclient import exc HTML_MSG = """<html> <head> <title>404 Entity Not Found

404 Entity Not Found

Entity could not be found

""" class TestHTTPExceptions(testtools.TestCase): def test_from_response(self): """exc.from_response should return instance of an HTTP exception.""" mock_resp = mock.Mock() mock_resp.status_code = 400 out = exc.from_response(mock_resp) self.assertIsInstance(out, exc.HTTPBadRequest) def test_handles_json(self): """exc.from_response should not print JSON.""" mock_resp = mock.Mock() mock_resp.status_code = 413 mock_resp.json.return_value = { "overLimit": { "code": 413, "message": "OverLimit Retry...", "details": "Error Details...", "retryAt": "2014-12-03T13:33:06Z" } } mock_resp.headers = { "content-type": "application/json" } err = exc.from_response(mock_resp, "Non-empty body") self.assertIsInstance(err, exc.HTTPOverLimit) self.assertEqual("OverLimit Retry...", err.details) def test_handles_html(self): """exc.from_response should not print HTML.""" mock_resp = mock.Mock() mock_resp.status_code = 404 mock_resp.text = HTML_MSG mock_resp.headers = { "content-type": "text/html" } err = exc.from_response(mock_resp, HTML_MSG) self.assertIsInstance(err, exc.HTTPNotFound) self.assertEqual("404 Entity Not Found: Entity could not be found", err.details) def test_format_no_content_type(self): mock_resp = mock.Mock() mock_resp.status_code = 400 mock_resp.headers = {'content-type': 'application/octet-stream'} body = b'Error \n\n' err = exc.from_response(mock_resp, body) self.assertEqual('Error \n', err.details) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/test_http.py0000664000175000017500000005112300000000000024654 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. import functools import json import logging from unittest import mock import uuid import fixtures import io from keystoneauth1 import session from keystoneauth1 import token_endpoint from oslo_utils import encodeutils import requests from requests_mock.contrib import fixture from urllib import parse from testscenarios import load_tests_apply_scenarios as load_tests # noqa import testtools from testtools import matchers import types import glanceclient from glanceclient.common import http from glanceclient.tests import utils def original_only(f): @functools.wraps(f) def wrapper(self, *args, **kwargs): if not hasattr(self.client, 'log_curl_request'): self.skipTest('Skip logging tests for session client') return f(self, *args, **kwargs) class TestClient(testtools.TestCase): scenarios = [ ('httpclient', {'create_client': '_create_http_client'}), ('session', {'create_client': '_create_session_client'}) ] def _create_http_client(self): return http.HTTPClient(self.endpoint, token=self.token) def _create_session_client(self): auth = token_endpoint.Token(self.endpoint, self.token) sess = session.Session(auth=auth) return http.SessionClient(sess) def setUp(self): super(TestClient, self).setUp() self.mock = self.useFixture(fixture.Fixture()) self.endpoint = 'http://example.com:9292' self.ssl_endpoint = 'https://example.com:9292' self.token = 'abc123' self.client = getattr(self, self.create_client)() def test_identity_headers_and_token(self): identity_headers = { 'X-Auth-Token': 'auth_token', 'X-User-Id': 'user', 'X-Tenant-Id': 'tenant', 'X-Roles': 'roles', 'X-Identity-Status': 'Confirmed', 'X-Service-Catalog': 'service_catalog', } # with token kwargs = {'token': 'fake-token', 'identity_headers': identity_headers} http_client_object = http.HTTPClient(self.endpoint, **kwargs) self.assertEqual('auth_token', http_client_object.auth_token) self.assertTrue(http_client_object.identity_headers. get('X-Auth-Token') is None) def test_identity_headers_and_no_token_in_header(self): identity_headers = { 'X-User-Id': 'user', 'X-Tenant-Id': 'tenant', 'X-Roles': 'roles', 'X-Identity-Status': 'Confirmed', 'X-Service-Catalog': 'service_catalog', } # without X-Auth-Token in identity headers kwargs = {'token': 'fake-token', 'identity_headers': identity_headers} http_client_object = http.HTTPClient(self.endpoint, **kwargs) self.assertEqual('fake-token', http_client_object.auth_token) self.assertTrue(http_client_object.identity_headers. get('X-Auth-Token') is None) def test_identity_headers_and_no_token_in_session_header(self): # Tests that if token or X-Auth-Token are not provided in the kwargs # when creating the http client, the session headers don't contain # the X-Auth-Token key. identity_headers = { 'X-User-Id': 'user', 'X-Tenant-Id': 'tenant', 'X-Roles': 'roles', 'X-Identity-Status': 'Confirmed', 'X-Service-Catalog': 'service_catalog', } kwargs = {'identity_headers': identity_headers} http_client_object = http.HTTPClient(self.endpoint, **kwargs) self.assertIsNone(http_client_object.auth_token) self.assertNotIn('X-Auth-Token', http_client_object.session.headers) def test_identity_headers_are_passed(self): # Tests that if token or X-Auth-Token are not provided in the kwargs # when creating the http client, the session headers don't contain # the X-Auth-Token key. identity_headers = { 'X-User-Id': b'user', 'X-Tenant-Id': b'tenant', 'X-Roles': b'roles', 'X-Identity-Status': b'Confirmed', 'X-Service-Catalog': b'service_catalog', } kwargs = {'identity_headers': identity_headers} http_client = http.HTTPClient(self.endpoint, **kwargs) path = '/v1/images/my-image' self.mock.get(self.endpoint + path) http_client.get(path) headers = self.mock.last_request.headers for k, v in identity_headers.items(): self.assertEqual(v, headers[k]) def test_language_header_passed(self): kwargs = {'language_header': 'nb_NO'} http_client = http.HTTPClient(self.endpoint, **kwargs) path = '/v2/images/my-image' self.mock.get(self.endpoint + path) http_client.get(path) headers = self.mock.last_request.headers self.assertEqual(kwargs['language_header'], headers['Accept-Language']) def test_request_id_header_passed(self): global_id = encodeutils.safe_encode("req-%s" % uuid.uuid4()) kwargs = {'global_request_id': global_id} http_client = http.HTTPClient(self.endpoint, **kwargs) path = '/v2/images/my-image' self.mock.get(self.endpoint + path) http_client.get(path) headers = self.mock.last_request.headers self.assertEqual(global_id, headers['X-OpenStack-Request-ID']) def test_language_header_not_passed_no_language(self): kwargs = {} http_client = http.HTTPClient(self.endpoint, **kwargs) path = '/v2/images/my-image' self.mock.get(self.endpoint + path) http_client.get(path) headers = self.mock.last_request.headers self.assertNotIn('Accept-Language', headers) def test_connection_timeout(self): """Verify a InvalidEndpoint is received if connection times out.""" def cb(request, context): raise requests.exceptions.Timeout path = '/v1/images' self.mock.get(self.endpoint + path, text=cb) comm_err = self.assertRaises(glanceclient.exc.InvalidEndpoint, self.client.get, '/v1/images') self.assertIn(self.endpoint, comm_err.message) def test_connection_refused(self): """Verify a CommunicationError is received if connection is refused. The error should list the host and port that refused the connection. """ def cb(request, context): raise requests.exceptions.ConnectionError() path = '/v1/images/detail?limit=20' self.mock.get(self.endpoint + path, text=cb) comm_err = self.assertRaises(glanceclient.exc.CommunicationError, self.client.get, '/v1/images/detail?limit=20') self.assertIn(self.endpoint, comm_err.message) def test_http_encoding(self): path = '/v1/images/detail' text = 'Ok' self.mock.get(self.endpoint + path, text=text, headers={"Content-Type": "text/plain"}) headers = {"test": 'ni\xf1o'} resp, body = self.client.get(path, headers=headers) self.assertEqual(text, resp.text) def test_headers_encoding(self): value = 'ni\xf1o' fake_location = b'http://web_server:80/images/fake.img' headers = {"test": value, "none-val": None, "Name": "value", "x-image-meta-location": fake_location} encoded = http.encode_headers(headers) # Bug #1766235: According to RFC 8187, headers must be # encoded as 7-bit ASCII, so expect to see only displayable # chars in percent-encoding self.assertEqual(b"ni%C3%B1o", encoded[b"test"]) self.assertNotIn("none-val", encoded) self.assertNotIn(b"none-val", encoded) self.assertEqual(b"value", encoded[b"Name"]) # Bug #1788942: Colons in URL should not get percent-encoded self.assertEqual(fake_location, encoded[b"x-image-meta-location"]) @mock.patch('keystoneauth1.adapter.Adapter.request') def test_http_duplicate_content_type_headers(self, mock_ksarq): """Test proper handling of Content-Type headers. encode_headers() must be called as late as possible before a request is sent. If this principle is violated, and if any changes are made to the headers between encode_headers() and the actual request (for instance a call to _set_common_request_kwargs()), and if you're trying to set a Content-Type that is not equal to application/octet-stream (the default), it is entirely possible that you'll end up with two Content-Type headers defined (yours plus application/octet-stream). The request will go out the door with only one of them chosen seemingly at random. This situation only occurs in python3. This test will never fail in python2. """ path = "/v2/images/my-image" headers = { "Content-Type": "application/openstack-images-v2.1-json-patch" } data = '[{"value": "qcow2", "path": "/disk_format", "op": "replace"}]' self.mock.patch(self.endpoint + path) sess_http_client = self._create_session_client() sess_http_client.patch(path, headers=headers, data=data) # Pull out the headers with which Adapter.request was invoked ksarqh = mock_ksarq.call_args[1]['headers'] # Only one Content-Type header (of any text-type) self.assertEqual(1, [encodeutils.safe_decode(key) for key in ksarqh.keys()].count('Content-Type')) # And it's the one we set self.assertEqual(b"application/openstack-images-v2.1-json-patch", ksarqh[b"Content-Type"]) def test_request_id_header_session_client(self): global_id = "req-%s" % uuid.uuid4() kwargs = {'global_request_id': global_id} auth = token_endpoint.Token(self.endpoint, self.token) sess = session.Session(auth=auth) http_client = http.SessionClient(sess, **kwargs) path = '/v2/images/my-image' self.mock.get(self.endpoint + path) http_client.get(path) headers = self.mock.last_request.headers self.assertEqual(global_id, headers['X-OpenStack-Request-ID']) def test_raw_request(self): """Verify the path being used for HTTP requests reflects accurately.""" headers = {"Content-Type": "text/plain"} text = 'Ok' path = '/v1/images/detail' self.mock.get(self.endpoint + path, text=text, headers=headers) resp, body = self.client.get('/v1/images/detail', headers=headers) self.assertEqual(headers, resp.headers) self.assertEqual(text, resp.text) def test_parse_endpoint(self): endpoint = 'http://example.com:9292' test_client = http.HTTPClient(endpoint, token='adc123') actual = test_client.parse_endpoint(endpoint) expected = parse.SplitResult(scheme='http', netloc='example.com:9292', path='', query='', fragment='') self.assertEqual(expected, actual) def test_get_connections_kwargs_http(self): endpoint = 'http://example.com:9292' test_client = http.HTTPClient(endpoint, token='adc123') self.assertEqual(600.0, test_client.timeout) def test__chunk_body_exact_size_chunk(self): test_client = http._BaseHTTPClient() bytestring = b'x' * http.CHUNKSIZE data = io.BytesIO(bytestring) chunk = list(test_client._chunk_body(data)) self.assertEqual(1, len(chunk)) self.assertEqual([bytestring], chunk) def test_http_chunked_request(self): text = "Ok" data = io.StringIO(text) path = '/v1/images/' self.mock.post(self.endpoint + path, text=text) headers = {"test": 'chunked_request'} resp, body = self.client.post(path, headers=headers, data=data) self.assertIsInstance(self.mock.last_request.body, types.GeneratorType) self.assertEqual(text, resp.text) def test_http_json(self): data = {"test": "json_request"} path = '/v1/images' text = 'OK' self.mock.post(self.endpoint + path, text=text) headers = {"test": 'chunked_request'} resp, body = self.client.post(path, headers=headers, data=data) self.assertEqual(text, resp.text) self.assertIsInstance(self.mock.last_request.body, str) self.assertEqual(data, json.loads(self.mock.last_request.body)) def test_http_chunked_response(self): data = "TEST" path = '/v1/images/' self.mock.get(self.endpoint + path, body=io.StringIO(data), headers={"Content-Type": "application/octet-stream"}) resp, body = self.client.get(path) self.assertIsInstance(body, types.GeneratorType) self.assertEqual([data], list(body)) @original_only def test_log_http_response_with_non_ascii_char(self): try: response = 'Ok' headers = {"Content-Type": "text/plain", "test": "value1\xa5\xa6"} fake = utils.FakeResponse(headers, io.StringIO(response)) self.client.log_http_response(fake) except UnicodeDecodeError as e: self.fail("Unexpected UnicodeDecodeError exception '%s'" % e) @original_only def test_log_curl_request_with_non_ascii_char(self): try: headers = {'header1': 'value1\xa5\xa6'} body = 'examplebody\xa5\xa6' self.client.log_curl_request('GET', '/api/v1/\xa5', headers, body, None) except UnicodeDecodeError as e: self.fail("Unexpected UnicodeDecodeError exception '%s'" % e) @original_only @mock.patch('glanceclient.common.http.LOG.debug') def test_log_curl_request_with_body_and_header(self, mock_log): hd_name = 'header1' hd_val = 'value1' headers = {hd_name: hd_val} body = 'examplebody' self.client.log_curl_request('GET', '/api/v1/', headers, body, None) self.assertTrue(mock_log.called, 'LOG.debug never called') self.assertTrue(mock_log.call_args[0], 'LOG.debug called with no arguments') hd_regex = r".*\s-H\s+'\s*%s\s*:\s*%s\s*'.*" % (hd_name, hd_val) self.assertThat(mock_log.call_args[0][0], matchers.MatchesRegex(hd_regex), 'header not found in curl command') body_regex = r".*\s-d\s+'%s'\s.*" % body self.assertThat(mock_log.call_args[0][0], matchers.MatchesRegex(body_regex), 'body not found in curl command') def _test_log_curl_request_with_certs(self, mock_log, key, cert, cacert): headers = {'header1': 'value1'} http_client_object = http.HTTPClient(self.ssl_endpoint, key_file=key, cert_file=cert, cacert=cacert, token='fake-token') http_client_object.log_curl_request('GET', '/api/v1/', headers, None, None) self.assertTrue(mock_log.called, 'LOG.debug never called') self.assertTrue(mock_log.call_args[0], 'LOG.debug called with no arguments') needles = {'key': key, 'cert': cert, 'cacert': cacert} for option, value in needles.items(): if value: regex = r".*\s--%s\s+('%s'|%s).*" % (option, value, value) self.assertThat(mock_log.call_args[0][0], matchers.MatchesRegex(regex), 'no --%s option in curl command' % option) else: regex = r".*\s--%s\s+.*" % option self.assertThat(mock_log.call_args[0][0], matchers.Not(matchers.MatchesRegex(regex)), 'unexpected --%s option in curl command' % option) @mock.patch('glanceclient.common.http.LOG.debug') def test_log_curl_request_with_all_certs(self, mock_log): self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1', 'cacert2') @mock.patch('glanceclient.common.http.LOG.debug') def test_log_curl_request_with_some_certs(self, mock_log): self._test_log_curl_request_with_certs(mock_log, 'key1', 'cert1', None) @mock.patch('glanceclient.common.http.LOG.debug') def test_log_curl_request_with_insecure_param(self, mock_log): headers = {'header1': 'value1'} http_client_object = http.HTTPClient(self.ssl_endpoint, insecure=True, token='fake-token') http_client_object.log_curl_request('GET', '/api/v1/', headers, None, None) self.assertTrue(mock_log.called, 'LOG.debug never called') self.assertTrue(mock_log.call_args[0], 'LOG.debug called with no arguments') self.assertThat(mock_log.call_args[0][0], matchers.MatchesRegex(r'.*\s-k\s.*'), 'no -k option in curl command') @mock.patch('glanceclient.common.http.LOG.debug') def test_log_curl_request_with_token_header(self, mock_log): fake_token = 'fake-token' headers = {'X-Auth-Token': fake_token} http_client_object = http.HTTPClient(self.endpoint, identity_headers=headers) http_client_object.log_curl_request('GET', '/api/v1/', headers, None, None) self.assertTrue(mock_log.called, 'LOG.debug never called') self.assertTrue(mock_log.call_args[0], 'LOG.debug called with no arguments') token_regex = '.*%s.*' % fake_token self.assertThat(mock_log.call_args[0][0], matchers.Not(matchers.MatchesRegex(token_regex)), 'token found in LOG.debug parameter') def test_log_request_id_once(self): logger = self.useFixture(fixtures.FakeLogger(level=logging.DEBUG)) data = "TEST" path = '/v1/images/' self.mock.get(self.endpoint + path, body=io.StringIO(data), headers={"Content-Type": "application/octet-stream", 'x-openstack-request-id': "1234"}) resp, body = self.client.get(path) self.assertIsInstance(body, types.GeneratorType) self.assertEqual([data], list(body)) expected_log = ("GET call to image " "for http://example.com:9292/v1/images/ " "used request id 1234") self.assertEqual(1, logger.output.count(expected_log)) def test_expired_token_has_changed(self): # instantiate client with some token fake_token = b'fake-token' http_client = http.HTTPClient(self.endpoint, token=fake_token) path = '/v1/images/my-image' self.mock.get(self.endpoint + path) http_client.get(path) headers = self.mock.last_request.headers self.assertEqual(fake_token, headers['X-Auth-Token']) # refresh the token refreshed_token = b'refreshed-token' http_client.auth_token = refreshed_token http_client.get(path) headers = self.mock.last_request.headers self.assertEqual(refreshed_token, headers['X-Auth-Token']) # regression check for bug 1448080 unicode_token = 'ni\xf1o+==' http_client.auth_token = unicode_token http_client.get(path) headers = self.mock.last_request.headers # Bug #1766235: According to RFC 8187, headers must be # encoded as 7-bit ASCII, so expect to see only displayable # chars in percent-encoding. The '+' and '= 'chars will not # be changed. self.assertEqual(b'ni%C3%B1o+==', headers['X-Auth-Token']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/test_progressbar.py0000664000175000017500000000567600000000000026242 0ustar00zuulzuul00000000000000# 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. import io import sys import requests import testtools from glanceclient.common import progressbar from glanceclient.common import utils from glanceclient.tests import utils as test_utils class TestProgressBarWrapper(testtools.TestCase): def test_iter_iterator_display_progress_bar(self): size = 100 # create fake response object to return request-id with iterator resp = requests.Response() resp.headers['x-openstack-request-id'] = 'req-1234' iterator_with_len = utils.IterableWithLength(iter('X' * 100), size) requestid_proxy = utils.RequestIdProxy((iterator_with_len, resp)) saved_stdout = sys.stdout try: sys.stdout = output = test_utils.FakeTTYStdout() # Consume iterator. data = list(progressbar.VerboseIteratorWrapper(requestid_proxy, size)) self.assertEqual(['X'] * 100, data) self.assertEqual( '[%s>] 100%%\n' % ('=' * 29), output.getvalue() ) finally: sys.stdout = saved_stdout def test_iter_file_display_progress_bar(self): size = 98304 file_obj = io.StringIO('X' * size) saved_stdout = sys.stdout try: sys.stdout = output = test_utils.FakeTTYStdout() file_obj = progressbar.VerboseFileWrapper(file_obj, size) chunksize = 1024 chunk = file_obj.read(chunksize) while chunk: chunk = file_obj.read(chunksize) self.assertEqual( '[%s>] 100%%\n' % ('=' * 29), output.getvalue() ) finally: sys.stdout = saved_stdout def test_iter_file_no_tty(self): size = 98304 file_obj = io.StringIO('X' * size) saved_stdout = sys.stdout try: sys.stdout = output = test_utils.FakeNoTTYStdout() file_obj = progressbar.VerboseFileWrapper(file_obj, size) chunksize = 1024 chunk = file_obj.read(chunksize) while chunk: chunk = file_obj.read(chunksize) # If stdout is not a tty progress bar should do nothing. self.assertEqual('', output.getvalue()) finally: sys.stdout = saved_stdout ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/test_shell.py0000664000175000017500000012150200000000000025003 0ustar00zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # Copyright (C) 2013 Yahoo! Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse from collections import OrderedDict import hashlib import io import logging import os import sys import traceback from unittest import mock import uuid import fixtures from keystoneauth1 import exceptions as ks_exc from keystoneauth1 import fixture as ks_fixture from requests_mock.contrib import fixture as rm_fixture from glanceclient.common import utils from glanceclient import exc from glanceclient import shell as openstack_shell from glanceclient.tests.unit.v2.fixtures import image_show_fixture from glanceclient.tests.unit.v2.fixtures import image_versions_fixture from glanceclient.tests import utils as testutils # NOTE (esheffield) Used for the schema caching tests from glanceclient.v2 import schemas as schemas import json DEFAULT_IMAGE_URL = 'http://127.0.0.1:9292/' DEFAULT_IMAGE_URL_INTERNAL = 'http://127.0.0.1:9191/' DEFAULT_USERNAME = 'username' DEFAULT_PAGE_SIZE = 200 DEFAULT_PASSWORD = 'password' DEFAULT_TENANT_ID = 'tenant_id' DEFAULT_TENANT_NAME = 'tenant_name' DEFAULT_PROJECT_ID = '0123456789' DEFAULT_USER_DOMAIN_NAME = 'user_domain_name' DEFAULT_UNVERSIONED_AUTH_URL = 'http://127.0.0.1:5000/' DEFAULT_V2_AUTH_URL = '%sv2.0' % DEFAULT_UNVERSIONED_AUTH_URL DEFAULT_V3_AUTH_URL = '%sv3' % DEFAULT_UNVERSIONED_AUTH_URL DEFAULT_AUTH_TOKEN = ' 3bcc3d3a03f44e3d8377f9247b0ad155' TEST_SERVICE_URL = 'http://127.0.0.1:5000/' DEFAULT_SERVICE_TYPE = 'image' DEFAULT_ENDPOINT_TYPE = 'public' FAKE_V2_ENV = {'OS_USERNAME': DEFAULT_USERNAME, 'OS_PASSWORD': DEFAULT_PASSWORD, 'OS_TENANT_NAME': DEFAULT_TENANT_NAME, 'OS_AUTH_URL': DEFAULT_V2_AUTH_URL, 'OS_IMAGE_URL': DEFAULT_IMAGE_URL} FAKE_V3_ENV = {'OS_USERNAME': DEFAULT_USERNAME, 'OS_PASSWORD': DEFAULT_PASSWORD, 'OS_PROJECT_ID': DEFAULT_PROJECT_ID, 'OS_USER_DOMAIN_NAME': DEFAULT_USER_DOMAIN_NAME, 'OS_AUTH_URL': DEFAULT_V3_AUTH_URL, 'OS_IMAGE_URL': DEFAULT_IMAGE_URL} FAKE_V4_ENV = {'OS_USERNAME': DEFAULT_USERNAME, 'OS_PASSWORD': DEFAULT_PASSWORD, 'OS_PROJECT_ID': DEFAULT_PROJECT_ID, 'OS_USER_DOMAIN_NAME': DEFAULT_USER_DOMAIN_NAME, 'OS_AUTH_URL': DEFAULT_V3_AUTH_URL, 'OS_SERVICE_TYPE': DEFAULT_SERVICE_TYPE, 'OS_ENDPOINT_TYPE': DEFAULT_ENDPOINT_TYPE, 'OS_AUTH_TOKEN': DEFAULT_AUTH_TOKEN} TOKEN_ID = uuid.uuid4().hex V2_TOKEN = ks_fixture.V2Token(token_id=TOKEN_ID) V2_TOKEN.set_scope() _s = V2_TOKEN.add_service('image', name='glance') _s.add_endpoint(DEFAULT_IMAGE_URL) V3_TOKEN = ks_fixture.V3Token() V3_TOKEN.set_project_scope() _s = V3_TOKEN.add_service('image', name='glance') _s.add_standard_endpoints(public=DEFAULT_IMAGE_URL, internal=DEFAULT_IMAGE_URL_INTERNAL) class ShellTest(testutils.TestCase): # auth environment to use auth_env = FAKE_V2_ENV.copy() # expected auth plugin to invoke token_url = DEFAULT_V2_AUTH_URL + '/tokens' # Patch os.environ to avoid required auth info def make_env(self, exclude=None): env = dict((k, v) for k, v in self.auth_env.items() if k != exclude) self.useFixture(fixtures.MonkeyPatch('os.environ', env)) def setUp(self): super(ShellTest, self).setUp() global _old_env _old_env, os.environ = os.environ, self.auth_env self.requests = self.useFixture(rm_fixture.Fixture()) json_list = ks_fixture.DiscoveryList(DEFAULT_UNVERSIONED_AUTH_URL) self.requests.get(DEFAULT_UNVERSIONED_AUTH_URL, json=json_list, status_code=300) json_v2 = {'version': ks_fixture.V2Discovery(DEFAULT_V2_AUTH_URL)} self.requests.get(DEFAULT_V2_AUTH_URL, json=json_v2) json_v3 = {'version': ks_fixture.V3Discovery(DEFAULT_V3_AUTH_URL)} self.requests.get(DEFAULT_V3_AUTH_URL, json=json_v3) self.v2_auth = self.requests.post(DEFAULT_V2_AUTH_URL + '/tokens', json=V2_TOKEN) headers = {'X-Subject-Token': TOKEN_ID} self.v3_auth = self.requests.post(DEFAULT_V3_AUTH_URL + '/auth/tokens', headers=headers, json=V3_TOKEN) global shell, _shell, assert_called, assert_called_anytime _shell = openstack_shell.OpenStackImagesShell() shell = lambda cmd: _shell.main(cmd.split()) def tearDown(self): super(ShellTest, self).tearDown() global _old_env os.environ = _old_env def shell(self, argstr, exitcodes=(0,)): orig = sys.stdout orig_stderr = sys.stderr try: sys.stdout = io.StringIO() sys.stderr = io.StringIO() _shell = openstack_shell.OpenStackImagesShell() _shell.main(argstr.split()) except SystemExit: exc_type, exc_value, exc_traceback = sys.exc_info() self.assertIn(exc_value.code, exitcodes) finally: stdout = sys.stdout.getvalue() sys.stdout.close() sys.stdout = orig stderr = sys.stderr.getvalue() sys.stderr.close() sys.stderr = orig_stderr return (stdout, stderr) def test_help_unknown_command(self): shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 2 help foofoo' self.assertRaises(exc.CommandError, shell.main, argstr.split()) @mock.patch('sys.stdout', io.StringIO()) @mock.patch('sys.stderr', io.StringIO()) @mock.patch('sys.argv', ['glance', 'help', 'foofoo']) def test_no_stacktrace_when_debug_disabled(self): with mock.patch.object(traceback, 'print_exc') as mock_print_exc: try: openstack_shell.main() except SystemExit: pass self.assertFalse(mock_print_exc.called) @mock.patch('sys.stdout', io.StringIO()) @mock.patch('sys.stderr', io.StringIO()) @mock.patch('sys.argv', ['glance', 'help', 'foofoo']) def test_stacktrace_when_debug_enabled_by_env(self): old_environment = os.environ.copy() os.environ = {'GLANCECLIENT_DEBUG': '1'} try: with mock.patch.object(traceback, 'print_exc') as mock_print_exc: try: openstack_shell.main() except SystemExit: pass self.assertTrue(mock_print_exc.called) finally: os.environ = old_environment @mock.patch('sys.stdout', io.StringIO()) @mock.patch('sys.stderr', io.StringIO()) @mock.patch('sys.argv', ['glance', '--debug', 'help', 'foofoo']) def test_stacktrace_when_debug_enabled(self): with mock.patch.object(traceback, 'print_exc') as mock_print_exc: try: openstack_shell.main() except SystemExit: pass self.assertTrue(mock_print_exc.called) def test_help(self): shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 2 help' with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock: actual = shell.main(argstr.split()) self.assertEqual(0, actual) self.assertFalse(et_mock.called) def test_help_no_subcommand(self): shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 2' with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock: actual = shell.main(argstr.split()) self.assertEqual(0, actual) self.assertFalse(et_mock.called) def test_blank_call(self): shell = openstack_shell.OpenStackImagesShell() with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock: actual = shell.main('') self.assertEqual(0, actual) self.assertFalse(et_mock.called) def test_help_on_subcommand_error(self): self.assertRaises(exc.CommandError, shell, '--os-image-api-version 2 help bad') def test_help_v2_no_schema(self): shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 2 help image-create' with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock: actual = shell.main(argstr.split()) self.assertEqual(0, actual) self.assertNotIn('', actual) self.assertFalse(et_mock.called) argstr = '--os-image-api-version 2 help md-namespace-create' with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock: actual = shell.main(argstr.split()) self.assertEqual(0, actual) self.assertNotIn('', actual) self.assertFalse(et_mock.called) argstr = '--os-image-api-version 2 help md-resource-type-associate' with mock.patch.object(shell, '_get_keystone_auth_plugin') as et_mock: actual = shell.main(argstr.split()) self.assertEqual(0, actual) self.assertNotIn('', actual) self.assertFalse(et_mock.called) def test_get_base_parser(self): test_shell = openstack_shell.OpenStackImagesShell() # NOTE(stevemar): Use the current sys.argv for base_parser since it # doesn't matter for this test, it just needs to initialize the CLI actual_parser = test_shell.get_base_parser(sys.argv) description = 'Command-line interface to the OpenStack Images API.' expected = argparse.ArgumentParser( prog='glance', usage=None, description=description, conflict_handler='error', add_help=False, formatter_class=openstack_shell.HelpFormatter,) # NOTE(guochbo): Can't compare ArgumentParser instances directly # Convert ArgumentPaser to string first. self.assertEqual(str(expected), str(actual_parser)) @mock.patch.object(openstack_shell.OpenStackImagesShell, '_get_versioned_client') def test_cert_and_key_args_interchangeable(self, mock_versioned_client): # make sure --os-cert and --os-key are passed correctly args = ('--os-image-api-version 2 ' '--os-cert mycert ' '--os-key mykey image-list') shell(args) assert mock_versioned_client.called ((api_version, args), kwargs) = mock_versioned_client.call_args self.assertEqual('mycert', args.os_cert) self.assertEqual('mykey', args.os_key) @mock.patch('glanceclient.v1.client.Client') def test_no_auth_with_token_and_image_url_with_v1(self, v1_client): # test no authentication is required if both token and endpoint url # are specified args = ('--os-image-api-version 1 --os-auth-token mytoken' ' --os-image-url https://image:1234/v1 image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) assert v1_client.called (args, kwargs) = v1_client.call_args self.assertEqual('mytoken', kwargs['token']) self.assertEqual('https://image:1234', args[0]) @mock.patch('glanceclient.v2.client.Client') def test_no_auth_with_token_and_image_url_with_v2(self, v2_client): # test no authentication is required if both token and endpoint url # are specified args = ('--os-image-api-version 2 --os-auth-token mytoken ' '--os-image-url https://image:1234 image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertTrue(v2_client.called) (args, kwargs) = v2_client.call_args self.assertEqual('mytoken', kwargs['token']) self.assertEqual('https://image:1234', args[0]) def _assert_auth_plugin_args(self): # make sure our auth plugin is invoked with the correct args self.assertFalse(self.v3_auth.called) body = json.loads(self.v2_auth.last_request.body) self.assertEqual(self.auth_env['OS_TENANT_NAME'], body['auth']['tenantName']) self.assertEqual(self.auth_env['OS_USERNAME'], body['auth']['passwordCredentials']['username']) self.assertEqual(self.auth_env['OS_PASSWORD'], body['auth']['passwordCredentials']['password']) @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', return_value=False) @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_without_version(self, v2_client, cache_schemas): cli2 = mock.MagicMock() v2_client.return_value = cli2 cli2.http_client.get.return_value = (None, {'versions': [{'id': 'v2'}]}) args = 'image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) # NOTE(flaper87): this currently calls auth twice since it'll # authenticate to get the version list *and* to execute the command. # This is not the ideal behavior and it should be fixed in a follow # up patch. @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_with_v1(self, v1_client): args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(0, self.v2_auth.call_count) @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_with_v2(self, v2_client): args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(0, self.v2_auth.call_count) @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_with_unversioned_auth_url_with_v1( self, v1_client): args = ('--os-image-api-version 1 --os-auth-url %s image-list' % DEFAULT_UNVERSIONED_AUTH_URL) glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) @mock.patch('glanceclient.v2.client.Client') @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', return_value=False) def test_auth_plugin_invocation_with_unversioned_auth_url_with_v2( self, v2_client, cache_schemas): args = ('--os-auth-url %s --os-image-api-version 2 ' 'image-list') % DEFAULT_UNVERSIONED_AUTH_URL glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) @mock.patch('glanceclient.Client') def test_endpoint_token_no_auth_req(self, mock_client): def verify_input(version=None, endpoint=None, *args, **kwargs): self.assertIn('token', kwargs) self.assertEqual(TOKEN_ID, kwargs['token']) self.assertEqual(DEFAULT_IMAGE_URL, endpoint) return mock.MagicMock() mock_client.side_effect = verify_input glance_shell = openstack_shell.OpenStackImagesShell() args = ['--os-image-api-version', '2', '--os-auth-token', TOKEN_ID, '--os-image-url', DEFAULT_IMAGE_URL, 'image-list'] glance_shell.main(args) self.assertEqual(1, mock_client.call_count) @mock.patch('sys.stdin', side_effect=mock.MagicMock) @mock.patch('getpass.getpass', side_effect=EOFError) @mock.patch('glanceclient.v2.client.Client') def test_password_prompted_ctrlD_with_v2(self, v2_client, mock_getpass, mock_stdin): cli2 = mock.MagicMock() v2_client.return_value = cli2 cli2.http_client.get.return_value = (None, {'versions': []}) glance_shell = openstack_shell.OpenStackImagesShell() self.make_env(exclude='OS_PASSWORD') # We should get Command Error because we mock Ctl-D. self.assertRaises(exc.CommandError, glance_shell.main, ['image-list']) # Make sure we are actually prompted. mock_getpass.assert_called_with('OS Password: ') @mock.patch( 'glanceclient.shell.OpenStackImagesShell._get_keystone_auth_plugin') @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', return_value=False) def test_no_auth_with_proj_name(self, cache_schemas, session): with mock.patch('glanceclient.v2.client.Client'): args = ('--os-project-name myname ' '--os-project-domain-name mydomain ' '--os-project-domain-id myid ' '--os-image-api-version 2 image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) ((args), kwargs) = session.call_args self.assertEqual('myname', kwargs['project_name']) self.assertEqual('mydomain', kwargs['project_domain_name']) self.assertEqual('myid', kwargs['project_domain_id']) @mock.patch.object(openstack_shell.OpenStackImagesShell, 'main') def test_shell_keyboard_interrupt(self, mock_glance_shell): # Ensure that exit code is 130 for KeyboardInterrupt try: mock_glance_shell.side_effect = KeyboardInterrupt() openstack_shell.main() except SystemExit as ex: self.assertEqual(130, ex.code) @mock.patch('glanceclient.common.utils.exit', side_effect=utils.exit) def test_shell_illegal_version(self, mock_exit): # Only int versions are allowed on cli shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 1.1 image-list' try: shell.main(argstr.split()) except SystemExit as ex: self.assertEqual(1, ex.code) msg = ("Invalid API version parameter. " "Supported values are %s" % openstack_shell.SUPPORTED_VERSIONS) mock_exit.assert_called_with(msg=msg) @mock.patch('glanceclient.common.utils.exit', side_effect=utils.exit) def test_shell_unsupported_version(self, mock_exit): # Test an integer version which is not supported (-1) shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version -1 image-list' try: shell.main(argstr.split()) except SystemExit as ex: self.assertEqual(1, ex.code) msg = ("Invalid API version parameter. " "Supported values are %s" % openstack_shell.SUPPORTED_VERSIONS) mock_exit.assert_called_with(msg=msg) @mock.patch.object(openstack_shell.OpenStackImagesShell, 'get_subcommand_parser') def test_shell_import_error_with_mesage(self, mock_parser): msg = 'Unable to import module xxx' mock_parser.side_effect = ImportError('%s' % msg) shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 2 image-list' try: shell.main(argstr.split()) self.fail('No import error returned') except ImportError as e: self.assertEqual(msg, str(e)) @mock.patch.object(openstack_shell.OpenStackImagesShell, 'get_subcommand_parser') def test_shell_import_error_default_message(self, mock_parser): mock_parser.side_effect = ImportError shell = openstack_shell.OpenStackImagesShell() argstr = '--os-image-api-version 2 image-list' try: shell.main(argstr.split()) self.fail('No import error returned') except ImportError as e: msg = 'Unable to import module. Re-run with --debug for more info.' self.assertEqual(msg, str(e)) @mock.patch('glanceclient.v2.client.Client') @mock.patch('glanceclient.v1.images.ImageManager.list') def test_shell_v1_fallback_from_v2(self, v1_imgs, v2_client): self.make_env() cli2 = mock.MagicMock() v2_client.return_value = cli2 cli2.http_client.get.return_value = (None, {'versions': []}) args = 'image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertFalse(cli2.schemas.get.called) self.assertTrue(v1_imgs.called) @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas') @mock.patch('glanceclient.v2.client.Client') def test_shell_no_fallback_from_v2(self, v2_client, cache_schemas): self.make_env() cli2 = mock.MagicMock() v2_client.return_value = cli2 cli2.http_client.get.return_value = (None, {'versions': [{'id': 'v2'}]}) cache_schemas.return_value = False args = 'image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertTrue(cli2.images.list.called) @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_without_username_with_v1(self, v1_client): self.make_env(exclude='OS_USERNAME') args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_without_username_with_v2(self, v2_client): self.make_env(exclude='OS_USERNAME') args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_without_auth_url_with_v1(self, v1_client): self.make_env(exclude='OS_AUTH_URL') args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_without_auth_url_with_v2(self, v2_client): self.make_env(exclude='OS_AUTH_URL') args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_without_tenant_with_v1(self, v1_client): if 'OS_TENANT_NAME' in os.environ: self.make_env(exclude='OS_TENANT_NAME') if 'OS_PROJECT_ID' in os.environ: self.make_env(exclude='OS_PROJECT_ID') args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('glanceclient.v2.client.Client') @mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas', return_value=False) def test_auth_plugin_invocation_without_tenant_with_v2(self, v2_client, cache_schemas): if 'OS_TENANT_NAME' in os.environ: self.make_env(exclude='OS_TENANT_NAME') if 'OS_PROJECT_ID' in os.environ: self.make_env(exclude='OS_PROJECT_ID') args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) @mock.patch('sys.argv', ['glance']) @mock.patch('sys.stdout', io.StringIO()) @mock.patch('sys.stderr', io.StringIO()) def test_main_noargs(self): # Ensure that main works with no command-line arguments try: openstack_shell.main() except SystemExit: self.fail('Unexpected SystemExit') # We expect the normal v2 usage as a result expected = ['Command-line interface to the OpenStack Images API', 'image-list', 'image-deactivate', 'location-add'] for output in expected: self.assertIn(output, sys.stdout.getvalue()) @mock.patch('glanceclient.v2.client.Client') @mock.patch('glanceclient.v1.shell.do_image_list') @mock.patch('glanceclient.shell.logging.basicConfig') def test_setup_debug(self, conf, func, v2_client): cli2 = mock.MagicMock() v2_client.return_value = cli2 cli2.http_client.get.return_value = (None, {'versions': []}) args = '--debug image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) glance_logger = logging.getLogger('glanceclient') self.assertEqual(glance_logger.getEffectiveLevel(), logging.DEBUG) conf.assert_called_with(level=logging.DEBUG) def test_subcommand_help(self): # Ensure that main works with sub command help stdout, stderr = self.shell('help stores-delete') expected = 'usage: glance stores-delete --store ' \ '\n\nDelete image from specific store.' \ '\n\nPositional arguments:\n ' \ 'ID of image to update.\n\nRequired arguments:\n ' \ '--store Store to delete image from.\n' self.assertEqual(expected, stdout) class ShellTestWithKeystoneV3Auth(ShellTest): # auth environment to use auth_env = FAKE_V3_ENV.copy() token_url = DEFAULT_V3_AUTH_URL + '/auth/tokens' def _assert_auth_plugin_args(self): self.assertFalse(self.v2_auth.called) body = json.loads(self.v3_auth.last_request.body) user = body['auth']['identity']['password']['user'] self.assertEqual(self.auth_env['OS_USERNAME'], user['name']) self.assertEqual(self.auth_env['OS_PASSWORD'], user['password']) self.assertEqual(self.auth_env['OS_USER_DOMAIN_NAME'], user['domain']['name']) self.assertEqual(self.auth_env['OS_PROJECT_ID'], body['auth']['scope']['project']['id']) @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_with_v1(self, v1_client): args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(0, self.v3_auth.call_count) @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_with_v2(self, v2_client): args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(0, self.v3_auth.call_count) @mock.patch('keystoneauth1.discover.Discover', side_effect=ks_exc.ClientException()) def test_api_discovery_failed_with_unversioned_auth_url(self, discover): args = ('--os-image-api-version 2 --os-auth-url %s image-list' % DEFAULT_UNVERSIONED_AUTH_URL) glance_shell = openstack_shell.OpenStackImagesShell() self.assertRaises(exc.CommandError, glance_shell.main, args.split()) def test_bash_completion(self): stdout, stderr = self.shell('--os-image-api-version 2 bash_completion') # just check we have some output required = [ '--status', 'image-create', 'help', '--size'] for r in required: self.assertIn(r, stdout.split()) avoided = [ 'bash_completion', 'bash-completion'] for r in avoided: self.assertNotIn(r, stdout.split()) class ShellTestWithNoOSImageURLPublic(ShellTestWithKeystoneV3Auth): # auth environment to use # default uses public auth_env = FAKE_V4_ENV.copy() def setUp(self): super(ShellTestWithNoOSImageURLPublic, self).setUp() self.image_url = DEFAULT_IMAGE_URL self.requests.get(DEFAULT_IMAGE_URL + 'v2/images', text='{"images": []}') @mock.patch('glanceclient.v1.client.Client') def test_auth_plugin_invocation_with_v1(self, v1_client): args = '--os-image-api-version 1 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(1, self.v3_auth.call_count) self._assert_auth_plugin_args() @mock.patch('glanceclient.v2.client.Client') def test_auth_plugin_invocation_with_v2(self, v2_client): args = '--os-image-api-version 2 image-list' glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(1, self.v3_auth.call_count) self._assert_auth_plugin_args() @mock.patch('glanceclient.v2.client.Client') def test_endpoint_from_interface(self, v2_client): args = ('--os-image-api-version 2 image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) assert v2_client.called (args, kwargs) = v2_client.call_args self.assertEqual(self.image_url, kwargs['endpoint_override']) def test_endpoint_real_from_interface(self): args = ('--os-image-api-version 2 image-list') glance_shell = openstack_shell.OpenStackImagesShell() glance_shell.main(args.split()) self.assertEqual(self.requests.request_history[2].url, self.image_url + "v2/images?" f"limit={DEFAULT_PAGE_SIZE}&" "sort_key=name&sort_dir=asc") class ShellTestWithNoOSImageURLInternal(ShellTestWithNoOSImageURLPublic): # auth environment to use # this uses internal FAKE_V5_ENV = FAKE_V4_ENV.copy() FAKE_V5_ENV['OS_ENDPOINT_TYPE'] = 'internal' auth_env = FAKE_V5_ENV.copy() def setUp(self): super(ShellTestWithNoOSImageURLPublic, self).setUp() self.image_url = DEFAULT_IMAGE_URL_INTERNAL self.requests.get(DEFAULT_IMAGE_URL_INTERNAL + 'v2/images', text='{"images": []}') class ShellCacheSchemaTest(testutils.TestCase): def setUp(self): super(ShellCacheSchemaTest, self).setUp() self._mock_client_setup() self._mock_shell_setup() self.cache_dir = '/dir_for_cached_schema' self.os_auth_url = 'http://localhost:5000/v2' url_hex = hashlib.sha1(self.os_auth_url.encode('utf-8')).hexdigest() self.prefix_path = (self.cache_dir + '/' + url_hex) self.cache_files = [self.prefix_path + '/image_schema.json', self.prefix_path + '/namespace_schema.json', self.prefix_path + '/resource_type_schema.json'] def tearDown(self): super(ShellCacheSchemaTest, self).tearDown() def _mock_client_setup(self): self.schema_dict = { 'name': 'image', 'properties': { 'name': {'type': 'string', 'description': 'Name of image'}, }, } self.client = mock.Mock() schema_odict = OrderedDict(self.schema_dict) self.client.schemas.get.return_value = schemas.Schema(schema_odict) def _mock_shell_setup(self): self.shell = openstack_shell.OpenStackImagesShell() self.shell._get_versioned_client = mock.create_autospec( self.shell._get_versioned_client, return_value=self.client, spec_set=True ) def _make_args(self, args): class Args(object): def __init__(self, entries): self.__dict__.update(entries) return Args(args) @mock.patch('builtins.open', new=mock.mock_open(), create=True) @mock.patch('os.path.exists', return_value=True) def test_cache_schemas_gets_when_forced(self, exists_mock): options = { 'get_schema': True, 'os_auth_url': self.os_auth_url } schema_odict = OrderedDict(self.schema_dict) args = self._make_args(options) client = self.shell._get_versioned_client('2', args) self.shell._cache_schemas(args, client, home_dir=self.cache_dir) self.assertEqual(12, open.mock_calls.__len__()) self.assertEqual(mock.call(self.cache_files[0], 'w'), open.mock_calls[0]) self.assertEqual(mock.call(self.cache_files[1], 'w'), open.mock_calls[4]) actual = json.loads(open.mock_calls[2][1][0]) self.assertEqual(schema_odict, actual) actual = json.loads(open.mock_calls[6][1][0]) self.assertEqual(schema_odict, actual) @mock.patch('builtins.open', new=mock.mock_open(), create=True) @mock.patch('os.path.exists', side_effect=[True, False, False, False]) def test_cache_schemas_gets_when_not_exists(self, exists_mock): options = { 'get_schema': False, 'os_auth_url': self.os_auth_url } schema_odict = OrderedDict(self.schema_dict) args = self._make_args(options) client = self.shell._get_versioned_client('2', args) self.shell._cache_schemas(args, client, home_dir=self.cache_dir) self.assertEqual(12, open.mock_calls.__len__()) self.assertEqual(mock.call(self.cache_files[0], 'w'), open.mock_calls[0]) self.assertEqual(mock.call(self.cache_files[1], 'w'), open.mock_calls[4]) actual = json.loads(open.mock_calls[2][1][0]) self.assertEqual(schema_odict, actual) actual = json.loads(open.mock_calls[6][1][0]) self.assertEqual(schema_odict, actual) @mock.patch('builtins.open', new=mock.mock_open(), create=True) @mock.patch('os.path.exists', return_value=True) def test_cache_schemas_leaves_when_present_not_forced(self, exists_mock): options = { 'get_schema': False, 'os_auth_url': self.os_auth_url } client = mock.MagicMock() self.shell._cache_schemas(self._make_args(options), client, home_dir=self.cache_dir) exists_mock.assert_has_calls([ mock.call(self.prefix_path), mock.call(self.cache_files[0]), mock.call(self.cache_files[1]), mock.call(self.cache_files[2]) ]) self.assertEqual(4, exists_mock.call_count) self.assertEqual(0, open.mock_calls.__len__()) @mock.patch('builtins.open', new=mock.mock_open(), create=True) @mock.patch('os.path.exists', return_value=True) def test_cache_schemas_leaves_auto_switch(self, exists_mock): options = { 'get_schema': True, 'os_auth_url': self.os_auth_url } self.client.schemas.get.return_value = Exception() client = mock.MagicMock() switch_version = self.shell._cache_schemas(self._make_args(options), client, home_dir=self.cache_dir) self.assertEqual(True, switch_version) class ShellTestRequests(testutils.TestCase): """Shell tests using the requests mock library.""" def _make_args(self, args): # NOTE(venkatesh): this conversion from a dict to an object # is required because the test_shell.do_xxx(gc, args) methods # expects the args to be attributes of an object. If passed as # dict directly, it throws an AttributeError. class Args(object): def __init__(self, entries): self.__dict__.update(entries) return Args(args) def setUp(self): super(ShellTestRequests, self).setUp() self._old_env = os.environ os.environ = {} def tearDown(self): super(ShellTestRequests, self).tearDown() os.environ = self._old_env def test_download_has_no_stray_output_to_stdout(self): """Regression test for bug 1488914""" saved_stdout = sys.stdout try: sys.stdout = output = testutils.FakeNoTTYStdout() id = image_show_fixture['id'] self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/versions', json=image_versions_fixture) headers = {'Content-Length': '4', 'Content-type': 'application/octet-stream'} fake = testutils.FakeResponse(headers, io.StringIO('DATA')) self.requests.get('http://example.com/v1/images/%s' % id, raw=fake) self.requests.get('http://example.com/v1/images/detail' '?sort_key=name&sort_dir=asc&limit=20') headers = {'X-Image-Meta-Id': id} self.requests.head('http://example.com/v1/images/%s' % id, headers=headers) with mock.patch.object(openstack_shell.OpenStackImagesShell, '_cache_schemas') as mocked_cache_schema: mocked_cache_schema.return_value = True shell = openstack_shell.OpenStackImagesShell() argstr = ('--os-auth-token faketoken ' '--os-image-url http://example.com ' 'image-download %s' % id) shell.main(argstr.split()) self.assertTrue(mocked_cache_schema.called) # Ensure we have *only* image data self.assertEqual('DATA', output.getvalue()) finally: sys.stdout = saved_stdout def test_v1_download_has_no_stray_output_to_stdout(self): """Ensure no stray print statements corrupt the image""" saved_stdout = sys.stdout try: sys.stdout = output = testutils.FakeNoTTYStdout() id = image_show_fixture['id'] self.requests = self.useFixture(rm_fixture.Fixture()) headers = {'X-Image-Meta-Id': id} self.requests.head('http://example.com/v1/images/%s' % id, headers=headers) headers = {'Content-Length': '4', 'Content-type': 'application/octet-stream'} fake = testutils.FakeResponse(headers, io.StringIO('DATA')) self.requests.get('http://example.com/v1/images/%s' % id, headers=headers, raw=fake) shell = openstack_shell.OpenStackImagesShell() argstr = ('--os-image-api-version 1 --os-auth-token faketoken ' '--os-image-url http://example.com ' 'image-download %s' % id) shell.main(argstr.split()) # Ensure we have *only* image data self.assertEqual('DATA', output.getvalue()) finally: sys.stdout = saved_stdout def test_v2_download_has_no_stray_output_to_stdout(self): """Ensure no stray print statements corrupt the image""" saved_stdout = sys.stdout try: sys.stdout = output = testutils.FakeNoTTYStdout() id = image_show_fixture['id'] headers = {'Content-Length': '4', 'Content-type': 'application/octet-stream'} fake = testutils.FakeResponse(headers, io.StringIO('DATA')) self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/v2/images/%s/file' % id, headers=headers, raw=fake) self.requests.get('http://example.com/v2/images/%s' % id, headers={'Content-type': 'application/json'}, json=image_show_fixture) shell = openstack_shell.OpenStackImagesShell() argstr = ('--os-image-api-version 2 --os-auth-token faketoken ' '--os-image-url http://example.com ' 'image-download %s' % id) shell.main(argstr.split()) # Ensure we have *only* image data self.assertEqual('DATA', output.getvalue()) finally: sys.stdout = saved_stdout ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/test_ssl.py0000664000175000017500000002140400000000000024475 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. import os from unittest import mock import ssl import testtools import threading from glanceclient import Client from glanceclient import exc from glanceclient import v1 from glanceclient import v2 import socketserver TEST_VAR_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), 'var')) class ThreadedTCPRequestHandler(socketserver.BaseRequestHandler): def handle(self): self.request.recv(1024) response = b'somebytes' self.request.sendall(response) class ThreadedTCPServer(socketserver.ThreadingMixIn, socketserver.TCPServer): def get_request(self): key_file = os.path.join(TEST_VAR_DIR, 'privatekey.key') cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') (_sock, addr) = socketserver.TCPServer.get_request(self) sock = ssl.wrap_socket(_sock, certfile=cert_file, keyfile=key_file, ca_certs=cacert, server_side=True, cert_reqs=ssl.CERT_REQUIRED) return sock, addr class TestHTTPSVerifyCert(testtools.TestCase): """Check 'requests' based ssl verification occurs. The requests library performs SSL certificate validation, however there is still a need to check that the glance client is properly integrated with requests so that cert validation actually happens. """ def setUp(self): # Rather than spinning up a new process, we create # a thread to perform client/server interaction. # This should run more quickly. super(TestHTTPSVerifyCert, self).setUp() server = ThreadedTCPServer(('127.0.0.1', 0), ThreadedTCPRequestHandler) __, self.port = server.server_address server_thread = threading.Thread(target=server.serve_forever) server_thread.daemon = True server_thread.start() @mock.patch('sys.stderr') def test_v1_requests_cert_verification(self, __): """v1 regression test for bug 115260.""" port = self.port url = 'https://0.0.0.0:%d' % port try: client = v1.Client(url, insecure=False, ssl_compression=True) client.images.get('image123') self.fail('No SSL exception has been raised') except exc.CommunicationError as e: if 'certificate verify failed' not in e.message: self.fail('No certificate failure message is received') @mock.patch('sys.stderr') def test_v1_requests_cert_verification_no_compression(self, __): """v1 regression test for bug 115260.""" # Legacy test. Verify 'no compression' has no effect port = self.port url = 'https://0.0.0.0:%d' % port try: client = v1.Client(url, insecure=False, ssl_compression=False) client.images.get('image123') self.fail('No SSL exception has been raised') except exc.CommunicationError as e: if 'certificate verify failed' not in e.message: self.fail('No certificate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_cert_verification(self, __): """v2 regression test for bug 115260.""" port = self.port url = 'https://0.0.0.0:%d' % port try: gc = v2.Client(url, insecure=False, ssl_compression=True) gc.images.get('image123') self.fail('No SSL exception has been raised') except exc.CommunicationError as e: if 'certificate verify failed' not in e.message: self.fail('No certificate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_cert_verification_no_compression(self, __): """v2 regression test for bug 115260.""" # Legacy test. Verify 'no compression' has no effect port = self.port url = 'https://0.0.0.0:%d' % port try: gc = v2.Client(url, insecure=False, ssl_compression=False) gc.images.get('image123') self.fail('No SSL exception has been raised') except exc.CommunicationError as e: if 'certificate verify failed' not in e.message: self.fail('No certificate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_valid_cert_verification(self, __): """Test absence of SSL key file.""" port = self.port url = 'https://0.0.0.0:%d' % port cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') try: gc = Client('2', url, insecure=False, ssl_compression=True, cacert=cacert) gc.images.get('image123') except exc.CommunicationError as e: if 'certificate verify failed' in e.message: self.fail('Certificate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_valid_cert_verification_no_compression(self, __): """Test VerifiedHTTPSConnection: absence of SSL key file.""" port = self.port url = 'https://0.0.0.0:%d' % port cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') try: gc = Client('2', url, insecure=False, ssl_compression=False, cacert=cacert) gc.images.get('image123') except exc.CommunicationError as e: if 'certificate verify failed' in e.message: self.fail('Certificate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_valid_cert_no_key(self, __): """Test VerifiedHTTPSConnection: absence of SSL key file.""" port = self.port url = 'https://0.0.0.0:%d' % port cert_file = os.path.join(TEST_VAR_DIR, 'certificate.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') try: gc = Client('2', url, insecure=False, ssl_compression=False, cert_file=cert_file, cacert=cacert) gc.images.get('image123') except exc.CommunicationError as e: if ('PEM lib' not in e.message): self.fail('No appropriate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_bad_cert(self, __): """Test VerifiedHTTPSConnection: absence of SSL key file.""" port = self.port url = 'https://0.0.0.0:%d' % port cert_file = os.path.join(TEST_VAR_DIR, 'badcert.crt') cacert = os.path.join(TEST_VAR_DIR, 'ca.crt') try: gc = Client('2', url, insecure=False, ssl_compression=False, cert_file=cert_file, cacert=cacert) gc.images.get('image123') except exc.CommunicationError as e: # NOTE(dsariel) # starting from python 2.7.8 the way to handle loading private # keys into the SSL_CTX was changed and error message become # similar to the one in 3.X if 'PEM lib' not in e.message: self.fail('No appropriate failure message is received') @mock.patch('sys.stderr') def test_v2_requests_bad_ca(self, __): """Test VerifiedHTTPSConnection: absence of SSL key file.""" port = self.port url = 'https://0.0.0.0:%d' % port cacert = os.path.join(TEST_VAR_DIR, 'badca.crt') try: gc = Client('2', url, insecure=False, ssl_compression=False, cacert=cacert) gc.images.get('image123') except exc.CommunicationError as e: if 'invalid path' not in e.message: raise ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/test_utils.py0000664000175000017500000002603000000000000025034 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. import io import sys from unittest import mock from oslo_utils import encodeutils from requests import Response # NOTE(jokke): simplified transition to py3, behaves like py2 xrange import testtools from glanceclient.common import utils REQUEST_ID = 'req-1234' def create_response_obj_with_req_id(req_id): resp = Response() resp.headers['x-openstack-request-id'] = req_id return resp class TestUtils(testtools.TestCase): def test_make_size_human_readable(self): self.assertEqual("106B", utils.make_size_human_readable(106)) self.assertEqual("1000kB", utils.make_size_human_readable(1024000)) self.assertEqual("1MB", utils.make_size_human_readable(1048576)) self.assertEqual("1.4GB", utils.make_size_human_readable(1476395008)) self.assertEqual("9.3MB", utils.make_size_human_readable(9761280)) self.assertEqual("0B", utils.make_size_human_readable(None)) def test_get_new_file_size(self): size = 98304 file_obj = io.StringIO('X' * size) try: self.assertEqual(size, utils.get_file_size(file_obj)) # Check that get_file_size didn't change original file position. self.assertEqual(0, file_obj.tell()) finally: file_obj.close() def test_get_consumed_file_size(self): size, consumed = 98304, 304 file_obj = io.StringIO('X' * size) file_obj.seek(consumed) try: self.assertEqual(size, utils.get_file_size(file_obj)) # Check that get_file_size didn't change original file position. self.assertEqual(consumed, file_obj.tell()) finally: file_obj.close() def test_prettytable(self): class Struct(object): def __init__(self, **entries): self.__dict__.update(entries) # test that the prettytable output is wellformatted (left-aligned) columns = ['ID', 'Name'] val = ['Name1', 'another', 'veeeery long'] images = [Struct(**{'id': i ** 16, 'name': val[i]}) for i in range(len(val))] saved_stdout = sys.stdout try: sys.stdout = output_list = io.StringIO() utils.print_list(images, columns) sys.stdout = output_dict = io.StringIO() utils.print_dict({'K': 'k', 'Key': 'veeeeeeeeeeeeeeeeeeeeeeee' 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' 'eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee' 'eeeeeeeeeeeery long value'}, max_column_width=60) finally: sys.stdout = saved_stdout self.assertEqual('''\ +-------+--------------+ | ID | Name | +-------+--------------+ | | Name1 | | 1 | another | | 65536 | veeeery long | +-------+--------------+ ''', output_list.getvalue()) self.assertEqual('''\ +----------+--------------------------------------------------------------+ | Property | Value | +----------+--------------------------------------------------------------+ | K | k | | Key | veeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee | | | eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee | | | ery long value | +----------+--------------------------------------------------------------+ ''', output_dict.getvalue()) def test_print_list_with_list_no_unicode(self): class Struct(object): def __init__(self, **entries): self.__dict__.update(entries) # test for removing 'u' from lists in print_list output columns = ['ID', 'Tags'] images = [Struct(**{'id': 'b8e1c57e-907a-4239-aed8-0df8e54b8d2d', 'tags': ['Name1', 'Tag_123', 'veeeery long']})] saved_stdout = sys.stdout try: sys.stdout = output_list = io.StringIO() utils.print_list(images, columns) finally: sys.stdout = saved_stdout self.assertEqual('''\ +--------------------------------------+--------------------------------------+ | ID | Tags | +--------------------------------------+--------------------------------------+ | b8e1c57e-907a-4239-aed8-0df8e54b8d2d | ['Name1', 'Tag_123', 'veeeery long'] | +--------------------------------------+--------------------------------------+ ''', output_list.getvalue()) def test_print_image_virtual_size_available(self): image = {'id': '42', 'virtual_size': 1337} saved_stdout = sys.stdout try: sys.stdout = output_list = io.StringIO() utils.print_image(image) finally: sys.stdout = saved_stdout self.assertEqual('''\ +--------------+-------+ | Property | Value | +--------------+-------+ | id | 42 | | virtual_size | 1337 | +--------------+-------+ ''', output_list.getvalue()) def test_print_image_virtual_size_not_available(self): image = {'id': '42', 'virtual_size': None} saved_stdout = sys.stdout try: sys.stdout = output_list = io.StringIO() utils.print_image(image) finally: sys.stdout = saved_stdout self.assertEqual('''\ +--------------+---------------+ | Property | Value | +--------------+---------------+ | id | 42 | | virtual_size | Not available | +--------------+---------------+ ''', output_list.getvalue()) def test_schema_args_with_list_types(self): # NOTE(flaper87): Regression for bug # https://bugs.launchpad.net/python-glanceclient/+bug/1401032 def schema_getter(_type='string', enum=False): prop = { 'type': ['null', _type], 'description': 'Test schema', } prop_readonly = { 'type': ['null', _type], 'readOnly': True, 'description': 'Test schema read-only', } if enum: prop['enum'] = [None, 'opt-1', 'opt-2'] prop_readonly['enum'] = [None, 'opt-ro-1', 'opt-ro-2'] def actual_getter(): return { 'additionalProperties': False, 'required': ['name'], 'name': 'test_schema', 'properties': { 'test': prop, 'readonly-test': prop_readonly, } } return actual_getter def dummy_func(): pass decorated = utils.schema_args(schema_getter())(dummy_func) self.assertEqual(len(decorated.__dict__['arguments']), 1) arg, opts = decorated.__dict__['arguments'][0] self.assertIn('--test', arg) self.assertEqual(encodeutils.safe_decode, opts['type']) decorated = utils.schema_args(schema_getter('integer'))(dummy_func) arg, opts = decorated.__dict__['arguments'][0] self.assertIn('--test', arg) self.assertEqual(int, opts['type']) decorated = utils.schema_args(schema_getter(enum=True))(dummy_func) arg, opts = decorated.__dict__['arguments'][0] self.assertIn('--test', arg) self.assertEqual(encodeutils.safe_decode, opts['type']) self.assertIn('None, opt-1, opt-2', opts['help']) # Make sure we use strict checking for boolean values. decorated = utils.schema_args(schema_getter('boolean'))(dummy_func) arg, opts = decorated.__dict__['arguments'][0] type_function = opts['type'] self.assertEqual(type_function('False'), False) self.assertEqual(type_function('True'), True) self.assertRaises(ValueError, type_function, 'foo') def test_iterable_closes(self): # Regression test for bug 1461678. def _iterate(i): for chunk in i: raise(IOError) data = io.StringIO('somestring') data.close = mock.Mock() i = utils.IterableWithLength(data, 10) self.assertRaises(IOError, _iterate, i) data.close.assert_called_with() def test_safe_header(self): self.assertEqual(('somekey', 'somevalue'), utils.safe_header('somekey', 'somevalue')) self.assertEqual(('somekey', None), utils.safe_header('somekey', None)) for sensitive_header in utils.SENSITIVE_HEADERS: (name, value) = utils.safe_header( sensitive_header, encodeutils.safe_encode('somestring')) self.assertEqual(sensitive_header, name) self.assertTrue(value.startswith("{SHA1}")) (name, value) = utils.safe_header(sensitive_header, None) self.assertEqual(sensitive_header, name) self.assertIsNone(value) def test_generator_proxy(self): def _test_decorator(): i = 1 resp = create_response_obj_with_req_id(REQUEST_ID) while True: yield i, resp i += 1 gen_obj = _test_decorator() proxy = utils.GeneratorProxy(gen_obj) # Proxy object should succeed in behaving as the # wrapped object self.assertIsInstance(proxy, type(gen_obj)) # Initially request_ids should be empty self.assertEqual([], proxy.request_ids) # Only after we have started iterating we should # see non-empty `request_ids` property proxy.next() self.assertEqual([REQUEST_ID], proxy.request_ids) # Even after multiple iterations `request_ids` property # should only contain one request id proxy.next() proxy.next() self.assertEqual(1, len(proxy.request_ids)) def test_request_id_proxy(self): def test_data(val): resp = create_response_obj_with_req_id(REQUEST_ID) return val, resp # Object of any type except decorator can be passed to test_data proxy = utils.RequestIdProxy(test_data(11)) # Verify that proxy object has a property `request_ids` and it is # a list of one request id self.assertEqual([REQUEST_ID], proxy.request_ids) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1688740210.0821638 python-glanceclient-4.4.0/glanceclient/tests/unit/v1/0000775000175000017500000000000000000000000022610 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v1/__init__.py0000664000175000017500000000000000000000000024707 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v1/test_image_members.py0000664000175000017500000001031300000000000027013 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. import testtools from glanceclient.tests import utils import glanceclient.v1.image_members import glanceclient.v1.images fixtures = { '/v1/images/1/members': { 'GET': ( {}, {'members': [ {'member_id': '1', 'can_share': False}, ]}, ), 'PUT': ({}, None), }, '/v1/images/1/members/1': { 'GET': ( {}, {'member': { 'member_id': '1', 'can_share': False, }}, ), 'PUT': ({}, None), 'DELETE': ({}, None), }, '/v1/shared-images/1': { 'GET': ( {}, {'shared_images': [ {'image_id': '1', 'can_share': False}, ]}, ), }, } class ImageMemberManagerTest(testtools.TestCase): def setUp(self): super(ImageMemberManagerTest, self).setUp() self.api = utils.FakeAPI(fixtures) self.mgr = glanceclient.v1.image_members.ImageMemberManager(self.api) self.image = glanceclient.v1.images.Image(self.api, {'id': '1'}, True) def test_list_by_image(self): members = self.mgr.list(image=self.image) expect = [('GET', '/v1/images/1/members', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual(1, len(members)) self.assertEqual('1', members[0].member_id) self.assertEqual('1', members[0].image_id) self.assertEqual(False, members[0].can_share) def test_list_by_member(self): resource_class = glanceclient.v1.image_members.ImageMember member = resource_class(self.api, {'member_id': '1'}, True) self.mgr.list(member=member) expect = [('GET', '/v1/shared-images/1', {}, None)] self.assertEqual(expect, self.api.calls) def test_get(self): member = self.mgr.get(self.image, '1') expect = [('GET', '/v1/images/1/members/1', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', member.member_id) self.assertEqual('1', member.image_id) self.assertEqual(False, member.can_share) def test_delete(self): self.mgr.delete('1', '1') expect = [('DELETE', '/v1/images/1/members/1', {}, None)] self.assertEqual(expect, self.api.calls) def test_create(self): self.mgr.create(self.image, '1', can_share=True) expect_body = {'member': {'can_share': True}} expect = [('PUT', '/v1/images/1/members/1', {}, sorted(expect_body.items()))] self.assertEqual(expect, self.api.calls) def test_replace(self): body = [ {'member_id': '2', 'can_share': False}, {'member_id': '3'}, ] self.mgr.replace(self.image, body) expect = [('PUT', '/v1/images/1/members', {}, sorted({'memberships': body}.items()))] self.assertEqual(expect, self.api.calls) def test_replace_objects(self): body = [ glanceclient.v1.image_members.ImageMember( self.mgr, {'member_id': '2', 'can_share': False}, True), glanceclient.v1.image_members.ImageMember( self.mgr, {'member_id': '3', 'can_share': True}, True), ] self.mgr.replace(self.image, body) expect_body = { 'memberships': [ {'member_id': '2', 'can_share': False}, {'member_id': '3', 'can_share': True}, ], } expect = [('PUT', '/v1/images/1/members', {}, sorted(expect_body.items()))] self.assertEqual(expect, self.api.calls) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v1/test_images.py0000664000175000017500000007734600000000000025507 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. import errno import io import json import testtools from urllib import parse from glanceclient.tests import utils from glanceclient.v1 import client from glanceclient.v1 import images from glanceclient.v1 import shell DEFAULT_PAGE_SIZE = 20 fixtures = { '/v1/images': { 'POST': ( { 'location': '/v1/images/1', 'x-openstack-request-id': 'req-1234', }, json.dumps( {'image': { 'id': '1', 'name': 'image-1', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': '1024', 'min_ram': '512', 'min_disk': '10', 'properties': {'a': 'b', 'c': 'd'}, 'is_public': False, 'protected': False, 'deleted': False, }}, ), ), }, '/v1/images/detail?limit=20': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, f'/v1/images/detail?is_public=None&limit={DEFAULT_PAGE_SIZE}': { 'GET': ( {'x-openstack-request-id': 'req-1234'}, {'images': [ { 'id': 'a', 'owner': 'A', 'is_public': 'True', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'owner': 'B', 'is_public': 'False', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, { 'id': 'c', 'is_public': 'False', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?is_public=None&limit=5': { 'GET': ( {}, {'images': [ { 'id': 'a', 'owner': 'A', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'owner': 'B', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b2', 'owner': 'B', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, { 'id': 'c', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=5': { 'GET': ( {}, {'images': [ { 'id': 'a', 'owner': 'A', 'is_public': 'False', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'owner': 'A', 'is_public': 'False', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b2', 'owner': 'B', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, { 'id': 'c', 'is_public': 'True', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, f'/v1/images/detail?limit={DEFAULT_PAGE_SIZE}&marker=a': { 'GET': ( {}, {'images': [ { 'id': 'b', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'c', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=1': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-0', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=1&marker=a': { 'GET': ( {}, {'images': [ { 'id': 'b', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=2': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/detail?limit=2&marker=b': { 'GET': ( {}, {'images': [ { 'id': 'c', 'name': 'image-3', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, f'/v1/images/detail?limit={DEFAULT_PAGE_SIZE}&name=foo': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, f'/v1/images/detail?limit={DEFAULT_PAGE_SIZE}&property-ping=pong': { 'GET': ( {}, {'images': [ { 'id': '1', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, f'/v1/images/detail?limit={DEFAULT_PAGE_SIZE}&sort_dir=desc': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, f'/v1/images/detail?limit={DEFAULT_PAGE_SIZE}&sort_key=name': { 'GET': ( {}, {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]}, ), }, '/v1/images/1': { 'HEAD': ( { 'x-image-meta-id': '1', 'x-image-meta-name': 'image-1', 'x-image-meta-property-arch': 'x86_64', 'x-image-meta-is_public': 'false', 'x-image-meta-protected': 'false', 'x-image-meta-deleted': 'false', }, None), 'GET': ( {}, 'XXX', ), 'PUT': ( {}, json.dumps( {'image': { 'id': '1', 'name': 'image-2', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': '1024', 'min_ram': '512', 'min_disk': '10', 'properties': {'a': 'b', 'c': 'd'}, 'is_public': False, 'protected': False, }}, ), ), 'DELETE': ({}, None), }, '/v1/images/2': { 'HEAD': ( { 'x-image-meta-id': '2' }, None, ), 'GET': ( { 'x-image-meta-checksum': 'wrong' }, 'YYY', ), }, '/v1/images/3': { 'HEAD': ( { 'x-image-meta-id': '3', 'x-image-meta-name': u"ni\xf1o" }, None, ), 'GET': ( { 'x-image-meta-checksum': '0745064918b49693cca64d6b6a13d28a' }, 'ZZZ', ), }, '/v1/images/4': { 'HEAD': ( { 'x-image-meta-id': '4', 'x-image-meta-name': 'image-4', 'x-image-meta-property-arch': 'x86_64', 'x-image-meta-is_public': 'false', 'x-image-meta-protected': 'false', 'x-image-meta-deleted': 'false', 'x-openstack-request-id': 'req-1234', }, None), 'GET': ( { 'x-openstack-request-id': 'req-1234', }, 'XXX', ), 'PUT': ( { 'x-openstack-request-id': 'req-1234', }, json.dumps( {'image': { 'id': '4', 'name': 'image-4', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': '1024', 'min_ram': '512', 'min_disk': '10', 'properties': {'a': 'b', 'c': 'd'}, 'is_public': False, 'protected': False, }}, ), ), 'DELETE': ( { 'x-openstack-request-id': 'req-1234', }, None), }, '/v1/images/v2_created_img': { 'PUT': ( {}, json.dumps({ "image": { "status": "queued", "deleted": False, "container_format": "bare", "min_ram": 0, "updated_at": "2013-12-20T01:51:45", "owner": "foo", "min_disk": 0, "is_public": False, "deleted_at": None, "id": "v2_created_img", "size": None, "name": "bar", "checksum": None, "created_at": "2013-12-20T01:50:38", "disk_format": "qcow2", "properties": {}, "protected": False } }) ), }, } class ImageManagerTest(testtools.TestCase): def setUp(self): super(ImageManagerTest, self).setUp() self.api = utils.FakeAPI(fixtures) self.mgr = images.ImageManager(self.api) def test_paginated_list(self): images = list(self.mgr.list(page_size=2)) expect = [ ('GET', '/v1/images/detail?limit=2', {}, None), ('GET', '/v1/images/detail?limit=2&marker=b', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(3, len(images)) self.assertEqual('a', images[0].id) self.assertEqual('b', images[1].id) self.assertEqual('c', images[2].id) def test_list_with_limit_less_than_page_size(self): results = list(self.mgr.list(page_size=2, limit=1)) expect = [('GET', '/v1/images/detail?limit=2', {}, None)] self.assertEqual(1, len(results)) self.assertEqual(expect, self.api.calls) def test_list_with_limit_greater_than_page_size(self): images = list(self.mgr.list(page_size=1, limit=2)) expect = [ ('GET', '/v1/images/detail?limit=1', {}, None), ('GET', '/v1/images/detail?limit=1&marker=a', {}, None), ] self.assertEqual(2, len(images)) self.assertEqual('a', images[0].id) self.assertEqual('b', images[1].id) self.assertEqual(expect, self.api.calls) def test_list_with_marker(self): list(self.mgr.list(marker='a')) url = f'/v1/images/detail?limit={DEFAULT_PAGE_SIZE}&marker=a' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_list_with_filter(self): list(self.mgr.list(filters={'name': "foo"})) url = f'/v1/images/detail?limit={DEFAULT_PAGE_SIZE}&name=foo' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_list_with_property_filters(self): list(self.mgr.list(filters={'properties': {'ping': 'pong'}})) url = f'/v1/images/detail?limit={DEFAULT_PAGE_SIZE}&property-ping=pong' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_list_with_sort_dir(self): list(self.mgr.list(sort_dir='desc')) url = f'/v1/images/detail?limit={DEFAULT_PAGE_SIZE}&sort_dir=desc' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_list_with_sort_key(self): list(self.mgr.list(sort_key='name')) url = f'/v1/images/detail?limit={DEFAULT_PAGE_SIZE}&sort_key=name' expect = [('GET', url, {}, None)] self.assertEqual(expect, self.api.calls) def test_get(self): image = self.mgr.get('1') expect = [('HEAD', '/v1/images/1', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', image.id) self.assertEqual('image-1', image.name) self.assertEqual(False, image.is_public) self.assertEqual(False, image.protected) self.assertEqual(False, image.deleted) self.assertEqual({'arch': 'x86_64'}, image.properties) def test_get_int(self): image = self.mgr.get(1) expect = [('HEAD', '/v1/images/1', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', image.id) self.assertEqual('image-1', image.name) self.assertEqual(False, image.is_public) self.assertEqual(False, image.protected) self.assertEqual(False, image.deleted) self.assertEqual({'arch': 'x86_64'}, image.properties) def test_get_encoding(self): image = self.mgr.get('3') self.assertEqual(u"ni\xf1o", image.name) def test_get_req_id(self): params = {'return_req_id': []} self.mgr.get('4', **params) expect_req_id = ['req-1234'] self.assertEqual(expect_req_id, params['return_req_id']) def test_data(self): data = ''.join([b for b in self.mgr.data('1', do_checksum=False)]) expect = [('GET', '/v1/images/1', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('XXX', data) expect += [('GET', '/v1/images/1', {}, None)] data = ''.join([b for b in self.mgr.data('1')]) self.assertEqual(expect, self.api.calls) self.assertEqual('XXX', data) def test_data_with_wrong_checksum(self): data = ''.join([b for b in self.mgr.data('2', do_checksum=False)]) expect = [('GET', '/v1/images/2', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('YYY', data) expect += [('GET', '/v1/images/2', {}, None)] data = self.mgr.data('2') self.assertEqual(expect, self.api.calls) try: data = ''.join([b for b in data]) self.fail('data did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong' self.assertIn(msg, str(e)) def test_data_req_id(self): params = { 'do_checksum': False, 'return_req_id': [], } ''.join([b for b in self.mgr.data('4', **params)]) expect_req_id = ['req-1234'] self.assertEqual(expect_req_id, params['return_req_id']) def test_data_with_checksum(self): data = ''.join([b for b in self.mgr.data('3', do_checksum=False)]) expect = [('GET', '/v1/images/3', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('ZZZ', data) expect += [('GET', '/v1/images/3', {}, None)] data = ''.join([b for b in self.mgr.data('3')]) self.assertEqual(expect, self.api.calls) self.assertEqual('ZZZ', data) def test_delete(self): self.mgr.delete('1') expect = [('DELETE', '/v1/images/1', {}, None)] self.assertEqual(expect, self.api.calls) def test_delete_req_id(self): params = { 'return_req_id': [] } self.mgr.delete('4', **params) expect = [('DELETE', '/v1/images/4', {}, None)] self.assertEqual(self.api.calls, expect) expect_req_id = ['req-1234'] self.assertEqual(expect_req_id, params['return_req_id']) def test_create_without_data(self): params = { 'id': '1', 'name': 'image-1', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': 1024, 'min_ram': 512, 'min_disk': 10, 'copy_from': 'http://example.com', 'properties': {'a': 'b', 'c': 'd'}, } image = self.mgr.create(**params) expect_headers = { 'x-image-meta-id': '1', 'x-image-meta-name': 'image-1', 'x-image-meta-container_format': 'ovf', 'x-image-meta-disk_format': 'vhd', 'x-image-meta-owner': 'asdf', 'x-image-meta-size': '1024', 'x-image-meta-min_ram': '512', 'x-image-meta-min_disk': '10', 'x-glance-api-copy-from': 'http://example.com', 'x-image-meta-property-a': 'b', 'x-image-meta-property-c': 'd', } expect = [('POST', '/v1/images', expect_headers, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', image.id) self.assertEqual('image-1', image.name) self.assertEqual('ovf', image.container_format) self.assertEqual('vhd', image.disk_format) self.assertEqual('asdf', image.owner) self.assertEqual(1024, image.size) self.assertEqual(512, image.min_ram) self.assertEqual(10, image.min_disk) self.assertEqual(False, image.is_public) self.assertEqual(False, image.protected) self.assertEqual(False, image.deleted) self.assertEqual({'a': 'b', 'c': 'd'}, image.properties) def test_create_with_data(self): image_data = io.StringIO('XXX') self.mgr.create(data=image_data) expect_headers = {'x-image-meta-size': '3'} expect = [('POST', '/v1/images', expect_headers, image_data)] self.assertEqual(expect, self.api.calls) def test_create_req_id(self): params = { 'id': '4', 'name': 'image-4', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': 1024, 'min_ram': 512, 'min_disk': 10, 'copy_from': 'http://example.com', 'properties': {'a': 'b', 'c': 'd'}, 'return_req_id': [], } image = self.mgr.create(**params) expect_headers = { 'x-image-meta-id': '4', 'x-image-meta-name': 'image-4', 'x-image-meta-container_format': 'ovf', 'x-image-meta-disk_format': 'vhd', 'x-image-meta-owner': 'asdf', 'x-image-meta-size': '1024', 'x-image-meta-min_ram': '512', 'x-image-meta-min_disk': '10', 'x-glance-api-copy-from': 'http://example.com', 'x-image-meta-property-a': 'b', 'x-image-meta-property-c': 'd', } expect = [('POST', '/v1/images', expect_headers, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', image.id) expect_req_id = ['req-1234'] self.assertEqual(expect_req_id, params['return_req_id']) def test_update(self): fields = { 'name': 'image-2', 'container_format': 'ovf', 'disk_format': 'vhd', 'owner': 'asdf', 'size': 1024, 'min_ram': 512, 'min_disk': 10, 'copy_from': 'http://example.com', 'properties': {'a': 'b', 'c': 'd'}, 'deleted': False, } image = self.mgr.update('1', **fields) expect_hdrs = { 'x-image-meta-name': 'image-2', 'x-image-meta-container_format': 'ovf', 'x-image-meta-disk_format': 'vhd', 'x-image-meta-owner': 'asdf', 'x-image-meta-size': '1024', 'x-image-meta-min_ram': '512', 'x-image-meta-min_disk': '10', 'x-glance-api-copy-from': 'http://example.com', 'x-image-meta-property-a': 'b', 'x-image-meta-property-c': 'd', 'x-image-meta-deleted': 'False', 'x-glance-registry-purge-props': 'false', } expect = [('PUT', '/v1/images/1', expect_hdrs, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('1', image.id) self.assertEqual('image-2', image.name) self.assertEqual(1024, image.size) self.assertEqual(512, image.min_ram) self.assertEqual(10, image.min_disk) def test_update_with_data(self): image_data = io.StringIO('XXX') self.mgr.update('1', data=image_data) expect_headers = {'x-image-meta-size': '3', 'x-glance-registry-purge-props': 'false'} expect = [('PUT', '/v1/images/1', expect_headers, image_data)] self.assertEqual(expect, self.api.calls) def test_update_with_purge_props(self): self.mgr.update('1', purge_props=True) expect_headers = {'x-glance-registry-purge-props': 'true'} expect = [('PUT', '/v1/images/1', expect_headers, None)] self.assertEqual(expect, self.api.calls) def test_update_with_purge_props_false(self): self.mgr.update('1', purge_props=False) expect_headers = {'x-glance-registry-purge-props': 'false'} expect = [('PUT', '/v1/images/1', expect_headers, None)] self.assertEqual(expect, self.api.calls) def test_update_req_id(self): fields = { 'purge_props': True, 'return_req_id': [], } self.mgr.update('4', **fields) expect_headers = {'x-glance-registry-purge-props': 'true'} expect = [('PUT', '/v1/images/4', expect_headers, None)] self.assertEqual(expect, self.api.calls) expect_req_id = ['req-1234'] self.assertEqual(expect_req_id, fields['return_req_id']) def test_image_meta_from_headers_encoding(self): value = u"ni\xf1o" fields = {"x-image-meta-name": value} headers = self.mgr._image_meta_from_headers(fields) self.assertEqual(value, headers["name"]) def test_image_list_with_owner(self): images = self.mgr.list(owner='A', page_size=DEFAULT_PAGE_SIZE) image_list = list(images) self.assertEqual('A', image_list[0].owner) self.assertEqual('a', image_list[0].id) self.assertEqual(1, len(image_list)) def test_image_list_with_owner_req_id(self): fields = { 'owner': 'A', 'return_req_id': [], } images = self.mgr.list(**fields) next(images) self.assertEqual(['req-1234'], fields['return_req_id']) def test_image_list_with_notfound_owner(self): images = self.mgr.list(owner='X', page_size=DEFAULT_PAGE_SIZE) self.assertEqual(0, len(list(images))) def test_image_list_with_empty_string_owner(self): images = self.mgr.list(owner='', page_size=DEFAULT_PAGE_SIZE) image_list = list(images) self.assertRaises(AttributeError, lambda: image_list[0].owner) self.assertEqual('c', image_list[0].id) self.assertEqual(1, len(image_list)) def test_image_list_with_unspecified_owner(self): images = self.mgr.list(owner=None, page_size=5) image_list = list(images) self.assertEqual('A', image_list[0].owner) self.assertEqual('a', image_list[0].id) self.assertEqual('A', image_list[1].owner) self.assertEqual('b', image_list[1].id) self.assertEqual('B', image_list[2].owner) self.assertEqual('b2', image_list[2].id) self.assertRaises(AttributeError, lambda: image_list[3].owner) self.assertEqual('c', image_list[3].id) self.assertEqual(4, len(image_list)) def test_image_list_with_owner_and_limit(self): images = self.mgr.list(owner='B', page_size=5, limit=1) image_list = list(images) self.assertEqual('B', image_list[0].owner) self.assertEqual('b', image_list[0].id) self.assertEqual(1, len(image_list)) def test_image_list_all_tenants(self): images = self.mgr.list(is_public=None, page_size=5) image_list = list(images) self.assertEqual('A', image_list[0].owner) self.assertEqual('a', image_list[0].id) self.assertEqual('B', image_list[1].owner) self.assertEqual('b', image_list[1].id) self.assertEqual('B', image_list[2].owner) self.assertEqual('b2', image_list[2].id) self.assertRaises(AttributeError, lambda: image_list[3].owner) self.assertEqual('c', image_list[3].id) self.assertEqual(4, len(image_list)) def test_update_v2_created_image_using_v1(self): fields_to_update = { 'name': 'bar', 'container_format': 'bare', 'disk_format': 'qcow2', } image = self.mgr.update('v2_created_img', **fields_to_update) expect_hdrs = { 'x-image-meta-name': 'bar', 'x-image-meta-container_format': 'bare', 'x-image-meta-disk_format': 'qcow2', 'x-glance-registry-purge-props': 'false', } expect = [('PUT', '/v1/images/v2_created_img', expect_hdrs, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('v2_created_img', image.id) self.assertEqual('bar', image.name) self.assertEqual(0, image.size) self.assertEqual('bare', image.container_format) self.assertEqual('qcow2', image.disk_format) class ImageTest(testtools.TestCase): def setUp(self): super(ImageTest, self).setUp() self.api = utils.FakeAPI(fixtures) self.mgr = images.ImageManager(self.api) def test_delete(self): image = self.mgr.get('1') image.delete() expect = [ ('HEAD', '/v1/images/1', {}, None), ('HEAD', '/v1/images/1', {}, None), ('DELETE', '/v1/images/1', {}, None), ] self.assertEqual(expect, self.api.calls) def test_update(self): image = self.mgr.get('1') image.update(name='image-5') expect = [ ('HEAD', '/v1/images/1', {}, None), ('HEAD', '/v1/images/1', {}, None), ('PUT', '/v1/images/1', {'x-image-meta-name': 'image-5', 'x-glance-registry-purge-props': 'false'}, None), ] self.assertEqual(expect, self.api.calls) def test_data(self): image = self.mgr.get('1') data = ''.join([b for b in image.data()]) expect = [ ('HEAD', '/v1/images/1', {}, None), ('HEAD', '/v1/images/1', {}, None), ('GET', '/v1/images/1', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual('XXX', data) data = ''.join([b for b in image.data(do_checksum=False)]) expect += [('GET', '/v1/images/1', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('XXX', data) def test_data_with_wrong_checksum(self): image = self.mgr.get('2') data = ''.join([b for b in image.data(do_checksum=False)]) expect = [ ('HEAD', '/v1/images/2', {}, None), ('HEAD', '/v1/images/2', {}, None), ('GET', '/v1/images/2', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual('YYY', data) data = image.data() expect += [('GET', '/v1/images/2', {}, None)] self.assertEqual(expect, self.api.calls) try: data = ''.join([b for b in image.data()]) self.fail('data did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'was fd7c5c4fdaa97163ee4ba8842baa537a expected wrong' self.assertIn(msg, str(e)) def test_data_with_checksum(self): image = self.mgr.get('3') data = ''.join([b for b in image.data(do_checksum=False)]) expect = [ ('HEAD', '/v1/images/3', {}, None), ('HEAD', '/v1/images/3', {}, None), ('GET', '/v1/images/3', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual('ZZZ', data) data = ''.join([b for b in image.data()]) expect += [('GET', '/v1/images/3', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual('ZZZ', data) class ParameterFakeAPI(utils.FakeAPI): image_list = {'images': [ { 'id': 'a', 'name': 'image-1', 'properties': {'arch': 'x86_64'}, }, { 'id': 'b', 'name': 'image-2', 'properties': {'arch': 'x86_64'}, }, ]} def get(self, url, **kwargs): self.url = url return utils.FakeResponse({}), ParameterFakeAPI.image_list class FakeArg(object): def __init__(self, arg_dict): self.arg_dict = arg_dict self.fields = arg_dict.keys() def __getattr__(self, name): if name in self.arg_dict: return self.arg_dict[name] else: return None class UrlParameterTest(testtools.TestCase): def setUp(self): super(UrlParameterTest, self).setUp() self.api = ParameterFakeAPI({}) self.gc = client.Client("http://fakeaddress.com") self.gc.images = images.ImageManager(self.api) def test_is_public_list(self): shell.do_image_list(self.gc, FakeArg({"is_public": "True"})) parts = parse.urlparse(self.api.url) qs_dict = parse.parse_qs(parts.query) self.assertIn('is_public', qs_dict) self.assertTrue(qs_dict['is_public'][0].lower() == "true") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v1/test_shell.py0000664000175000017500000005373300000000000025343 0ustar00zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # Copyright (C) 2013 Yahoo! Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse import io import json import os from unittest import mock import subprocess import tempfile import testtools from glanceclient import exc from glanceclient import shell import glanceclient.v1.client as client import glanceclient.v1.images import glanceclient.v1.shell as v1shell from glanceclient.tests import utils fixtures = { '/v1/images/96d2c7e1-de4e-4612-8aa2-ba26610c804e': { 'PUT': ( { 'Location': 'http://fakeaddress.com:9292/v1/images/' '96d2c7e1-de4e-4612-8aa2-ba26610c804e', 'Etag': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'X-Openstack-Request-Id': 'req-b645039d-e1c7-43e5-b27b-2d18a173c42b', 'Date': 'Mon, 29 Apr 2013 10:24:32 GMT' }, json.dumps({ 'image': { 'status': 'active', 'name': 'testimagerename', 'deleted': False, 'container_format': 'ami', 'created_at': '2013-04-25T15:47:43', 'disk_format': 'ami', 'updated_at': '2013-04-29T10:24:32', 'id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e', 'min_disk': 0, 'protected': False, 'min_ram': 0, 'checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'owner': '1310db0cce8f40b0987a5acbe139765a', 'is_public': True, 'deleted_at': None, 'properties': { 'kernel_id': '1b108400-65d8-4762-9ea4-1bf6c7be7568', 'ramdisk_id': 'b759bee9-0669-4394-a05c-fa2529b1c114' }, 'size': 25165824 } }) ), 'HEAD': ( { 'x-image-meta-id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e', 'x-image-meta-status': 'active' }, None ), 'GET': ( { 'x-image-meta-status': 'active', 'x-image-meta-owner': '1310db0cce8f40b0987a5acbe139765a', 'x-image-meta-name': 'cirros-0.3.1-x86_64-uec', 'x-image-meta-container_format': 'ami', 'x-image-meta-created_at': '2013-04-25T15:47:43', 'etag': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'location': 'http://fakeaddress.com:9292/v1/images/' '96d2c7e1-de4e-4612-8aa2-ba26610c804e', 'x-image-meta-min_ram': '0', 'x-image-meta-updated_at': '2013-04-25T15:47:43', 'x-image-meta-id': '96d2c7e1-de4e-4612-8aa2-ba26610c804e', 'x-image-meta-property-ramdisk_id': 'b759bee9-0669-4394-a05c-fa2529b1c114', 'date': 'Mon, 29 Apr 2013 09:25:17 GMT', 'x-image-meta-property-kernel_id': '1b108400-65d8-4762-9ea4-1bf6c7be7568', 'x-openstack-request-id': 'req-842735bf-77e8-44a7-bfd1-7d95c52cec7f', 'x-image-meta-deleted': 'False', 'x-image-meta-checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'x-image-meta-protected': 'False', 'x-image-meta-min_disk': '0', 'x-image-meta-size': '25165824', 'x-image-meta-is_public': 'True', 'content-type': 'text/html; charset=UTF-8', 'x-image-meta-disk_format': 'ami', }, None ) }, '/v1/images/44d2c7e1-de4e-4612-8aa2-ba26610c444f': { 'PUT': ( { 'Location': 'http://fakeaddress.com:9292/v1/images/' '44d2c7e1-de4e-4612-8aa2-ba26610c444f', 'Etag': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'X-Openstack-Request-Id': 'req-b645039d-e1c7-43e5-b27b-2d18a173c42b', 'Date': 'Mon, 29 Apr 2013 10:24:32 GMT' }, json.dumps({ 'image': { 'status': 'queued', 'name': 'testimagerename', 'deleted': False, 'container_format': 'ami', 'created_at': '2013-04-25T15:47:43', 'disk_format': 'ami', 'updated_at': '2013-04-29T10:24:32', 'id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f', 'min_disk': 0, 'protected': False, 'min_ram': 0, 'checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'owner': '1310db0cce8f40b0987a5acbe139765a', 'is_public': True, 'deleted_at': None, 'properties': { 'kernel_id': '1b108400-65d8-4762-9ea4-1bf6c7be7568', 'ramdisk_id': 'b759bee9-0669-4394-a05c-fa2529b1c114' }, 'size': 25165824 } }) ), 'HEAD': ( { 'x-image-meta-id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f', 'x-image-meta-status': 'queued' }, None ), 'GET': ( { 'x-image-meta-status': 'queued', 'x-image-meta-owner': '1310db0cce8f40b0987a5acbe139765a', 'x-image-meta-name': 'cirros-0.3.1-x86_64-uec', 'x-image-meta-container_format': 'ami', 'x-image-meta-created_at': '2013-04-25T15:47:43', 'etag': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'location': 'http://fakeaddress.com:9292/v1/images/' '44d2c7e1-de4e-4612-8aa2-ba26610c444f', 'x-image-meta-min_ram': '0', 'x-image-meta-updated_at': '2013-04-25T15:47:43', 'x-image-meta-id': '44d2c7e1-de4e-4612-8aa2-ba26610c444f', 'x-image-meta-property-ramdisk_id': 'b759bee9-0669-4394-a05c-fa2529b1c114', 'date': 'Mon, 29 Apr 2013 09:25:17 GMT', 'x-image-meta-property-kernel_id': '1b108400-65d8-4762-9ea4-1bf6c7be7568', 'x-openstack-request-id': 'req-842735bf-77e8-44a7-bfd1-7d95c52cec7f', 'x-image-meta-deleted': 'False', 'x-image-meta-checksum': 'f8a2eeee2dc65b3d9b6e63678955bd83', 'x-image-meta-protected': 'False', 'x-image-meta-min_disk': '0', 'x-image-meta-size': '25165824', 'x-image-meta-is_public': 'True', 'content-type': 'text/html; charset=UTF-8', 'x-image-meta-disk_format': 'ami', }, None ) }, '/v1/images/detail?limit=20&name=70aa106f-3750-4d7c-a5ce-0a535ac08d0a': { 'GET': ( {}, {'images': [ { 'id': '70aa106f-3750-4d7c-a5ce-0a535ac08d0a', 'name': 'imagedeleted', 'deleted': True, 'status': 'deleted', }, ]}, ), }, '/v1/images/70aa106f-3750-4d7c-a5ce-0a535ac08d0a': { 'HEAD': ( { 'x-image-meta-id': '70aa106f-3750-4d7c-a5ce-0a535ac08d0a', 'x-image-meta-status': 'deleted' }, None ) } } class ShellInvalidEndpointandParameterTest(utils.TestCase): # Patch os.environ to avoid required auth info. def setUp(self): """Run before each test.""" super(ShellInvalidEndpointandParameterTest, self).setUp() self.old_environment = os.environ.copy() os.environ = { 'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', 'OS_TOKEN_ID': 'test', 'OS_AUTH_URL': 'http://127.0.0.1:5000/v2.0/', 'OS_AUTH_TOKEN': 'pass', 'OS_IMAGE_API_VERSION': '1', 'OS_REGION_NAME': 'test', 'OS_IMAGE_URL': 'http://is.invalid'} self.shell = shell.OpenStackImagesShell() self.patched = mock.patch('glanceclient.common.utils.get_data_file', autospec=True, return_value=None) self.mock_get_data_file = self.patched.start() self.gc = self._mock_glance_client() def _make_args(self, args): # NOTE(venkatesh): this conversion from a dict to an object # is required because the test_shell.do_xxx(gc, args) methods # expects the args to be attributes of an object. If passed as # dict directly, it throws an AttributeError. class Args(object): def __init__(self, entries): self.__dict__.update(entries) return Args(args) def _mock_glance_client(self): my_mocked_gc = mock.Mock() my_mocked_gc.get.return_value = {} return my_mocked_gc def tearDown(self): super(ShellInvalidEndpointandParameterTest, self).tearDown() os.environ = self.old_environment self.patched.stop() def run_command(self, cmd): self.shell.main(cmd.split()) def assert_called(self, method, url, body=None, **kwargs): return self.shell.cs.assert_called(method, url, body, **kwargs) def assert_called_anytime(self, method, url, body=None): return self.shell.cs.assert_called_anytime(method, url, body) def test_image_list_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'image-list') def test_image_create_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'image-create') def test_image_delete_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'image-delete ') def test_image_download_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'image-download ') def test_members_list_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'member-list --image-id fake') def test_image_show_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'image-show --human-readable ') def test_member_create_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'member-create --can-share ') def test_member_delete_invalid_endpoint(self): self.assertRaises( exc.CommunicationError, self.run_command, 'member-delete ') @mock.patch('sys.stderr') def test_image_create_invalid_size_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-create --size 10gb') @mock.patch('sys.stderr') def test_image_create_invalid_ram_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-create --min-ram 10gb') @mock.patch('sys.stderr') def test_image_create_invalid_min_disk_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-create --min-disk 10gb') @mock.patch('sys.stderr') def test_image_create_missing_disk_format(self, __): # We test for all possible sources for origin in ('--file', '--location', '--copy-from'): e = self.assertRaises(exc.CommandError, self.run_command, '--os-image-api-version 1 image-create ' + origin + ' fake_src --container-format bare') self.assertEqual('error: Must provide --disk-format when using ' + origin + '.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_container_format(self, __): # We test for all possible sources for origin in ('--file', '--location', '--copy-from'): e = self.assertRaises(exc.CommandError, self.run_command, '--os-image-api-version 1 image-create ' + origin + ' fake_src --disk-format qcow2') self.assertEqual('error: Must provide --container-format when ' 'using ' + origin + '.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_container_format_stdin_data(self, __): # Fake that get_data_file method returns data self.mock_get_data_file.return_value = io.StringIO() e = self.assertRaises(exc.CommandError, self.run_command, '--os-image-api-version 1 image-create' ' --disk-format qcow2') self.assertEqual('error: Must provide --container-format when ' 'using stdin.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_disk_format_stdin_data(self, __): # Fake that get_data_file method returns data self.mock_get_data_file.return_value = io.StringIO() e = self.assertRaises(exc.CommandError, self.run_command, '--os-image-api-version 1 image-create' ' --container-format bare') self.assertEqual('error: Must provide --disk-format when using stdin.', e.message) @mock.patch('sys.stderr') def test_image_update_invalid_size_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-update --size 10gb') @mock.patch('sys.stderr') def test_image_update_invalid_min_disk_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-update --min-disk 10gb') @mock.patch('sys.stderr') def test_image_update_invalid_ram_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-update --min-ram 10gb') @mock.patch('sys.stderr') def test_image_list_invalid_min_size_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-list --size-min 10gb') @mock.patch('sys.stderr') def test_image_list_invalid_max_size_parameter(self, __): self.assertRaises( SystemExit, self.run_command, 'image-list --size-max 10gb') def test_do_image_list_with_changes_since(self): input = { 'name': None, 'limit': None, 'status': None, 'container_format': 'bare', 'size_min': None, 'size_max': None, 'is_public': True, 'disk_format': 'raw', 'page_size': 20, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort_key': None, 'sort_dir': None, 'all_tenants': False, 'human_readable': True, 'changes_since': '2011-1-1' } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} v1shell.do_image_list(self.gc, args) exp_img_filters = {'container_format': 'bare', 'changes-since': '2011-1-1', 'disk_format': 'raw', 'is_public': True} mocked_list.assert_called_once_with(sort_dir=None, sort_key=None, owner='test', page_size=20, filters=exp_img_filters) class ShellStdinHandlingTests(testtools.TestCase): def _fake_update_func(self, *args, **kwargs): """Replace glanceclient.images.update with a fake. To determine the parameters that would be supplied with the update request. """ # Store passed in args self.collected_args = (args, kwargs) # Return the first arg, which is an image, # as do_image_update expects this. return args[0] def setUp(self): super(ShellStdinHandlingTests, self).setUp() self.api = utils.FakeAPI(fixtures) self.gc = client.Client("http://fakeaddress.com") self.gc.images = glanceclient.v1.images.ImageManager(self.api) # Store real stdin, so it can be restored in tearDown. self.real_sys_stdin_fd = os.dup(0) # Replace stdin with a FD that points to /dev/null. dev_null = open('/dev/null') self.dev_null_fd = dev_null.fileno() os.dup2(dev_null.fileno(), 0) # Replace the image update function with a fake, # so that we can tell if the data field was set correctly. self.real_update_func = self.gc.images.update self.collected_args = [] self.gc.images.update = self._fake_update_func def tearDown(self): """Restore stdin and gc.images.update to their pretest states.""" super(ShellStdinHandlingTests, self).tearDown() def try_close(fd): try: os.close(fd) except OSError: # Already closed pass # Restore stdin os.dup2(self.real_sys_stdin_fd, 0) # Close duplicate stdin handle try_close(self.real_sys_stdin_fd) # Close /dev/null handle try_close(self.dev_null_fd) # Restore the real image update function self.gc.images.update = self.real_update_func def _do_update(self, image='96d2c7e1-de4e-4612-8aa2-ba26610c804e'): """call v1/shell's do_image_update function.""" v1shell.do_image_update( self.gc, argparse.Namespace( image=image, name='testimagerename', property={}, purge_props=False, human_readable=False, file=None, progress=False ) ) def test_image_delete_deleted(self): self.assertRaises( exc.CommandError, v1shell.do_image_delete, self.gc, argparse.Namespace( images=['70aa106f-3750-4d7c-a5ce-0a535ac08d0a'] ) ) def test_image_update_closed_stdin(self): """Test image update with a closed stdin. Supply glanceclient with a closed stdin, and perform an image update to an active image. Glanceclient should not attempt to read stdin. """ # NOTE(hughsaunders) Close stdin, which is repointed to /dev/null by # setUp() os.close(0) self._do_update() self.assertTrue( 'data' not in self.collected_args[1] or self.collected_args[1]['data'] is None ) def test_image_update_opened_stdin(self): """Test image update with an opened stdin. Supply glanceclient with a stdin, and perform an image update to an active image. Glanceclient should not allow it. """ self.assertRaises( SystemExit, v1shell.do_image_update, self.gc, argparse.Namespace( image='96d2c7e1-de4e-4612-8aa2-ba26610c804e', property={}, ) ) def test_image_update_data_is_read_from_file(self): """Ensure that data is read from a file.""" try: # NOTE(hughsaunders) Create a tmpfile, write some data to it and # set it as stdin f = open(tempfile.mktemp(), 'w+') f.write('Some Data') f.flush() f.seek(0) os.dup2(f.fileno(), 0) self._do_update('44d2c7e1-de4e-4612-8aa2-ba26610c444f') self.assertIn('data', self.collected_args[1]) self.assertIsInstance(self.collected_args[1]['data'], io.IOBase) self.assertEqual(b'Some Data', self.collected_args[1]['data'].read()) finally: try: f.close() os.remove(f.name) except Exception: pass def test_image_update_data_is_read_from_pipe(self): """Ensure that data is read from a pipe.""" try: # NOTE(hughsaunders): Setup a pipe, duplicate it to stdin # ensure it is read. process = subprocess.Popen(['/bin/echo', 'Some Data'], stdout=subprocess.PIPE) os.dup2(process.stdout.fileno(), 0) self._do_update('44d2c7e1-de4e-4612-8aa2-ba26610c444f') self.assertIn('data', self.collected_args[1]) self.assertIsInstance(self.collected_args[1]['data'], io.IOBase) self.assertEqual(b'Some Data\n', self.collected_args[1]['data'].read()) finally: try: process.stdout.close() except OSError: pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v1/test_versions.py0000664000175000017500000000476500000000000026105 0ustar00zuulzuul00000000000000# Copyright 2015 OpenStack Foundation # Copyright 2015 Huawei 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. import testtools from glanceclient.tests import utils import glanceclient.v1.versions fixtures = { '/versions': { 'GET': ( {}, {"versions": [ { "status": "EXPERIMENTAL", "id": "v3.0", "links": [ { "href": "http://10.229.45.145:9292/v3/", "rel": "self" } ] }, { "status": "CURRENT", "id": "v2.3", "links": [ { "href": "http://10.229.45.145:9292/v2/", "rel": "self" } ] }, { "status": "SUPPORTED", "id": "v1.0", "links": [ { "href": "http://10.229.45.145:9292/v1/", "rel": "self" } ] } ]} ) } } class TestVersions(testtools.TestCase): def setUp(self): super(TestVersions, self).setUp() self.api = utils.FakeAPI(fixtures) self.mgr = glanceclient.v1.versions.VersionManager(self.api) def test_version_list(self): versions = self.mgr.list() expect = [('GET', '/versions', {}, None)] self.assertEqual(expect, self.api.calls) self.assertEqual(3, len(versions)) self.assertEqual('v3.0', versions[0]['id']) self.assertEqual('EXPERIMENTAL', versions[0]['status']) self.assertEqual([{"href": "http://10.229.45.145:9292/v3/", "rel": "self"}], versions[0]['links']) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1688740210.0821638 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/0000775000175000017500000000000000000000000022611 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/__init__.py0000664000175000017500000000000000000000000024710 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/base.py0000664000175000017500000001067100000000000024102 0ustar00zuulzuul00000000000000# Copyright 2016 NTT DATA # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools class BaseController(testtools.TestCase): def __init__(self, api, schema_api, controller_class): self.controller = controller_class(api, schema_api) def _assertRequestId(self, obj): self.assertIsNotNone(getattr(obj, 'request_ids', None)) self.assertEqual(['req-1234'], obj.request_ids) def list(self, *args, **kwargs): gen_obj = self.controller.list(*args, **kwargs) # For generator cases the request_ids property will be an empty list # until the underlying generator is invoked at-least once. resources = list(gen_obj) if len(resources) > 0: self._assertRequestId(gen_obj) else: # If list is empty that means geneator object has raised # StopIteration for first iteration and will not contain the # request_id in it. self.assertEqual([], gen_obj.request_ids) return resources def get_associated_image_tasks(self, *args, **kwargs): resource = self.controller.get_associated_image_tasks( *args, **kwargs) self._assertRequestId(resource) return resource def get(self, *args, **kwargs): resource = self.controller.get(*args, **kwargs) self._assertRequestId(resource) return resource def create(self, *args, **kwargs): resource = self.controller.create(*args, **kwargs) self._assertRequestId(resource) return resource def create_multiple(self, *args, **kwargs): tags = self.controller.create_multiple(*args, **kwargs) actual = [tag.name for tag in tags] self._assertRequestId(tags) return actual def update(self, *args, **properties): resource = self.controller.update(*args, **properties) self._assertRequestId(resource) return resource def delete(self, *args): resp = self.controller.delete(*args) self._assertRequestId(resp) def delete_all(self, *args): resp = self.controller.delete_all(*args) self._assertRequestId(resp) def deactivate(self, *args): resp = self.controller.deactivate(*args) self._assertRequestId(resp) def reactivate(self, *args): resp = self.controller.reactivate(*args) self._assertRequestId(resp) def upload(self, *args, **kwargs): resp = self.controller.upload(*args, **kwargs) self._assertRequestId(resp) def data(self, *args, **kwargs): body = self.controller.data(*args, **kwargs) self._assertRequestId(body) return body def delete_locations(self, *args): resp = self.controller.delete_locations(*args) self._assertRequestId(resp) def add_location(self, *args, **kwargs): resp = self.controller.add_location(*args, **kwargs) self._assertRequestId(resp) def update_location(self, *args, **kwargs): resp = self.controller.update_location(*args, **kwargs) self._assertRequestId(resp) def associate(self, *args, **kwargs): resource_types = self.controller.associate(*args, **kwargs) self._assertRequestId(resource_types) return resource_types def deassociate(self, *args): resp = self.controller.deassociate(*args) self._assertRequestId(resp) def image_import(self, *args, **kwargs): resp = self.controller.image_import(*args, **kwargs) self._assertRequestId(resp) class BaseResourceTypeController(BaseController): def __init__(self, api, schema_api, controller_class): super(BaseResourceTypeController, self).__init__(api, schema_api, controller_class) def get(self, *args, **kwargs): resource_types = self.controller.get(*args) names = [rt.name for rt in resource_types] self._assertRequestId(resource_types) return names ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/fixtures.py0000664000175000017500000002767200000000000025052 0ustar00zuulzuul00000000000000# Copyright (c) 2015 OpenStack Foundation # 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. import hashlib UUID = "3fc2ba62-9a02-433e-b565-d493ffc69034" image_list_fixture = { "images": [ { "checksum": "9cb02fe7fcac26f8a25d6db3109063ae", "container_format": "bare", "created_at": "2015-07-23T16:58:50.000000", "deleted": "false", "deleted_at": "null", "disk_format": "raw", "id": UUID, "is_public": "false", "min_disk": 0, "min_ram": 0, "name": "test", "owner": "3447cea05d6947658d73791ed9e0ed9f", "properties": { "kernel_id": 1234, "ramdisk_id": 5678 }, "protected": "false", "size": 145, "status": "active", "updated_at": "2015-07-23T16:58:51.000000", "virtual_size": "null" } ] } image_show_fixture = { "checksum": "9cb02fe7fcac26f8a25d6db3109063ae", "container_format": "bare", "created_at": "2015-07-24T12:18:13Z", "disk_format": "raw", "file": "/v2/images/%s/file" % UUID, "id": UUID, "kernel_id": "1234", "min_disk": 0, "min_ram": 0, "name": "img1", "owner": "411423405e10431fb9c47ac5b2446557", "protected": "false", "ramdisk_id": "5678", "schema": "/v2/schemas/image", "self": "/v2/images/%s" % UUID, "size": 145, "status": "active", "tags": [], "updated_at": "2015-07-24T12:18:13Z", "virtual_size": "null", "visibility": "shared", "os_hash_algo": "sha384", "os_hash_value": hashlib.sha384(b'DATA').hexdigest() } image_create_fixture = { "checksum": "9cb02fe7fcac26f8a25d6db3109063ae", "container_format": "bare", "created_at": "2015-07-24T12:18:13Z", "disk_format": "raw", "file": "/v2/images/%s/file" % UUID, "id": UUID, "kernel_id": "af81fccd-b2e8-4232-886c-aa98dda22882", "min_disk": 0, "min_ram": 0, "name": "img1", "owner": "411423405e10431fb9c47ac5b2446557", "protected": False, "ramdisk_id": "fdb3f864-9458-4185-bd26-5d27fe6b6adf", "schema": "/v2/schemas/image", "self": "/v2/images/%s" % UUID, "size": 145, "status": "active", "tags": [], "updated_at": "2015-07-24T12:18:13Z", "virtual_size": 123, "visibility": "private" } schema_fixture = { "additionalProperties": { "type": "string" }, "links": [ { "href": "{self}", "rel": "self" }, { "href": "{file}", "rel": "enclosure" }, { "href": "{schema}", "rel": "describedby" } ], "name": "image", "properties": { "architecture": { "description": "Operating system architecture as specified in " "http://docs.openstack.org/user-guide/common" "/cli_manage_images.html", "is_base": "false", "type": "string" }, "checksum": { "readOnly": True, "description": "md5 hash of image contents.", "maxLength": 32, "type": [ "null", "string" ] }, "container_format": { "description": "Format of the container", "enum": [ "null", "ami", "ari", "aki", "bare", "ovf", "ova", "docker" ], "type": [ "null", "string" ] }, "created_at": { "readOnly": True, "description": "Date and time of image registration", "type": "string" }, "direct_url": { "readOnly": True, "description": "URL to access the image file kept in external " "store", "type": "string" }, "disk_format": { "description": "Format of the disk", "enum": [ "null", "ami", "ari", "aki", "vhd", "vmdk", "raw", "qcow2", "vdi", "iso", "ploop" ], "type": [ "null", "string" ] }, "file": { "readOnly": True, "description": "An image file url", "type": "string" }, "id": { "description": "An identifier for the image", "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])" "{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", "type": "string" }, "instance_uuid": { "description": ("Metadata which can be used to record which " "instance this image is associated with. " "(Informational only, does not create an instance " "snapshot.)"), "is_base": "false", "type": "string" }, "kernel_id": { "description": "ID of image stored in Glance that should be used " "as the kernel when booting an AMI-style image.", "is_base": "false", "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])" "{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", "type": [ "null", "string" ] }, "locations": { "description": "A set of URLs to access the image file kept " "in external store", "items": { "properties": { "metadata": { "type": "object" }, "url": { "maxLength": 255, "type": "string" } }, "required": [ "url", "metadata" ], "type": "object" }, "type": "array" }, "min_disk": { "description": "Amount of disk space (in GB) required to " "boot image.", "type": "integer" }, "min_ram": { "description": "Amount of ram (in MB) required to boot image.", "type": "integer" }, "name": { "description": "Descriptive name for the image", "maxLength": 255, "type": [ "null", "string" ] }, "os_distro": { "description": "Common name of operating system distribution as " "specified in http://docs.openstack.org/trunk/" "openstack-compute/admin/content/" "adding-images.html", "is_base": "false", "type": "string" }, "os_version": { "description": "Operating system version as specified " "by the distributor", "is_base": "false", "type": "string" }, "owner": { "description": "Owner of the image", "maxLength": 255, "type": [ "null", "string" ] }, "protected": { "description": "If true, image will not be deletable.", "type": "boolean" }, "ramdisk_id": { "description": "ID of image stored in Glance that should be used " "as the ramdisk when booting an AMI-style image.", "is_base": "false", "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])" "{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", "type": [ "null", "string" ] }, "schema": { "readOnly": True, "description": "An image schema url", "type": "string" }, "self": { "readOnly": True, "description": "An image self url", "type": "string" }, "size": { "readOnly": True, "description": "Size of image file in bytes", "type": [ "null", "integer" ] }, "status": { "readOnly": True, "description": "Status of the image", "enum": [ "deactivated", "queued", "saving", "active", "killed", "deleted", "pending_delete", "uploading", "importing" ], "type": "string" }, "tags": { "description": "List of strings related to the image", "items": { "maxLength": 255, "type": "string" }, "type": "array" }, "updated_at": { "readOnly": True, "description": "Date and time of the last image " "modification", "type": "string" }, "virtual_size": { "readOnly": True, "description": "Virtual size of image in bytes", "type": [ "null", "integer" ] }, "visibility": { "description": "Scope of image accessibility", "enum": [ "public", "private", "community", "shared" ], "type": "string" } } } image_versions_fixture = { "versions": [ { "id": "v2.3", "links": [ { "href": "http://localhost:9292/v2/", "rel": "self" } ], "status": "CURRENT" }, { "id": "v2.2", "links": [ { "href": "http://localhost:9292/v2/", "rel": "self" } ], "status": "SUPPORTED" }, { "id": "v2.1", "links": [ { "href": "http://localhost:9292/v2/", "rel": "self" } ], "status": "SUPPORTED" }, { "id": "v2.0", "links": [ { "href": "http://localhost:9292/v2/", "rel": "self" } ], "status": "SUPPORTED" }, { "id": "v1.1", "links": [ { "href": "http://localhost:9292/v1/", "rel": "self" } ], "status": "SUPPORTED" }, { "id": "v1.0", "links": [ { "href": "http://localhost:9292/v1/", "rel": "self" } ], "status": "SUPPORTED" } ] } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_cache.py0000664000175000017500000001134200000000000025266 0ustar00zuulzuul00000000000000# Copyright 2021 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 testtools from unittest import mock from glanceclient.common import utils as common_utils from glanceclient import exc from glanceclient.tests import utils from glanceclient.v2 import cache data_fixtures = { '/v2/cache': { 'GET': ( {}, { 'cached_images': [ { 'id': 'b0aa672a-bc26-4fcb-8be1-f53ca361943d', 'Last Accessed (UTC)': '2021-08-09T07:08:20.214543', 'Last Modified (UTC)': '2021-08-09T07:08:20.214543', 'Size': 13267968, 'Hits': 0 }, { 'id': 'df601a47-7251-4d20-84ae-07de335af424', 'Last Accessed (UTC)': '2021-08-09T07:08:20.214543', 'Last Modified (UTC)': '2021-08-09T07:08:20.214543', 'Size': 13267968, 'Hits': 0 }, ], 'queued_images': [ '3a4560a1-e585-443e-9b39-553b46ec92d1', '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' ], }, ), 'DELETE': ( {}, '', ), }, '/v2/cache/3a4560a1-e585-443e-9b39-553b46ec92d1': { 'PUT': ( {}, '', ), 'DELETE': ( {}, '', ), }, } class TestCacheController(testtools.TestCase): def setUp(self): super(TestCacheController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.controller = cache.Controller(self.api) @mock.patch.object(common_utils, 'has_version') def test_list_cached(self, mock_has_version): mock_has_version.return_value = True images = self.controller.list() # Verify that we have 2 cached and 2 queued images self.assertEqual(2, len(images['cached_images'])) self.assertEqual(2, len(images['queued_images'])) @mock.patch.object(common_utils, 'has_version') def test_list_cached_empty_response(self, mock_has_version): dummy_fixtures = { '/v2/cache': { 'GET': ( {}, { 'cached_images': [], 'queued_images': [], }, ), } } dummy_api = utils.FakeAPI(dummy_fixtures) dummy_controller = cache.Controller(dummy_api) mock_has_version.return_value = True images = dummy_controller.list() # Verify that we have 0 cached and 0 queued images self.assertEqual(0, len(images['cached_images'])) self.assertEqual(0, len(images['queued_images'])) @mock.patch.object(common_utils, 'has_version') def test_queue_image(self, mock_has_version): mock_has_version.return_value = True image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' self.controller.queue(image_id) expect = [('PUT', '/v2/cache/%s' % image_id, {}, None)] self.assertEqual(expect, self.api.calls) @mock.patch.object(common_utils, 'has_version') def test_cache_clear_with_header(self, mock_has_version): mock_has_version.return_value = True self.controller.clear("cache") expect = [('DELETE', '/v2/cache', {'x-image-cache-clear-target': 'cache'}, None)] self.assertEqual(expect, self.api.calls) @mock.patch.object(common_utils, 'has_version') def test_cache_delete(self, mock_has_version): mock_has_version.return_value = True image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' self.controller.delete(image_id) expect = [('DELETE', '/v2/cache/%s' % image_id, {}, None)] self.assertEqual(expect, self.api.calls) @mock.patch.object(common_utils, 'has_version') def test_cache_not_supported(self, mock_has_version): mock_has_version.return_value = False self.assertRaises(exc.HTTPNotImplemented, self.controller.list) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_client_requests.py0000664000175000017500000000764300000000000027445 0ustar00zuulzuul00000000000000# Copyright (c) 2015 OpenStack Foundation # 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 requests_mock.contrib import fixture as rm_fixture from glanceclient import client from glanceclient.tests.unit.v2.fixtures import image_create_fixture from glanceclient.tests.unit.v2.fixtures import image_list_fixture from glanceclient.tests.unit.v2.fixtures import image_show_fixture from glanceclient.tests.unit.v2.fixtures import schema_fixture from glanceclient.tests import utils as testutils from glanceclient.v2.image_schema import _BASE_SCHEMA DEFAULT_PAGE_SIZE = 200 class ClientTestRequests(testutils.TestCase): """Client tests using the requests mock library.""" def test_list_bad_image_schema(self): # if kernel_id or ramdisk_id are not uuids, verify we can # still perform an image listing. Regression test for bug # 1477910 self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/v2/schemas/image', json=schema_fixture) self.requests.get('http://example.com/v2/images?' f'limit={DEFAULT_PAGE_SIZE}', json=image_list_fixture) gc = client.Client(2.2, "http://example.com/v2.1") images = gc.images.list() for image in images: pass def test_show_bad_image_schema(self): # if kernel_id or ramdisk_id are not uuids, verify we # don't fail due to schema validation self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/v2/schemas/image', json=schema_fixture) self.requests.get('http://example.com/v2/images/%s' % image_show_fixture['id'], json=image_show_fixture) gc = client.Client(2.2, "http://example.com/v2.1") img = gc.images.get(image_show_fixture['id']) self.assertEqual(image_show_fixture['checksum'], img['checksum']) def test_invalid_disk_format(self): self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/v2/schemas/image', json=_BASE_SCHEMA) self.requests.post('http://example.com/v2/images', json=image_create_fixture) self.requests.get('http://example.com/v2/images/%s' % image_show_fixture['id'], json=image_show_fixture) gc = client.Client(2.2, "http://example.com/v2.1") fields = {"disk_format": "qbull2"} try: gc.images.create(**fields) self.fail("Failed to raise exception when using bad disk format") except TypeError: pass def test_valid_disk_format(self): self.requests = self.useFixture(rm_fixture.Fixture()) self.requests.get('http://example.com/v2/schemas/image', json=_BASE_SCHEMA) self.requests.post('http://example.com/v2/images', json=image_create_fixture) self.requests.get('http://example.com/v2/images/%s' % image_show_fixture['id'], json=image_show_fixture) gc = client.Client(2.2, "http://example.com/v2.1") fields = {"disk_format": "vhdx"} gc.images.create(**fields) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_images.py0000664000175000017500000015417400000000000025503 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. import errno import hashlib import testtools from unittest import mock import ddt from glanceclient.common import utils as common_utils from glanceclient import exc from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import images _CHKSUM = '93264c3edf5972c9f1cb309543d38a5c' _CHKSUM1 = '54264c3edf5972c9f1cb309453d38a46' _HASHVAL = '54264c3edf93264c3edf5972c9f1cb309543d38a5c5972c9f1cb309453d38a46' _HASHVAL1 = 'cb309543d38a5c5972c9f1cb309453d38a4654264c3edf93264c3edf5972c9f1' _HASHBAD = '93264c3edf597254264c3edf5972c9f1cb309453d38a46c9f1cb309543d38a5c' _TAG1 = 'power' _TAG2 = '64bit' _BOGUS_ID = '63e7f218-29de-4477-abdc-8db7c9533188' _EVERYTHING_ID = '802cbbb7-0379-4c38-853f-37302b5e3d29' _OWNED_IMAGE_ID = 'a4963502-acc7-42ba-ad60-5aa0962b7faf' _OWNER_ID = '6bd473f0-79ae-40ad-a927-e07ec37b642f' _PRIVATE_ID = 'e33560a7-3964-4de5-8339-5a24559f99ab' _PUBLIC_ID = '857806e7-05b6-48e0-9d40-cb0e6fb727b9' _SHARED_ID = '331ac905-2a38-44c5-a83d-653db8f08313' _COMMUNITY_ID = '609ec9fc-0ee4-44c4-854d-0480af576929' _STATUS_REJECTED_ID = 'f3ea56ff-d7e4-4451-998c-1e3d33539c8e' data_fixtures = { '/v2/schemas/image': { 'GET': ( {}, { 'name': 'image', 'properties': { 'id': {}, 'name': {}, 'locations': { 'type': 'array', 'items': { 'type': 'object', 'properties': { 'metadata': {'type': 'object'}, 'url': {'type': 'string'}, }, 'required': ['url', 'metadata'], }, }, 'color': {'type': 'string', 'is_base': False}, }, 'additionalProperties': {'type': 'string'}, }, ), }, '/v2/images?limit=%d' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?limit=2': { 'GET': ( {}, { 'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ], 'next': ('/v2/images?limit=2&' 'marker=6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'), }, ), }, '/v2/images?limit=1': { 'GET': ( {}, { 'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ], 'next': ('/v2/images?limit=1&' 'marker=3a4560a1-e585-443e-9b39-553b46ec92d1'), }, ), }, ('/v2/images?limit=1&marker=3a4560a1-e585-443e-9b39-553b46ec92d1'): { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, ('/v2/images?limit=1&marker=6f99bf80-2ee6-47cf-acfe-1f1fabb7e810'): { 'GET': ( {}, {'images': [ { 'id': '3f99bf80-2ee6-47cf-acfe-1f1fabb7e811', 'name': 'image-3', }, ]}, ), }, '/v2/images/3a4560a1-e585-443e-9b39-553b46ec92d1': { 'GET': ( {}, { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ), 'PATCH': ( {}, '', ), }, '/v2/images/e7e59ff6-fa2e-4075-87d3-1a1398a07dc3': { 'GET': ( {}, { 'id': 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3', 'name': 'image-3', 'barney': 'rubble', 'george': 'jetson', 'color': 'red', }, ), 'PATCH': ( {}, '', ), }, '/v2/images': { 'POST': ( {}, { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ), }, '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a': { 'DELETE': ( {}, { 'id': '87b634c1-f893-33c9-28a9-e5673c99239a', }, ), }, '/v2/images/606b0e88-7c5a-4d54-b5bb-046105d4de6f/file': { 'PUT': ( {}, '', ), }, '/v2/images/5cc4bebc-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( {}, 'A', ), }, '/v2/images/5cc4bebc-db27-11e1-a1eb-080027cbe205': { 'GET': ( {}, {}, ), }, '/v2/images/headeronly-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( { 'content-md5': 'wrong' }, 'BB', ), }, '/v2/images/headeronly-db27-11e1-a1eb-080027cbe205': { 'GET': ( {}, {}, ), }, '/v2/images/chkonly-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( { 'content-md5': 'wrong' }, 'BB', ), }, '/v2/images/chkonly-db27-11e1-a1eb-080027cbe205': { 'GET': ( {}, { 'checksum': 'wrong', }, ), }, '/v2/images/multihash-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( { 'content-md5': 'wrong' }, 'BB', ), }, '/v2/images/multihash-db27-11e1-a1eb-080027cbe205': { 'GET': ( {}, { 'checksum': 'wrong', 'os_hash_algo': 'md5', 'os_hash_value': 'junk' }, ), }, '/v2/images/badalgo-db27-11e1-a1eb-080027cbe205/file': { 'GET': ( { 'content-md5': hashlib.md5(b'BB').hexdigest() }, 'BB', ), }, '/v2/images/badalgo-db27-11e1-a1eb-080027cbe205': { 'GET': ( {}, { 'checksum': hashlib.md5(b'BB').hexdigest(), 'os_hash_algo': 'not_an_algo', 'os_hash_value': 'whatever' }, ), }, '/v2/images/bad-multihash-value-good-checksum/file': { 'GET': ( { 'content-md5': hashlib.md5(b'GOODCHECKSUM').hexdigest() }, 'GOODCHECKSUM', ), }, '/v2/images/bad-multihash-value-good-checksum': { 'GET': ( {}, { 'checksum': hashlib.md5(b'GOODCHECKSUM').hexdigest(), 'os_hash_algo': 'sha512', 'os_hash_value': 'badmultihashvalue' }, ), }, '/v2/images/headeronly-dd57-11e1-af0f-02163e68b1d8/file': { 'GET': ( { 'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae' }, 'CCC', ), }, '/v2/images/headeronly-dd57-11e1-af0f-02163e68b1d8': { 'GET': ( {}, {}, ), }, '/v2/images/chkonly-dd57-11e1-af0f-02163e68b1d8/file': { 'GET': ( { 'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae' }, 'CCC', ), }, '/v2/images/chkonly-dd57-11e1-af0f-02163e68b1d8': { 'GET': ( {}, { 'checksum': 'defb99e69a9f1f6e06f15006b1f166ae', }, ), }, '/v2/images/multihash-dd57-11e1-af0f-02163e68b1d8/file': { 'GET': ( { 'content-md5': 'defb99e69a9f1f6e06f15006b1f166ae' }, 'CCC', ), }, '/v2/images/multihash-dd57-11e1-af0f-02163e68b1d8': { 'GET': ( {}, { 'checksum': 'defb99e69a9f1f6e06f15006b1f166ae', 'os_hash_algo': 'sha384', 'os_hash_value': hashlib.sha384(b'CCC').hexdigest() }, ), }, '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a/actions/reactivate': { 'POST': ({}, None) }, '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a/actions/deactivate': { 'POST': ({}, None) }, '/v2/images/606b0e88-7c5a-4d54-b5bb-046105d4de6f/import': { 'POST': ({}, None) }, '/v2/images?limit=%d&visibility=public' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _PUBLIC_ID, 'harvey': 'lipshitz', }, ]}, ), }, '/v2/images?limit=%d&visibility=private' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _PRIVATE_ID, }, ]}, ), }, '/v2/images?limit=%d&visibility=shared' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _SHARED_ID, }, ]}, ), }, '/v2/images?limit=%d&visibility=community' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _COMMUNITY_ID, }, ]}, ), }, '/v2/images?limit=%d&member_status=rejected' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': _STATUS_REJECTED_ID, }, ]}, ), }, '/v2/images?limit=%d&member_status=pending' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _OWNER_ID): { 'GET': ( {}, {'images': [ { 'id': _OWNED_IMAGE_ID, }, ]}, ), }, '/v2/images?limit=%d&owner=%s' % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&member_status=pending&owner=%s&visibility=shared' % (images.DEFAULT_PAGE_SIZE, _BOGUS_ID): { 'GET': ( {}, {'images': [ { 'id': _EVERYTHING_ID, }, ]}, ), }, '/v2/images?checksum=%s&limit=%d' % (_CHKSUM, images.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', } ]}, ), }, '/v2/images?checksum=%s&limit=%d' % (_CHKSUM1, images.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?checksum=wrong&limit=%d' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&os_hash_value=%s' % (images.DEFAULT_PAGE_SIZE, _HASHVAL): { 'GET': ( {}, {'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', } ]}, ), }, '/v2/images?limit=%d&os_hash_value=%s' % (images.DEFAULT_PAGE_SIZE, _HASHVAL1): { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?limit=%d&os_hash_value=%s' % (images.DEFAULT_PAGE_SIZE, _HASHBAD): { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG1): { 'GET': ( {}, {'images': [ { 'id': '3a4560a1-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', } ]}, ), }, '/v2/images?limit=%d&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG2): { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?limit=%d&tag=%s&tag=%s' % (images.DEFAULT_PAGE_SIZE, _TAG1, _TAG2): { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', } ]}, ), }, '/v2/images?limit=%d&tag=fake' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images/a2b83adc-888e-11e3-8872-78acc0b951d8': { 'GET': ( {}, { 'id': 'a2b83adc-888e-11e3-8872-78acc0b951d8', 'name': 'image-location-tests', 'locations': [{'url': 'http://foo.com/', 'metadata': {'foo': 'foometa'}}, {'url': 'http://bar.com/', 'metadata': {'bar': 'barmeta'}}], }, ), 'PATCH': ( {}, '', ) }, '/v2/images?limit=%d&os_distro=NixOS' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '8b052954-c76c-4e02-8e90-be89a70183a8', 'name': 'image-5', 'os_distro': 'NixOS', }, ]}, ), }, '/v2/images?limit=%d&my_little_property=cant_be_this_cute' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': []}, ), }, '/v2/images?limit=%d&sort_key=name' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, ]}, ), }, '/v2/images?limit=%d&sort_key=name&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image', }, { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image', }, ]}, ), }, '/v2/images?limit=%d&sort_dir=desc&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, '/v2/images?limit=%d&sort_dir=desc&sort_key=name&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, '/v2/images?limit=%d&sort_dir=desc&sort_dir=asc&sort_key=name&sort_key=id' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, '/v2/images?limit=%d&sort=name%%3Adesc%%2Csize%%3Aasc' % images.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'images': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'name': 'image-2', }, { 'id': '2a4560b2-e585-443e-9b39-553b46ec92d1', 'name': 'image-1', }, ]}, ), }, '/v2/images/3a4560a1-e585-443e-9b39-553b46ec92d1/tasks': { 'GET': ( {}, {'tasks': [ { 'id': '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', 'status': 'succeed', 'message': 'Copied 44 MiB', 'updated_at': '2021-03-01T18:28:26.000000' } ]}, ), }, } schema_fixtures = { 'image': { 'GET': ( {}, { 'name': 'image', 'properties': { 'id': {}, 'name': {}, 'locations': { 'type': 'array', 'items': { 'type': 'object', 'properties': { 'metadata': {'type': 'object'}, 'url': {'type': 'string'}, }, 'required': ['url', 'metadata'], } }, 'color': {'type': 'string', 'is_base': False}, 'tags': {'type': 'array'}, }, 'additionalProperties': {'type': 'string'}, } ) } } @ddt.ddt class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, images.Controller) def test_image_tasks_supported(self): with mock.patch.object(common_utils, 'has_version') as mock_has_version: mock_has_version.return_value = True image_tasks = self.controller.get_associated_image_tasks( '3a4560a1-e585-443e-9b39-553b46ec92d1') self.assertEqual(1, len(image_tasks['tasks'])) def test_image_tasks_not_supported(self): with mock.patch.object(common_utils, 'has_version') as mock_has_version: mock_has_version.return_value = False self.assertRaises(exc.HTTPNotImplemented, self.controller.get_associated_image_tasks, '3a4560a1-e585-443e-9b39-553b46ec92d1') def test_list_images(self): images = self.controller.list() self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id) self.assertEqual('image-1', images[0].name) self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id) self.assertEqual('image-2', images[1].name) def test_list_images_paginated(self): images = self.controller.list(page_size=1) self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id) self.assertEqual('image-1', images[0].name) self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id) self.assertEqual('image-2', images[1].name) def test_list_images_paginated_with_limit(self): images = self.controller.list(limit=3, page_size=2) self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', images[0].id) self.assertEqual('image-1', images[0].name) self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[1].id) self.assertEqual('image-2', images[1].name) self.assertEqual('3f99bf80-2ee6-47cf-acfe-1f1fabb7e811', images[2].id) self.assertEqual('image-3', images[2].name) self.assertEqual(3, len(images)) def test_list_images_with_marker(self): images = self.controller.list( limit=1, marker='3a4560a1-e585-443e-9b39-553b46ec92d1') self.assertEqual('6f99bf80-2ee6-47cf-acfe-1f1fabb7e810', images[0].id) self.assertEqual('image-2', images[0].name) def test_list_images_visibility_public(self): filters = {'filters': {'visibility': 'public'}} images = self.controller.list(**filters) self.assertEqual(_PUBLIC_ID, images[0].id) def test_list_images_visibility_private(self): filters = {'filters': {'visibility': 'private'}} images = self.controller.list(**filters) self.assertEqual(_PRIVATE_ID, images[0].id) def test_list_images_visibility_shared(self): filters = {'filters': {'visibility': 'shared'}} images = self.controller.list(**filters) self.assertEqual(_SHARED_ID, images[0].id) def test_list_images_visibility_community(self): filters = {'filters': {'visibility': 'community'}} images = list(self.controller.list(**filters)) self.assertEqual(_COMMUNITY_ID, images[0].id) def test_list_images_member_status_rejected(self): filters = {'filters': {'member_status': 'rejected'}} images = self.controller.list(**filters) self.assertEqual(_STATUS_REJECTED_ID, images[0].id) def test_list_images_for_owner(self): filters = {'filters': {'owner': _OWNER_ID}} images = self.controller.list(**filters) self.assertEqual(_OWNED_IMAGE_ID, images[0].id) def test_list_images_for_checksum_single_image(self): fake_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' filters = {'filters': {'checksum': _CHKSUM}} images = self.controller.list(**filters) self.assertEqual(1, len(images)) self.assertEqual('%s' % fake_id, images[0].id) def test_list_images_for_checksum_multiple_images(self): fake_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' fake_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' filters = {'filters': {'checksum': _CHKSUM1}} images = self.controller.list(**filters) self.assertEqual(2, len(images)) self.assertEqual('%s' % fake_id1, images[0].id) self.assertEqual('%s' % fake_id2, images[1].id) def test_list_images_for_wrong_checksum(self): filters = {'filters': {'checksum': 'wrong'}} images = self.controller.list(**filters) self.assertEqual(0, len(images)) def test_list_images_for_hash_single_image(self): fake_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' filters = {'filters': {'os_hash_value': _HASHVAL}} images = self.controller.list(**filters) self.assertEqual(1, len(images)) self.assertEqual('%s' % fake_id, images[0].id) def test_list_images_for_hash_multiple_images(self): fake_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' fake_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' filters = {'filters': {'os_hash_value': _HASHVAL1}} images = self.controller.list(**filters) self.assertEqual(2, len(images)) self.assertEqual('%s' % fake_id1, images[0].id) self.assertEqual('%s' % fake_id2, images[1].id) def test_list_images_for_wrong_hash(self): filters = {'filters': {'os_hash_value': _HASHBAD}} images = self.controller.list(**filters) self.assertEqual(0, len(images)) def test_list_images_for_bogus_owner(self): filters = {'filters': {'owner': _BOGUS_ID}} images = self.controller.list(**filters) self.assertEqual([], images) def test_list_images_for_bunch_of_filters(self): filters = {'filters': {'owner': _BOGUS_ID, 'visibility': 'shared', 'member_status': 'pending'}} images = self.controller.list(**filters) self.assertEqual(_EVERYTHING_ID, images[0].id) def test_list_images_filters_encoding(self): filters = {"owner": u"ni\xf1o"} try: self.controller.list(filters=filters) except KeyError: # NOTE(flaper87): It raises KeyError because there's # no fixture supporting this query: # /v2/images?owner=ni%C3%B1o&limit=20 # We just want to make sure filters are correctly encoded. pass self.assertEqual(b"ni\xc3\xb1o", filters["owner"]) def test_list_images_for_tag_single_image(self): img_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' filters = {'filters': {'tag': [_TAG1]}} images = self.controller.list(**filters) self.assertEqual(1, len(images)) self.assertEqual('%s' % img_id, images[0].id) def test_list_images_for_tag_multiple_images(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' img_id2 = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' filters = {'filters': {'tag': [_TAG2]}} images = self.controller.list(**filters) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[0].id) self.assertEqual('%s' % img_id2, images[1].id) def test_list_images_for_multi_tags(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' filters = {'filters': {'tag': [_TAG1, _TAG2]}} images = self.controller.list(**filters) self.assertEqual(1, len(images)) self.assertEqual('%s' % img_id1, images[0].id) def test_list_images_for_non_existent_tag(self): filters = {'filters': {'tag': ['fake']}} images = self.controller.list(**filters) self.assertEqual(0, len(images)) def test_list_images_for_invalid_tag(self): filters = {'filters': {'tag': [[]]}} self.assertRaises(exc.HTTPBadRequest, self.controller.list, **filters) def test_list_images_with_single_sort_key(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = 'name' images = self.controller.list(sort_key=sort_key) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[0].id) def test_list_with_multiple_sort_keys(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = ['name', 'id'] images = self.controller.list(sort_key=sort_key) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[0].id) def test_list_images_with_desc_sort_dir(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = 'id' sort_dir = 'desc' images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_with_multiple_sort_keys_and_one_sort_dir(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = ['name', 'id'] sort_dir = 'desc' images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_with_multiple_sort_dirs(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort_key = ['name', 'id'] sort_dir = ['desc', 'asc'] images = self.controller.list(sort_key=sort_key, sort_dir=sort_dir) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_with_new_sorting_syntax(self): img_id1 = '2a4560b2-e585-443e-9b39-553b46ec92d1' sort = 'name:desc,size:asc' images = self.controller.list(sort=sort) self.assertEqual(2, len(images)) self.assertEqual('%s' % img_id1, images[1].id) def test_list_images_sort_dirs_fewer_than_keys(self): sort_key = ['name', 'id', 'created_at'] sort_dir = ['desc', 'asc'] self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort_key=sort_key, sort_dir=sort_dir) def test_list_images_combined_syntax(self): sort_key = ['name', 'id'] sort_dir = ['desc', 'asc'] sort = 'name:asc' self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort=sort, sort_key=sort_key, sort_dir=sort_dir) def test_list_images_new_sorting_syntax_invalid_key(self): sort = 'INVALID:asc' self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort=sort) def test_list_images_new_sorting_syntax_invalid_direction(self): sort = 'name:INVALID' self.assertRaises(exc.HTTPBadRequest, self.controller.list, sort=sort) def test_list_images_for_property(self): filters = {'filters': dict([('os_distro', 'NixOS')])} images = self.controller.list(**filters) self.assertEqual(1, len(images)) def test_list_images_for_non_existent_property(self): filters = {'filters': dict([('my_little_property', 'cant_be_this_cute')])} images = self.controller.list(**filters) self.assertEqual(0, len(images)) def test_get_image(self): image = self.controller.get('3a4560a1-e585-443e-9b39-553b46ec92d1') self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', image.id) self.assertEqual('image-1', image.name) def test_create_image(self): properties = { 'name': 'image-1' } image = self.controller.create(**properties) self.assertEqual('3a4560a1-e585-443e-9b39-553b46ec92d1', image.id) self.assertEqual('image-1', image.name) def test_create_bad_additionalProperty_type(self): properties = { 'name': 'image-1', 'bad_prop': True, } with testtools.ExpectedException(TypeError): self.controller.create(**properties) def test_delete_image(self): self.controller.delete('87b634c1-f893-33c9-28a9-e5673c99239a') expect = [ ('DELETE', '/v2/images/87b634c1-f893-33c9-28a9-e5673c99239a', {}, None)] self.assertEqual(expect, self.api.calls) def test_deactivate_image(self): id_image = '87b634c1-f893-33c9-28a9-e5673c99239a' self.controller.deactivate(id_image) expect = [('POST', '/v2/images/%s/actions/deactivate' % id_image, {}, None)] self.assertEqual(expect, self.api.calls) def test_reactivate_image(self): id_image = '87b634c1-f893-33c9-28a9-e5673c99239a' self.controller.reactivate(id_image) expect = [('POST', '/v2/images/%s/actions/reactivate' % id_image, {}, None)] self.assertEqual(expect, self.api.calls) def test_data_upload(self): image_data = 'CCC' image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f' self.controller.upload(image_id, image_data) expect = [('PUT', '/v2/images/%s/file' % image_id, {'Content-Type': 'application/octet-stream'}, image_data)] self.assertEqual(expect, self.api.calls) def test_data_upload_w_size(self): image_data = 'CCC' image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f' self.controller.upload(image_id, image_data, image_size=3) expect = [('PUT', '/v2/images/%s/file' % image_id, {'Content-Type': 'application/octet-stream'}, image_data)] self.assertEqual(expect, self.api.calls) def test_data_without_checksum(self): body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('A', body) body = self.controller.data('5cc4bebc-db27-11e1-a1eb-080027cbe205') body = ''.join([b for b in body]) self.assertEqual('A', body) def test_data_with_wrong_checksum(self): body = self.controller.data('headeronly-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('BB', body) body = self.controller.data('headeronly-db27-11e1-a1eb-080027cbe205') try: body = ''.join([b for b in body]) self.fail('data did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong' self.assertIn(msg, str(e)) body = self.controller.data('chkonly-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('BB', body) body = self.controller.data('chkonly-db27-11e1-a1eb-080027cbe205') try: body = ''.join([b for b in body]) self.fail('data did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected wrong' self.assertIn(msg, str(e)) body = self.controller.data('multihash-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('BB', body) body = self.controller.data('multihash-db27-11e1-a1eb-080027cbe205') try: body = ''.join([b for b in body]) self.fail('data did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'was 9d3d9048db16a7eee539e93e3618cbe7 expected junk' self.assertIn(msg, str(e)) body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('BB', body) try: body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205') self.fail('bad os_hash_algo did not raise an error.') except ValueError as e: msg = 'unsupported hash type not_an_algo' self.assertIn(msg, str(e)) def test_data_with_checksum(self): for prefix in ['headeronly', 'chkonly', 'multihash']: body = self.controller.data(prefix + '-dd57-11e1-af0f-02163e68b1d8', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('CCC', body) body = self.controller.data(prefix + '-dd57-11e1-af0f-02163e68b1d8') body = ''.join([b for b in body]) self.assertEqual('CCC', body) @ddt.data('headeronly', 'chkonly', 'multihash') def test_data_with_checksum_but_no_md5_algo(self, prefix): with mock.patch('hashlib.new', mock.MagicMock( side_effect=ValueError('unsupported hash type'))): body = self.controller.data(prefix + '-dd57-11e1-af0f-02163e68b1d8', allow_md5_fallback=True) try: body = ''.join([b for b in body]) self.fail('missing md5 algo did not raise an error') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'md5 algorithm is not available on the client' self.assertIn(msg, str(e)) def test_data_with_checksum_and_fallback(self): # make sure the allow_md5_fallback option does not cause any # incorrect behavior when fallback is not needed for prefix in ['headeronly', 'chkonly', 'multihash']: body = self.controller.data(prefix + '-dd57-11e1-af0f-02163e68b1d8', do_checksum=False, allow_md5_fallback=True) body = ''.join([b for b in body]) self.assertEqual('CCC', body) body = self.controller.data(prefix + '-dd57-11e1-af0f-02163e68b1d8', allow_md5_fallback=True) body = ''.join([b for b in body]) self.assertEqual('CCC', body) def test_data_with_bad_hash_algo_and_fallback(self): # shouldn't matter when do_checksum is False body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205', do_checksum=False, allow_md5_fallback=True) body = ''.join([b for b in body]) self.assertEqual('BB', body) # default value for do_checksum is True body = self.controller.data('badalgo-db27-11e1-a1eb-080027cbe205', allow_md5_fallback=True) body = ''.join([b for b in body]) self.assertEqual('BB', body) def test_neg_data_with_bad_hash_value_and_fallback_enabled(self): # make sure download fails when good hash_algo but bad hash_value # even when correct checksum is present regardless of # allow_md5_fallback setting body = self.controller.data('bad-multihash-value-good-checksum', allow_md5_fallback=False) try: body = ''.join([b for b in body]) self.fail('bad os_hash_value did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'expected badmultihashvalue' self.assertIn(msg, str(e)) body = self.controller.data('bad-multihash-value-good-checksum', allow_md5_fallback=True) try: body = ''.join([b for b in body]) self.fail('bad os_hash_value did not raise an error.') except IOError as e: self.assertEqual(errno.EPIPE, e.errno) msg = 'expected badmultihashvalue' self.assertIn(msg, str(e)) # download should succeed when do_checksum is off, though body = self.controller.data('bad-multihash-value-good-checksum', do_checksum=False) body = ''.join([b for b in body]) self.assertEqual('GOODCHECKSUM', body) def test_image_import_web_download(self): uri = 'http://example.com/image.qcow' data = [('method', {'name': 'web-download', 'uri': uri})] image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f' self.controller.image_import(image_id, 'web-download', uri) expect = [('POST', '/v2/images/%s/import' % image_id, {}, data)] self.assertEqual(expect, self.api.calls) def test_image_import_glance_download(self): region = 'REGION2' remote_image_id = '75baf7b6-253a-11ed-8307-4b1057986a78' image_id = '606b0e88-7c5a-4d54-b5bb-046105d4de6f' service_interface = 'public' data = [('method', {'name': 'glance-download', 'glance_region': region, 'glance_image_id': remote_image_id, 'glance_service_interface': service_interface})] self.controller.image_import( image_id, 'glance-download', remote_region=region, remote_image_id=remote_image_id, remote_service_interface=service_interface) expect = [('POST', '/v2/images/%s/import' % image_id, {}, data)] self.assertEqual(expect, self.api.calls) def test_download_no_data(self): resp = utils.FakeResponse(headers={}, status_code=204) self.controller.controller.http_client.get = mock.Mock( return_value=(resp, {})) self.controller.data('image_id') def test_download_forbidden(self): self.controller.controller.http_client.get = mock.Mock( side_effect=exc.HTTPForbidden()) try: self.controller.data('image_id') self.fail('No forbidden exception raised.') except exc.HTTPForbidden: pass def test_update_replace_prop(self): image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' params = {'name': 'pong'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'replace'), ('path', '/name'), ('value', 'pong')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-1', image.name) def test_update_add_prop(self): image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' params = {'finn': 'human'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'add'), ('path', '/finn'), ('value', 'human')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-1', image.name) def test_update_remove_prop(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' remove_props = ['barney'] image = self.controller.update(image_id, remove_props) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'remove'), ('path', '/barney')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-3', image.name) def test_update_replace_remove_same_prop(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' # Updating a property takes precedence over removing a property params = {'barney': 'miller'} remove_props = ['barney'] image = self.controller.update(image_id, remove_props, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = ([[('op', 'replace'), ('path', '/barney'), ('value', 'miller')]]) expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-3', image.name) def test_update_add_remove_same_prop(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' # Adding a property takes precedence over removing a property params = {'finn': 'human'} remove_props = ['finn'] image = self.controller.update(image_id, remove_props, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'add'), ('path', '/finn'), ('value', 'human')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) # NOTE(bcwaldon):due to limitations of our fake api framework, the name # will not actually change - yet in real life it will... self.assertEqual('image-3', image.name) def test_update_bad_additionalProperty_type(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' params = {'name': 'pong', 'bad_prop': False} with testtools.ExpectedException(TypeError): self.controller.update(image_id, **params) def test_update_add_custom_property(self): image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' params = {'color': 'red'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'add'), ('path', '/color'), ('value', 'red')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) def test_update_replace_custom_property(self): image_id = 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3' params = {'color': 'blue'} image = self.controller.update(image_id, **params) expect_hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch', } expect_body = [[('op', 'replace'), ('path', '/color'), ('value', 'blue')]] expect = [ ('GET', '/v2/images/%s' % image_id, {}, None), ('PATCH', '/v2/images/%s' % image_id, expect_hdrs, expect_body), ('GET', '/v2/images/%s' % image_id, {'x-openstack-request-id': 'req-1234'}, None), ] self.assertEqual(expect, self.api.calls) self.assertEqual(image_id, image.id) def test_location_ops_when_server_disabled_location_ops(self): # Location operations should not be allowed if server has not # enabled location related operations. There is no need to check it # when do location add, because the check would be done in server side. image_id = '3a4560a1-e585-443e-9b39-553b46ec92d1' estr = 'The administrator has disabled API access to image locations' url = 'http://bar.com/' meta = {'bar': 'barmeta'} e = self.assertRaises(exc.HTTPBadRequest, self.controller.delete_locations, image_id, set([url])) self.assertIn(estr, str(e)) e = self.assertRaises(exc.HTTPBadRequest, self.controller.update_location, image_id, url, meta) self.assertIn(estr, str(e)) def _empty_get(self, image_id, headers=None): return ('GET', '/v2/images/%s' % image_id, headers or {}, None) def _patch_req(self, image_id, patch_body): c_type = 'application/openstack-images-v2.1-json-patch' data = [sorted(d.items()) for d in patch_body] return ('PATCH', '/v2/images/%s' % image_id, {'Content-Type': c_type}, data) def test_add_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://spam.com/', 'metadata': {'spam': 'ham'}} add_patch = {'path': '/locations/-', 'value': new_loc, 'op': 'add'} headers = {'x-openstack-request-id': 'req-1234'} self.controller.add_location(image_id, **new_loc) self.assertEqual([self._patch_req(image_id, [add_patch]), self._empty_get(image_id, headers=headers)], self.api.calls) @mock.patch.object(images.Controller, '_send_image_update_request', side_effect=exc.HTTPBadRequest) def test_add_duplicate_location(self, mock_request): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://foo.com/', 'metadata': {'foo': 'newfoo'}} self.assertRaises(exc.HTTPBadRequest, self.controller.add_location, image_id, **new_loc) def test_remove_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' url_set = set(['http://foo.com/', 'http://bar.com/']) del_patches = [{'path': '/locations/1', 'op': 'remove'}, {'path': '/locations/0', 'op': 'remove'}] self.controller.delete_locations(image_id, url_set) self.assertEqual([self._empty_get(image_id), self._patch_req(image_id, del_patches)], self.api.calls) def test_remove_missing_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' url_set = set(['http://spam.ham/']) err_str = 'Unknown URL(s): %s' % list(url_set) err = self.assertRaises(exc.HTTPNotFound, self.controller.delete_locations, image_id, url_set) self.assertIn(err_str, str(err)) def test_update_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://foo.com/', 'metadata': {'spam': 'ham'}} headers = {'x-openstack-request-id': 'req-1234'} fixture_idx = '/v2/images/%s' % (image_id) orig_locations = data_fixtures[fixture_idx]['GET'][1]['locations'] loc_map = dict([(l['url'], l) for l in orig_locations]) loc_map[new_loc['url']] = new_loc mod_patch = [{'path': '/locations', 'op': 'replace', 'value': list(loc_map.values())}] self.controller.update_location(image_id, **new_loc) self.assertEqual([self._empty_get(image_id), self._patch_req(image_id, mod_patch), self._empty_get(image_id, headers=headers)], self.api.calls) def test_update_tags(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' tag_map = {'tags': ['tag01', 'tag02', 'tag03']} headers = {'x-openstack-request-id': 'req-1234'} image = self.controller.update(image_id, **tag_map) expected_body = [{'path': '/tags', 'op': 'replace', 'value': tag_map['tags']}] expected = [ self._empty_get(image_id), self._patch_req(image_id, expected_body), self._empty_get(image_id, headers=headers) ] self.assertEqual(expected, self.api.calls) self.assertEqual(image_id, image.id) def test_update_missing_location(self): image_id = 'a2b83adc-888e-11e3-8872-78acc0b951d8' new_loc = {'url': 'http://spam.com/', 'metadata': {'spam': 'ham'}} err_str = 'Unknown URL: %s' % new_loc['url'] err = self.assertRaises(exc.HTTPNotFound, self.controller.update_location, image_id, **new_loc) self.assertIn(err_str, str(err)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_info.py0000664000175000017500000000246700000000000025166 0ustar00zuulzuul00000000000000# Copyright 2022 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools from unittest import mock from glanceclient.v2 import info class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.fake_client = mock.MagicMock() self.info_controller = info.Controller(self.fake_client, None) def test_get_usage(self): fake_usage = { 'usage': { 'quota1': {'limit': 10, 'usage': 0}, 'quota2': {'limit': 20, 'usage': 5}, } } self.fake_client.get.return_value = (mock.MagicMock(), fake_usage) usage = self.info_controller.get_usage() self.assertEqual(fake_usage['usage'], usage) self.fake_client.get.assert_called_once_with('/v2/info/usage') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_members.py0000664000175000017500000001017200000000000025655 0ustar00zuulzuul00000000000000# 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. import testtools from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import image_members IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1' MEMBER = '11223344-5566-7788-9911-223344556677' data_fixtures = { '/v2/images/{image}/members'.format(image=IMAGE): { 'GET': ( {}, {'members': [ { 'image_id': IMAGE, 'member_id': MEMBER, }, ]}, ), 'POST': ( {}, { 'image_id': IMAGE, 'member_id': MEMBER, 'status': 'pending' } ) }, '/v2/images/{image}/members/{mem}'.format(image=IMAGE, mem=MEMBER): { 'GET': ( {}, { 'image_id': IMAGE, 'member_id': MEMBER, 'status': 'pending', 'created_at': '2013-11-26T07:21:21Z', 'updated_at': '2013-11-26T07:21:21Z', 'schema': "/v2/schemas/member" }, ), 'DELETE': ( {}, None, ), 'PUT': ( {}, { 'image_id': IMAGE, 'member_id': MEMBER, 'status': 'accepted' } ), } } schema_fixtures = { 'member': { 'GET': ( {}, { 'name': 'member', 'properties': { 'image_id': {}, 'member_id': {} } }, ) } } class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, image_members.Controller) def test_list_image_members(self): image_id = IMAGE image_members = self.controller.list(image_id) self.assertEqual(IMAGE, image_members[0].image_id) self.assertEqual(MEMBER, image_members[0].member_id) def test_get_image_members(self): image_member = self.controller.get(IMAGE, MEMBER) self.assertEqual(IMAGE, image_member.image_id) self.assertEqual(MEMBER, image_member.member_id) def test_delete_image_member(self): image_id = IMAGE member_id = MEMBER self.controller.delete(image_id, member_id) expect = [ ('DELETE', '/v2/images/{image}/members/{mem}'.format(image=IMAGE, mem=MEMBER), {}, None)] self.assertEqual(expect, self.api.calls) def test_update_image_members(self): image_id = IMAGE member_id = MEMBER status = 'accepted' image_member = self.controller.update(image_id, member_id, status) self.assertEqual(IMAGE, image_member.image_id) self.assertEqual(MEMBER, image_member.member_id) self.assertEqual(status, image_member.status) def test_create_image_members(self): image_id = IMAGE member_id = MEMBER status = 'pending' image_member = self.controller.create(image_id, member_id) self.assertEqual(IMAGE, image_member.image_id) self.assertEqual(MEMBER, image_member.member_id) self.assertEqual(status, image_member.status) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_metadefs_namespaces.py0000664000175000017500000006207300000000000030221 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. import testtools from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import metadefs DEFAULT_PAGE_SIZE = 20 NAMESPACE1 = 'Namespace1' NAMESPACE2 = 'Namespace2' NAMESPACE3 = 'Namespace3' NAMESPACE4 = 'Namespace4' NAMESPACE5 = 'Namespace5' NAMESPACE6 = 'Namespace6' NAMESPACE7 = 'Namespace7' NAMESPACE8 = 'Namespace8' NAMESPACENEW = 'NamespaceNew' RESOURCE_TYPE1 = 'ResourceType1' RESOURCE_TYPE2 = 'ResourceType2' OBJECT1 = 'Object1' PROPERTY1 = 'Property1' PROPERTY2 = 'Property2' def _get_namespace_fixture(ns_name, rt_name=RESOURCE_TYPE1, **kwargs): ns = { "display_name": "Flavor Quota", "description": "DESCRIPTION1", "self": "/v2/metadefs/namespaces/%s" % ns_name, "namespace": ns_name, "visibility": "public", "protected": True, "owner": "admin", "resource_types": [ { "name": rt_name } ], "schema": "/v2/schemas/metadefs/namespace", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ns.update(kwargs) return ns data_fixtures = { f"/v2/metadefs/namespaces?limit={DEFAULT_PAGE_SIZE}": { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=20", "namespaces": [ _get_namespace_fixture(NAMESPACE1), _get_namespace_fixture(NAMESPACE2), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces?limit=1": { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=1", "namespaces": [ _get_namespace_fixture(NAMESPACE7), ], "schema": "/v2/schemas/metadefs/namespaces", "next": "/v2/metadefs/namespaces?marker=%s&limit=1" % NAMESPACE7, } ) }, "/v2/metadefs/namespaces?limit=1&marker=%s" % NAMESPACE7: { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=2", "namespaces": [ _get_namespace_fixture(NAMESPACE8), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces?limit=2&marker=%s" % NAMESPACE6: { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=2", "namespaces": [ _get_namespace_fixture(NAMESPACE7), _get_namespace_fixture(NAMESPACE8), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, f"/v2/metadefs/namespaces?limit={DEFAULT_PAGE_SIZE}&sort_dir=asc": { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=1", "namespaces": [ _get_namespace_fixture(NAMESPACE1), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, f"/v2/metadefs/namespaces?limit={DEFAULT_PAGE_SIZE}&sort_key=created_at": { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=1", "namespaces": [ _get_namespace_fixture(NAMESPACE1), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces?limit=%d&resource_types=%s" % ( DEFAULT_PAGE_SIZE, RESOURCE_TYPE1): { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=20", "namespaces": [ _get_namespace_fixture(NAMESPACE3), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces?limit=%d&resource_types=" "%s%%2C%s" % (DEFAULT_PAGE_SIZE, RESOURCE_TYPE1, RESOURCE_TYPE2): { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=20", "namespaces": [ _get_namespace_fixture(NAMESPACE4), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, f"/v2/metadefs/namespaces?limit={DEFAULT_PAGE_SIZE}&visibility=private": { "GET": ( {}, { "first": "/v2/metadefs/namespaces?limit=20", "namespaces": [ _get_namespace_fixture(NAMESPACE5), ], "schema": "/v2/schemas/metadefs/namespaces" } ) }, "/v2/metadefs/namespaces": { "POST": ( {}, { "display_name": "Flavor Quota", "description": "DESCRIPTION1", "self": "/v2/metadefs/namespaces/%s" % 'NamespaceNew', "namespace": 'NamespaceNew', "visibility": "public", "protected": True, "owner": "admin", "schema": "/v2/schemas/metadefs/namespace", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ) }, "/v2/metadefs/namespaces/%s" % NAMESPACE1: { "GET": ( {}, { "display_name": "Flavor Quota", "description": "DESCRIPTION1", "objects": [ { "description": "DESCRIPTION2", "name": "OBJECT1", "self": "/v2/metadefs/namespaces/%s/objects/" % OBJECT1, "required": [], "properties": { PROPERTY1: { "type": "integer", "description": "DESCRIPTION3", "title": "Quota: CPU Shares" }, PROPERTY2: { "minimum": 1000, "type": "integer", "description": "DESCRIPTION4", "maximum": 1000000, "title": "Quota: CPU Period" }, }, "schema": "/v2/schemas/metadefs/object" } ], "self": "/v2/metadefs/namespaces/%s" % NAMESPACE1, "namespace": NAMESPACE1, "visibility": "public", "protected": True, "owner": "admin", "resource_types": [ { "name": RESOURCE_TYPE1 } ], "schema": "/v2/schemas/metadefs/namespace", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ), "PUT": ( {}, { "display_name": "Flavor Quota", "description": "DESCRIPTION1", "objects": [ { "description": "DESCRIPTION2", "name": "OBJECT1", "self": "/v2/metadefs/namespaces/%s/objects/" % OBJECT1, "required": [], "properties": { PROPERTY1: { "type": "integer", "description": "DESCRIPTION3", "title": "Quota: CPU Shares" }, PROPERTY2: { "minimum": 1000, "type": "integer", "description": "DESCRIPTION4", "maximum": 1000000, "title": "Quota: CPU Period" }, }, "schema": "/v2/schemas/metadefs/object" } ], "self": "/v2/metadefs/namespaces/%s" % NAMESPACENEW, "namespace": NAMESPACENEW, "visibility": "public", "protected": True, "owner": "admin", "resource_types": [ { "name": RESOURCE_TYPE1 } ], "schema": "/v2/schemas/metadefs/namespace", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ), "DELETE": ( {}, {} ) }, "/v2/metadefs/namespaces/%s?resource_type=%s" % (NAMESPACE6, RESOURCE_TYPE1): { "GET": ( {}, { "display_name": "Flavor Quota", "description": "DESCRIPTION1", "objects": [], "self": "/v2/metadefs/namespaces/%s" % NAMESPACE1, "namespace": NAMESPACE6, "visibility": "public", "protected": True, "owner": "admin", "resource_types": [ { "name": RESOURCE_TYPE1 } ], "schema": "/v2/schemas/metadefs/namespace", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ), }, } schema_fixtures = { "metadefs/namespace": { "GET": ( {}, { "additionalProperties": False, "definitions": { "property": { "additionalProperties": { "required": [ "title", "type" ], "type": "object", "properties": { "additionalItems": { "type": "boolean" }, "enum": { "type": "array" }, "description": { "type": "string" }, "title": { "type": "string" }, "default": {}, "minLength": { "$ref": "#/definitions/" "positiveIntegerDefault0" }, "required": { "$ref": "#/definitions/stringArray" }, "maximum": { "type": "number" }, "minItems": { "$ref": "#/definitions/" "positiveIntegerDefault0" }, "readonly": { "type": "boolean" }, "minimum": { "type": "number" }, "maxItems": { "$ref": "#/definitions/" "positiveInteger" }, "maxLength": { "$ref": "#/definitions/positiveInteger" }, "uniqueItems": { "default": False, "type": "boolean" }, "pattern": { "type": "string", "format": "regex" }, "items": { "type": "object", "properties": { "enum": { "type": "array" }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } }, "type": "object" }, "positiveIntegerDefault0": { "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] }, "stringArray": { "uniqueItems": True, "items": { "type": "string" }, "type": "array" }, "positiveInteger": { "minimum": 0, "type": "integer" } }, "required": [ "namespace" ], "name": "namespace", "properties": { "description": { "type": "string", "description": "Provides a user friendly description " "of the namespace.", "maxLength": 500 }, "updated_at": { "type": "string", "readOnly": True, "description": "Date and time of the last namespace " "modification", "format": "date-time" }, "visibility": { "enum": [ "public", "private" ], "type": "string", "description": "Scope of namespace accessibility." }, "self": { "type": "string" }, "objects": { "items": { "type": "object", "properties": { "properties": { "$ref": "#/definitions/property" }, "required": { "$ref": "#/definitions/stringArray" }, "name": { "type": "string" }, "description": { "type": "string" } } }, "type": "array" }, "owner": { "type": "string", "description": "Owner of the namespace.", "maxLength": 255 }, "resource_types": { "items": { "type": "object", "properties": { "prefix": { "type": "string" }, "name": { "type": "string" }, "metadata_type": { "type": "string" } } }, "type": "array" }, "properties": { "$ref": "#/definitions/property" }, "display_name": { "type": "string", "description": "The user friendly name for the " "namespace. Used by UI if available.", "maxLength": 80 }, "created_at": { "type": "string", "readOnly": True, "description": "Date and time of namespace creation ", "format": "date-time" }, "namespace": { "type": "string", "description": "The unique namespace text.", "maxLength": 80 }, "protected": { "type": "boolean", "description": "If true, namespace will not be " "deletable." }, "schema": { "type": "string" } } } ), } } class TestNamespaceController(testtools.TestCase): def setUp(self): super(TestNamespaceController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, metadefs.NamespaceController) def test_list_namespaces(self): namespaces = self.controller.list() self.assertEqual(2, len(namespaces)) self.assertEqual(NAMESPACE1, namespaces[0]['namespace']) self.assertEqual(NAMESPACE2, namespaces[1]['namespace']) def test_list_namespaces_paginate(self): namespaces = self.controller.list(page_size=1) self.assertEqual(2, len(namespaces)) self.assertEqual(NAMESPACE7, namespaces[0]['namespace']) self.assertEqual(NAMESPACE8, namespaces[1]['namespace']) def test_list_with_limit_greater_than_page_size(self): namespaces = self.controller.list(page_size=1, limit=2) self.assertEqual(2, len(namespaces)) self.assertEqual(NAMESPACE7, namespaces[0]['namespace']) self.assertEqual(NAMESPACE8, namespaces[1]['namespace']) def test_list_with_marker(self): namespaces = self.controller.list(marker=NAMESPACE6, page_size=2) self.assertEqual(2, len(namespaces)) self.assertEqual(NAMESPACE7, namespaces[0]['namespace']) self.assertEqual(NAMESPACE8, namespaces[1]['namespace']) def test_list_with_sort_dir(self): namespaces = self.controller.list(sort_dir='asc', limit=1) self.assertEqual(1, len(namespaces)) self.assertEqual(NAMESPACE1, namespaces[0]['namespace']) def test_list_with_sort_dir_invalid(self): # NOTE(TravT): The clients work by returning an iterator. # Invoking the iterator is what actually executes the logic. self.assertRaises(ValueError, self.controller.list, sort_dir='foo') def test_list_with_sort_key(self): namespaces = self.controller.list(sort_key='created_at', limit=1) self.assertEqual(1, len(namespaces)) self.assertEqual(NAMESPACE1, namespaces[0]['namespace']) def test_list_with_sort_key_invalid(self): # NOTE(TravT): The clients work by returning an iterator. # Invoking the iterator is what actually executes the logic. self.assertRaises(ValueError, self.controller.list, sort_key='foo') def test_list_namespaces_with_one_resource_type_filter(self): namespaces = self.controller.list( filters={ 'resource_types': [RESOURCE_TYPE1] } ) self.assertEqual(1, len(namespaces)) self.assertEqual(NAMESPACE3, namespaces[0]['namespace']) def test_list_namespaces_with_multiple_resource_types_filter(self): namespaces = self.controller.list( filters={ 'resource_types': [RESOURCE_TYPE1, RESOURCE_TYPE2] } ) self.assertEqual(1, len(namespaces)) self.assertEqual(NAMESPACE4, namespaces[0]['namespace']) def test_list_namespaces_with_visibility_filter(self): namespaces = self.controller.list( filters={ 'visibility': 'private' } ) self.assertEqual(1, len(namespaces)) self.assertEqual(NAMESPACE5, namespaces[0]['namespace']) def test_get_namespace(self): namespace = self.controller.get(NAMESPACE1) self.assertEqual(NAMESPACE1, namespace.namespace) self.assertTrue(namespace.protected) def test_get_namespace_with_resource_type(self): namespace = self.controller.get(NAMESPACE6, resource_type=RESOURCE_TYPE1) self.assertEqual(NAMESPACE6, namespace.namespace) self.assertTrue(namespace.protected) def test_create_namespace(self): properties = { 'namespace': NAMESPACENEW } namespace = self.controller.create(**properties) self.assertEqual(NAMESPACENEW, namespace.namespace) self.assertTrue(namespace.protected) def test_create_namespace_invalid_data(self): properties = {} self.assertRaises(TypeError, self.controller.create, **properties) def test_create_namespace_invalid_property(self): properties = {'namespace': 'NewNamespace', 'protected': '123'} self.assertRaises(TypeError, self.controller.create, **properties) def test_update_namespace(self): properties = {'display_name': 'My Updated Name'} namespace = self.controller.update(NAMESPACE1, **properties) self.assertEqual(NAMESPACE1, namespace.namespace) def test_update_namespace_invalid_property(self): properties = {'protected': '123'} self.assertRaises(TypeError, self.controller.update, NAMESPACE1, **properties) def test_update_namespace_disallowed_fields(self): properties = {'display_name': 'My Updated Name'} self.controller.update(NAMESPACE1, **properties) actual = self.api.calls _disallowed_fields = ['self', 'schema', 'created_at', 'updated_at'] for key in actual[1][3]: self.assertNotIn(key, _disallowed_fields) def test_delete_namespace(self): self.controller.delete(NAMESPACE1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s' % NAMESPACE1, {}, None)] self.assertEqual(expect, self.api.calls) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_metadefs_objects.py0000664000175000017500000003014000000000000027521 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. import testtools from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import metadefs NAMESPACE1 = 'Namespace1' OBJECT1 = 'Object1' OBJECT2 = 'Object2' OBJECTNEW = 'ObjectNew' PROPERTY1 = 'Property1' PROPERTY2 = 'Property2' PROPERTY3 = 'Property3' PROPERTY4 = 'Property4' def _get_object_fixture(ns_name, obj_name, **kwargs): obj = { "description": "DESCRIPTION", "name": obj_name, "self": "/v2/metadefs/namespaces/%s/objects/%s" % (ns_name, obj_name), "required": [], "properties": { PROPERTY1: { "type": "integer", "description": "DESCRIPTION", "title": "Quota: CPU Shares" }, PROPERTY2: { "minimum": 1000, "type": "integer", "description": "DESCRIPTION", "maximum": 1000000, "title": "Quota: CPU Period" }}, "schema": "/v2/schemas/metadefs/object", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } obj.update(kwargs) return obj data_fixtures = { "/v2/metadefs/namespaces/%s/objects" % NAMESPACE1: { "GET": ( {}, { "objects": [ _get_object_fixture(NAMESPACE1, OBJECT1), _get_object_fixture(NAMESPACE1, OBJECT2) ], "schema": "v2/schemas/metadefs/objects" } ), "POST": ( {}, _get_object_fixture(NAMESPACE1, OBJECTNEW) ), "DELETE": ( {}, {} ) }, "/v2/metadefs/namespaces/%s/objects/%s" % (NAMESPACE1, OBJECT1): { "GET": ( {}, _get_object_fixture(NAMESPACE1, OBJECT1) ), "PUT": ( {}, _get_object_fixture(NAMESPACE1, OBJECT1) ), "DELETE": ( {}, {} ) } } schema_fixtures = { "metadefs/object": { "GET": ( {}, { "additionalProperties": False, "definitions": { "property": { "additionalProperties": { "required": [ "title", "type" ], "type": "object", "properties": { "additionalItems": { "type": "boolean" }, "enum": { "type": "array" }, "description": { "type": "string" }, "title": { "type": "string" }, "default": {}, "minLength": { "$ref": "#/definitions/positiveInteger" "Default0" }, "required": { "$ref": "#/definitions/stringArray" }, "maximum": { "type": "number" }, "minItems": { "$ref": "#/definitions/positiveInteger" "Default0" }, "readonly": { "type": "boolean" }, "minimum": { "type": "number" }, "maxItems": { "$ref": "#/definitions/positiveInteger" }, "maxLength": { "$ref": "#/definitions/positiveInteger" }, "uniqueItems": { "default": False, "type": "boolean" }, "pattern": { "type": "string", "format": "regex" }, "items": { "type": "object", "properties": { "enum": { "type": "array" }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } }, "type": "object" }, "positiveIntegerDefault0": { "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] }, "stringArray": { "uniqueItems": True, "items": { "type": "string" }, "type": "array" }, "positiveInteger": { "minimum": 0, "type": "integer" } }, "required": [ "name" ], "name": "object", "properties": { "created_at": { "type": "string", "readOnly": True, "description": "Date and time of object creation ", "format": "date-time" }, "description": { "type": "string" }, "name": { "type": "string" }, "self": { "type": "string" }, "required": { "$ref": "#/definitions/stringArray" }, "properties": { "$ref": "#/definitions/property" }, "schema": { "type": "string" }, "updated_at": { "type": "string", "readOnly": True, "description": "Date and time of the last object " "modification", "format": "date-time" }, } } ) } } class TestObjectController(testtools.TestCase): def setUp(self): super(TestObjectController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, metadefs.ObjectController) def test_list_object(self): objects = self.controller.list(NAMESPACE1) actual = [obj.name for obj in objects] self.assertEqual([OBJECT1, OBJECT2], actual) def test_get_object(self): obj = self.controller.get(NAMESPACE1, OBJECT1) self.assertEqual(OBJECT1, obj.name) self.assertEqual(sorted([PROPERTY1, PROPERTY2]), sorted(list(obj.properties.keys()))) def test_create_object(self): properties = { 'name': OBJECTNEW, 'description': 'DESCRIPTION' } obj = self.controller.create(NAMESPACE1, **properties) self.assertEqual(OBJECTNEW, obj.name) def test_create_object_invalid_property(self): properties = { 'namespace': NAMESPACE1 } self.assertRaises(TypeError, self.controller.create, **properties) def test_update_object(self): properties = { 'description': 'UPDATED_DESCRIPTION' } obj = self.controller.update(NAMESPACE1, OBJECT1, **properties) self.assertEqual(OBJECT1, obj.name) def test_update_object_invalid_property(self): properties = { 'required': 'INVALID' } self.assertRaises(TypeError, self.controller.update, NAMESPACE1, OBJECT1, **properties) def test_update_object_disallowed_fields(self): properties = { 'description': 'UPDATED_DESCRIPTION' } self.controller.update(NAMESPACE1, OBJECT1, **properties) actual = self.api.calls # API makes three calls(GET, PUT, GET) for object update. # PUT has the request body in the list '''('PUT', '/v2/metadefs/namespaces/Namespace1/objects/Object1', {}, [('description', 'UPDATED_DESCRIPTION'), ('name', 'Object1'), ('properties', ...), ('required', [])])''' _disallowed_fields = ['self', 'schema', 'created_at', 'updated_at'] for key in actual[1][3]: self.assertNotIn(key, _disallowed_fields) def test_delete_object(self): self.controller.delete(NAMESPACE1, OBJECT1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/objects/%s' % (NAMESPACE1, OBJECT1), {}, None)] self.assertEqual(expect, self.api.calls) def test_delete_all_objects(self): self.controller.delete_all(NAMESPACE1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/objects' % NAMESPACE1, {}, None)] self.assertEqual(expect, self.api.calls) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_metadefs_properties.py0000664000175000017500000002361200000000000030272 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. import testtools from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import metadefs NAMESPACE1 = 'Namespace1' PROPERTY1 = 'Property1' PROPERTY2 = 'Property2' PROPERTYNEW = 'PropertyNew' data_fixtures = { "/v2/metadefs/namespaces/%s/properties" % NAMESPACE1: { "GET": ( {}, { "properties": { PROPERTY1: { "default": "1", "type": "integer", "description": "Number of cores.", "title": "cores" }, PROPERTY2: { "items": { "enum": [ "Intel", "AMD" ], "type": "string" }, "type": "array", "description": "Specifies the CPU manufacturer.", "title": "Vendor" }, } } ), "POST": ( {}, { "items": { "enum": [ "Intel", "AMD" ], "type": "string" }, "type": "array", "description": "UPDATED_DESCRIPTION", "title": "Vendor", "name": PROPERTYNEW } ), "DELETE": ( {}, {} ) }, "/v2/metadefs/namespaces/%s/properties/%s" % (NAMESPACE1, PROPERTY1): { "GET": ( {}, { "items": { "enum": [ "Intel", "AMD" ], "type": "string" }, "type": "array", "description": "Specifies the CPU manufacturer.", "title": "Vendor" } ), "PUT": ( {}, { "items": { "enum": [ "Intel", "AMD" ], "type": "string" }, "type": "array", "description": "UPDATED_DESCRIPTION", "title": "Vendor" } ), "DELETE": ( {}, {} ) } } schema_fixtures = { "metadefs/property": { "GET": ( {}, { "additionalProperties": False, "definitions": { "positiveIntegerDefault0": { "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ] }, "stringArray": { "minItems": 1, "items": { "type": "string" }, "uniqueItems": True, "type": "array" }, "positiveInteger": { "minimum": 0, "type": "integer" } }, "required": [ "name", "title", "type" ], "name": "property", "properties": { "description": { "type": "string" }, "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, "enum": { "type": "array" }, "minimum": { "type": "number" }, "maxItems": { "$ref": "#/definitions/positiveInteger" }, "maxLength": { "$ref": "#/definitions/positiveInteger" }, "uniqueItems": { "default": False, "type": "boolean" }, "additionalItems": { "type": "boolean" }, "name": { "type": "string" }, "title": { "type": "string" }, "default": {}, "pattern": { "type": "string", "format": "regex" }, "required": { "$ref": "#/definitions/stringArray" }, "maximum": { "type": "number" }, "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, "readonly": { "type": "boolean" }, "items": { "type": "object", "properties": { "enum": { "type": "array" }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } }, "type": { "enum": [ "array", "boolean", "integer", "number", "object", "string", "null" ], "type": "string" } } } ) } } class TestPropertyController(testtools.TestCase): def setUp(self): super(TestPropertyController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, metadefs.PropertyController) def test_list_property(self): properties = self.controller.list(NAMESPACE1) actual = [prop.name for prop in properties] self.assertEqual(sorted([PROPERTY1, PROPERTY2]), sorted(actual)) def test_get_property(self): prop = self.controller.get(NAMESPACE1, PROPERTY1) self.assertEqual(PROPERTY1, prop.name) def test_create_property(self): properties = { 'name': PROPERTYNEW, 'title': 'TITLE', 'type': 'string' } obj = self.controller.create(NAMESPACE1, **properties) self.assertEqual(PROPERTYNEW, obj.name) def test_create_property_invalid_property(self): properties = { 'namespace': NAMESPACE1 } self.assertRaises(TypeError, self.controller.create, **properties) def test_update_property(self): properties = { 'description': 'UPDATED_DESCRIPTION' } prop = self.controller.update(NAMESPACE1, PROPERTY1, **properties) self.assertEqual(PROPERTY1, prop.name) def test_update_property_invalid_property(self): properties = { 'type': 'INVALID' } self.assertRaises(TypeError, self.controller.update, NAMESPACE1, PROPERTY1, **properties) def test_update_property_disallowed_fields(self): properties = { 'description': 'UPDATED_DESCRIPTION' } self.controller.update(NAMESPACE1, PROPERTY1, **properties) actual = self.api.calls _disallowed_fields = ['created_at', 'updated_at'] for key in actual[1][3]: self.assertNotIn(key, _disallowed_fields) def test_delete_property(self): self.controller.delete(NAMESPACE1, PROPERTY1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/properties/%s' % (NAMESPACE1, PROPERTY1), {}, None)] self.assertEqual(expect, self.api.calls) def test_delete_all_properties(self): self.controller.delete_all(NAMESPACE1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/properties' % NAMESPACE1, {}, None)] self.assertEqual(expect, self.api.calls) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_metadefs_resource_types.py0000664000175000017500000001613400000000000031152 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. import testtools from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import metadefs NAMESPACE1 = 'Namespace1' RESOURCE_TYPE1 = 'ResourceType1' RESOURCE_TYPE2 = 'ResourceType2' RESOURCE_TYPE3 = 'ResourceType3' RESOURCE_TYPE4 = 'ResourceType4' RESOURCE_TYPENEW = 'ResourceTypeNew' data_fixtures = { "/v2/metadefs/namespaces/%s/resource_types" % NAMESPACE1: { "GET": ( {}, { "resource_type_associations": [ { "name": RESOURCE_TYPE3, "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", }, { "name": RESOURCE_TYPE4, "prefix": "PREFIX:", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ] } ), "POST": ( {}, { "name": RESOURCE_TYPENEW, "prefix": "PREFIX:", "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ), }, "/v2/metadefs/namespaces/%s/resource_types/%s" % (NAMESPACE1, RESOURCE_TYPE1): { "DELETE": ( {}, {} ), }, "/v2/metadefs/resource_types": { "GET": ( {}, { "resource_types": [ { "name": RESOURCE_TYPE1, "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", }, { "name": RESOURCE_TYPE2, "created_at": "2014-08-14T09:07:06Z", "updated_at": "2014-08-14T09:07:06Z", } ] } ) } } schema_fixtures = { "metadefs/resource_type": { "GET": ( {}, { "name": "resource_type", "properties": { "prefix": { "type": "string", "description": "Specifies the prefix to use for the " "given resource type. Any properties " "in the namespace should be prefixed " "with this prefix when being applied " "to the specified resource type. Must " "include prefix separator (e.g. a " "colon :).", "maxLength": 80 }, "properties_target": { "type": "string", "description": "Some resource types allow more than " "one key / value pair per instance. " "For example, Cinder allows user and " "image metadata on volumes. Only the " "image properties metadata is " "evaluated by Nova (scheduling or " "drivers). This property allows a " "namespace target to remove the " "ambiguity.", "maxLength": 80 }, "name": { "type": "string", "description": "Resource type names should be " "aligned with Heat resource types " "whenever possible: http://docs." "openstack.org/developer/heat/" "template_guide/openstack.html", "maxLength": 80 }, "created_at": { "type": "string", "readOnly": True, "description": "Date and time of resource type " "association", "format": "date-time" }, "updated_at": { "type": "string", "readOnly": True, "description": "Date and time of the last resource " "type association modification ", "format": "date-time" }, } } ) } } class TestResoureTypeController(testtools.TestCase): def setUp(self): super(TestResoureTypeController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseResourceTypeController( self.api, self.schema_api, metadefs.ResourceTypeController) def test_list_resource_types(self): resource_types = self.controller.list() names = [rt.name for rt in resource_types] self.assertEqual([RESOURCE_TYPE1, RESOURCE_TYPE2], names) def test_get_resource_types(self): resource_types = self.controller.get(NAMESPACE1) self.assertEqual([RESOURCE_TYPE3, RESOURCE_TYPE4], resource_types) def test_associate_resource_types(self): resource_types = self.controller.associate(NAMESPACE1, name=RESOURCE_TYPENEW) self.assertEqual(RESOURCE_TYPENEW, resource_types['name']) def test_associate_resource_types_invalid_property(self): longer = '1234' * 50 properties = {'name': RESOURCE_TYPENEW, 'prefix': longer} self.assertRaises(TypeError, self.controller.associate, NAMESPACE1, **properties) def test_deassociate_resource_types(self): self.controller.deassociate(NAMESPACE1, RESOURCE_TYPE1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/resource_types/%s' % (NAMESPACE1, RESOURCE_TYPE1), {}, None)] self.assertEqual(expect, self.api.calls) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_metadefs_tags.py0000664000175000017500000001173300000000000027035 0ustar00zuulzuul00000000000000# Copyright 2015 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 testtools from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import metadefs NAMESPACE1 = 'Namespace1' TAG1 = 'Tag1' TAG2 = 'Tag2' TAGNEW1 = 'TagNew1' TAGNEW2 = 'TagNew2' TAGNEW3 = 'TagNew3' def _get_tag_fixture(tag_name, **kwargs): tag = { "name": tag_name } tag.update(kwargs) return tag data_fixtures = { "/v2/metadefs/namespaces/%s/tags" % NAMESPACE1: { "GET": ( {}, { "tags": [ _get_tag_fixture(TAG1), _get_tag_fixture(TAG2) ] } ), "POST": ( {}, { 'tags': [ _get_tag_fixture(TAGNEW2), _get_tag_fixture(TAGNEW3) ] } ), "DELETE": ( {}, {} ) }, "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW1): { "POST": ( {}, _get_tag_fixture(TAGNEW1) ) }, "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAG1): { "GET": ( {}, _get_tag_fixture(TAG1) ), "PUT": ( {}, _get_tag_fixture(TAG2) ), "DELETE": ( {}, {} ) }, "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAG2): { "GET": ( {}, _get_tag_fixture(TAG2) ), }, "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW2): { "GET": ( {}, _get_tag_fixture(TAGNEW2) ), }, "/v2/metadefs/namespaces/%s/tags/%s" % (NAMESPACE1, TAGNEW3): { "GET": ( {}, _get_tag_fixture(TAGNEW3) ), } } schema_fixtures = { "metadefs/tag": { "GET": ( {}, { "additionalProperties": True, "name": { "type": "string" }, "created_at": { "type": "string", "readOnly": True, "description": ("Date and time of tag creation"), "format": "date-time" }, "updated_at": { "type": "string", "readOnly": True, "description": ("Date and time of the last tag" " modification"), "format": "date-time" }, 'properties': {} } ) } } class TestTagController(testtools.TestCase): def setUp(self): super(TestTagController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, metadefs.TagController) def test_list_tag(self): tags = self.controller.list(NAMESPACE1) actual = [tag.name for tag in tags] self.assertEqual([TAG1, TAG2], actual) def test_get_tag(self): tag = self.controller.get(NAMESPACE1, TAG1) self.assertEqual(TAG1, tag.name) def test_create_tag(self): tag = self.controller.create(NAMESPACE1, TAGNEW1) self.assertEqual(TAGNEW1, tag.name) def test_create_multiple_tags(self): properties = { 'tags': [TAGNEW2, TAGNEW3] } tags = self.controller.create_multiple(NAMESPACE1, **properties) self.assertEqual([TAGNEW2, TAGNEW3], tags) def test_update_tag(self): properties = { 'name': TAG2 } tag = self.controller.update(NAMESPACE1, TAG1, **properties) self.assertEqual(TAG2, tag.name) def test_delete_tag(self): self.controller.delete(NAMESPACE1, TAG1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/tags/%s' % (NAMESPACE1, TAG1), {}, None)] self.assertEqual(expect, self.api.calls) def test_delete_all_tags(self): self.controller.delete_all(NAMESPACE1) expect = [ ('DELETE', '/v2/metadefs/namespaces/%s/tags' % NAMESPACE1, {}, None)] self.assertEqual(expect, self.api.calls) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_schemas.py0000664000175000017500000001613100000000000025647 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. import jsonpatch import testtools import warlock from glanceclient.tests import utils from glanceclient.v2 import schemas fixtures = { '/v2/schemas': { 'GET': ( {}, { 'image': '/v2/schemas/image', 'access': '/v2/schemas/image/access', }, ), }, '/v2/schemas/image': { 'GET': ( {}, { 'name': 'image', 'properties': { 'name': {'type': 'string', 'description': 'Name of image'}, 'tags': {'type': 'array'} }, }, ), }, } _SCHEMA = schemas.Schema({ 'name': 'image', 'properties': { 'name': {'type': 'string'}, 'color': {'type': 'string'}, 'shape': {'type': 'string', 'is_base': False}, 'tags': {'type': 'array'} }, }) def compare_json_patches(a, b): """Return 0 if a and b describe the same JSON patch.""" return(jsonpatch.JsonPatch.from_string(a) == jsonpatch.JsonPatch.from_string(b)) class TestSchemaProperty(testtools.TestCase): def test_property_minimum(self): prop = schemas.SchemaProperty('size') self.assertEqual('size', prop.name) def test_property_description(self): prop = schemas.SchemaProperty('size', description='some quantity') self.assertEqual('size', prop.name) self.assertEqual('some quantity', prop.description) def test_property_is_base(self): prop1 = schemas.SchemaProperty('name') prop2 = schemas.SchemaProperty('foo', is_base=False) prop3 = schemas.SchemaProperty('foo', is_base=True) self.assertTrue(prop1.is_base) self.assertFalse(prop2.is_base) self.assertTrue(prop3.is_base) class TestSchema(testtools.TestCase): def test_schema_minimum(self): raw_schema = {'name': 'Country', 'properties': {}} schema = schemas.Schema(raw_schema) self.assertEqual('Country', schema.name) self.assertEqual([], schema.properties) def test_schema_with_property(self): raw_schema = {'name': 'Country', 'properties': {'size': {}}} schema = schemas.Schema(raw_schema) self.assertEqual('Country', schema.name) self.assertEqual(['size'], [p.name for p in schema.properties]) def test_raw(self): raw_schema = {'name': 'Country', 'properties': {}} schema = schemas.Schema(raw_schema) self.assertEqual(raw_schema, schema.raw()) def test_property_is_base(self): raw_schema = {'name': 'Country', 'properties': { 'size': {}, 'population': {'is_base': False}}} schema = schemas.Schema(raw_schema) self.assertTrue(schema.is_base_property('size')) self.assertFalse(schema.is_base_property('population')) self.assertFalse(schema.is_base_property('foo')) class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(fixtures) self.controller = schemas.Controller(self.api) def test_get_schema(self): schema = self.controller.get('image') self.assertEqual('image', schema.name) self.assertEqual(set(['name', 'tags']), set([p.name for p in schema.properties])) class TestSchemaBasedModel(testtools.TestCase): def setUp(self): super(TestSchemaBasedModel, self).setUp() self.model = warlock.model_factory(_SCHEMA.raw(), base_class=schemas.SchemaBasedModel) def test_patch_should_replace_missing_core_properties(self): obj = { 'name': 'fred' } original = self.model(obj) original['color'] = 'red' patch = original.patch expected = '[{"path": "/color", "value": "red", "op": "replace"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_add_extra_properties(self): obj = { 'name': 'fred', } original = self.model(obj) original['weight'] = '10' patch = original.patch expected = '[{"path": "/weight", "value": "10", "op": "add"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_replace_extra_properties(self): obj = { 'name': 'fred', 'weight': '10' } original = self.model(obj) original['weight'] = '22' patch = original.patch expected = '[{"path": "/weight", "value": "22", "op": "replace"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_remove_extra_properties(self): obj = { 'name': 'fred', 'weight': '10' } original = self.model(obj) del original['weight'] patch = original.patch expected = '[{"path": "/weight", "op": "remove"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_remove_core_properties(self): obj = { 'name': 'fred', 'color': 'red' } original = self.model(obj) del original['color'] patch = original.patch expected = '[{"path": "/color", "op": "remove"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_add_missing_custom_properties(self): obj = { 'name': 'fred' } original = self.model(obj) original['shape'] = 'circle' patch = original.patch expected = '[{"path": "/shape", "value": "circle", "op": "add"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_replace_custom_properties(self): obj = { 'name': 'fred', 'shape': 'circle' } original = self.model(obj) original['shape'] = 'square' patch = original.patch expected = '[{"path": "/shape", "value": "square", "op": "replace"}]' self.assertTrue(compare_json_patches(patch, expected)) def test_patch_should_replace_tags(self): obj = {'name': 'fred', } original = self.model(obj) original['tags'] = ['tag1', 'tag2'] patch = original.patch expected = '[{"path": "/tags", "value": ["tag1", "tag2"], ' \ '"op": "replace"}]' self.assertTrue(compare_json_patches(patch, expected)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_shell_v2.py0000664000175000017500000047025000000000000025750 0ustar00zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # Copyright (C) 2013 Yahoo! Inc. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import argparse from copy import deepcopy import io import json import os from unittest import mock import sys import tempfile import testtools from glanceclient.common import utils from glanceclient import exc from glanceclient import shell # NOTE(geguileo): This is very nasty, but I can't find a better way to set # command line arguments in glanceclient.v2.shell.do_image_create that are # set by decorator utils.schema_args while preserving the spirits of the test # Backup original decorator original_schema_args = utils.schema_args # Set our own decorator that calls the original but with simulated schema def schema_args(schema_getter, omit=None): global original_schema_args # We only add the 2 arguments that are required by image-create my_schema_getter = lambda: { 'properties': { 'container_format': { 'enum': [None, 'ami', 'ari', 'aki', 'bare', 'ovf', 'ova', 'docker'], 'type': 'string', 'description': 'Format of the container'}, 'disk_format': { 'enum': [None, 'ami', 'ari', 'aki', 'vhd', 'vhdx', 'vmdk', 'raw', 'qcow2', 'vdi', 'iso', 'ploop'], 'type': 'string', 'description': 'Format of the disk'}, 'location': {'type': 'string'}, 'locations': {'type': 'string'}, 'copy_from': {'type': 'string'}}} return original_schema_args(my_schema_getter, omit) utils.schema_args = schema_args from glanceclient.v2 import shell as test_shell # noqa # Return original decorator. utils.schema_args = original_schema_args class ShellV2Test(testtools.TestCase): def setUp(self): super(ShellV2Test, self).setUp() self._mock_utils() self.gc = self._mock_glance_client() self.shell = shell.OpenStackImagesShell() os.environ = { 'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', 'OS_TOKEN_ID': 'test', 'OS_AUTH_URL': 'http://127.0.0.1:5000/v2.0/', 'OS_AUTH_TOKEN': 'pass', 'OS_IMAGE_API_VERSION': '1', 'OS_REGION_NAME': 'test', 'OS_IMAGE_URL': 'http://is.invalid'} self.shell = shell.OpenStackImagesShell() self.patched = mock.patch('glanceclient.common.utils.get_data_file', autospec=True, return_value=None) self.mock_get_data_file = self.patched.start() def tearDown(self): super(ShellV2Test, self).tearDown() self.patched.stop() def _make_args(self, args): # NOTE(venkatesh): this conversion from a dict to an object # is required because the test_shell.do_xxx(gc, args) methods # expects the args to be attributes of an object. If passed as # dict directly, it throws an AttributeError. class Args(object): def __init__(self, entries): self.store = None self.__dict__.update(entries) return Args(args) def _mock_glance_client(self): my_mocked_gc = mock.Mock() my_mocked_gc.schemas.return_value = 'test' my_mocked_gc.get.return_value = {} return my_mocked_gc def _mock_utils(self): utils.print_list = mock.Mock() utils.print_dict = mock.Mock() utils.save_image = mock.Mock() utils.print_dict_list = mock.Mock() utils.print_cached_images = mock.Mock() def assert_exits_with_msg(self, func, func_args, err_msg=None): with mock.patch.object(utils, 'exit') as mocked_utils_exit: mocked_utils_exit.return_value = '%s' % err_msg func(self.gc, func_args) if err_msg: mocked_utils_exit.assert_called_once_with(err_msg) else: mocked_utils_exit.assert_called_once_with() def _run_command(self, cmd): self.shell.main(cmd.split()) stores_info_response = { "stores": [ { "default": "true", "id": "ceph1", "description": "RBD backend for glance." }, { "id": "file2", "description": "Filesystem backend for glance." }, { "id": "file1", "description": "Filesystem backend for gkance." }, { "id": "ceph2", "description": "RBD backend for glance." } ] } stores_info_detail_response = { "stores": [ { "default": "true", "id": "ceph1", "type": "rbd", "description": "RBD backend for glance.", "properties": { "pool": "pool1", "chunk_size": "4" } }, { "id": "file2", "type": "file", "description": "Filesystem backend for glance.", "properties": {} }, { "id": "file1", "type": "file", "description": "Filesystem backend for gkance.", "properties": {} }, { "id": "ceph2", "type": "rbd", "description": "RBD backend for glance.", "properties": { "pool": "pool2", "chunk_size": "4" } } ] } def test_do_stores_info(self): args = self._make_args({'detail': False}) with mock.patch.object(self.gc.images, 'get_stores_info') as mocked_list: mocked_list.return_value = self.stores_info_response test_shell.do_stores_info(self.gc, args) mocked_list.assert_called_once_with() utils.print_dict.assert_called_once_with(self.stores_info_response) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_stores_info( self, mock_stdin, mock_utils_exit): expected_msg = ('Multi Backend support is not enabled') args = self._make_args({'detail': False}) mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_stores_info') as mocked_info: mocked_info.side_effect = exc.HTTPNotFound try: test_shell.do_stores_info(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) def test_do_stores_info_with_detail(self): args = self._make_args({'detail': True}) with mock.patch.object(self.gc.images, 'get_stores_info_detail') as mocked_list: mocked_list.return_value = self.stores_info_detail_response test_shell.do_stores_info(self.gc, args) mocked_list.assert_called_once_with() utils.print_dict.assert_called_once_with( self.stores_info_detail_response) @mock.patch('sys.stderr') def test_image_create_missing_disk_format(self, __): e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 image-create ' + '--file fake_src --container-format bare') self.assertEqual('error: Must provide --disk-format when using ' '--file.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_container_format(self, __): e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 image-create ' + '--file fake_src --disk-format qcow2') self.assertEqual('error: Must provide --container-format when ' 'using --file.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_container_format_stdin_data(self, __): # Fake that get_data_file method returns data self.mock_get_data_file.return_value = io.StringIO() e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 image-create' ' --disk-format qcow2') self.assertEqual('error: Must provide --container-format when ' 'using stdin.', e.message) @mock.patch('sys.stderr') def test_image_create_missing_disk_format_stdin_data(self, __): # Fake that get_data_file method returns data self.mock_get_data_file.return_value = io.StringIO() e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 image-create' ' --container-format bare') self.assertEqual('error: Must provide --disk-format when using stdin.', e.message) @mock.patch('sys.stderr') def test_create_via_import_glance_direct_missing_disk_format(self, __): e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 ' 'image-create-via-import ' '--file fake_src --container-format bare') self.assertEqual('error: Must provide --disk-format when using ' '--file.', e.message) @mock.patch('sys.stderr') def test_create_via_import_glance_direct_missing_container_format( self, __): e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 ' 'image-create-via-import ' '--file fake_src --disk-format qcow2') self.assertEqual('error: Must provide --container-format when ' 'using --file.', e.message) @mock.patch('sys.stderr') def test_create_via_import_web_download_missing_disk_format(self, __): e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 ' 'image-create-via-import ' + '--import-method web-download ' + '--uri fake_uri --container-format bare') self.assertEqual('error: Must provide --disk-format when using ' '--uri.', e.message) @mock.patch('sys.stderr') def test_create_via_import_web_download_missing_container_format( self, __): e = self.assertRaises(exc.CommandError, self._run_command, '--os-image-api-version 2 ' 'image-create-via-import ' '--import-method web-download ' '--uri fake_uri --disk-format qcow2') self.assertEqual('error: Must provide --container-format when ' 'using --uri.', e.message) def test_do_image_list(self): input = { 'limit': None, 'page_size': 18, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort_key': ['name', 'id'], 'sort_dir': ['desc', 'asc'], 'sort': None, 'verbose': False, 'include_stores': False, 'os_hash_value': None, 'os_hidden': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) exp_img_filters = { 'owner': 'test', 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', 'tag': 'fake tag', 'os_hidden': False } mocked_list.assert_called_once_with(page_size=18, sort_key=['name', 'id'], sort_dir=['desc', 'asc'], filters=exp_img_filters) utils.print_list.assert_called_once_with({}, ['ID', 'Name']) def test_do_image_list_verbose(self): input = { 'limit': None, 'page_size': 18, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort_key': ['name', 'id'], 'sort_dir': ['desc', 'asc'], 'sort': None, 'verbose': True, 'include_stores': False, 'os_hash_value': None, 'os_hidden': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) utils.print_list.assert_called_once_with( {}, ['ID', 'Name', 'Disk_format', 'Container_format', 'Size', 'Status', 'Owner']) def test_do_image_list_verbose_cmd(self): self._run_command('--os-image-api-version 2 --verbose image-list') utils.print_list.assert_called_once_with( mock.ANY, ['ID', 'Name', 'Disk_format', 'Container_format', 'Size', 'Status', 'Owner']) def test_do_image_list_with_include_stores_true(self): input = { 'limit': None, 'page_size': 18, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort_key': ['name', 'id'], 'sort_dir': ['desc', 'asc'], 'sort': None, 'verbose': False, 'include_stores': True, 'os_hash_value': None, 'os_hidden': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) utils.print_list.assert_called_once_with( {}, ['ID', 'Name', 'Stores']) def test_do_image_list_verbose_with_include_stores_true(self): input = { 'limit': None, 'page_size': 18, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort_key': ['name', 'id'], 'sort_dir': ['desc', 'asc'], 'sort': None, 'verbose': True, 'include_stores': True, 'os_hash_value': None, 'os_hidden': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) utils.print_list.assert_called_once_with( {}, ['ID', 'Name', 'Disk_format', 'Container_format', 'Size', 'Status', 'Owner', 'Stores']) def test_do_image_list_with_hidden_true(self): input = { 'limit': None, 'page_size': 18, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort_key': ['name', 'id'], 'sort_dir': ['desc', 'asc'], 'sort': None, 'verbose': False, 'include_stores': False, 'os_hash_value': None, 'os_hidden': True } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) exp_img_filters = { 'owner': 'test', 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', 'tag': 'fake tag', 'os_hidden': True } mocked_list.assert_called_once_with(page_size=18, sort_key=['name', 'id'], sort_dir=['desc', 'asc'], filters=exp_img_filters) utils.print_list.assert_called_once_with({}, ['ID', 'Name']) def test_do_image_list_with_single_sort_key(self): input = { 'limit': None, 'page_size': 18, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort_key': ['name'], 'sort_dir': ['desc'], 'sort': None, 'verbose': False, 'include_stores': False, 'os_hash_value': None, 'os_hidden': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) exp_img_filters = { 'owner': 'test', 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', 'tag': 'fake tag', 'os_hidden': False } mocked_list.assert_called_once_with(page_size=18, sort_key=['name'], sort_dir=['desc'], filters=exp_img_filters) utils.print_list.assert_called_once_with({}, ['ID', 'Name']) def test_do_image_list_new_sorting_syntax(self): input = { 'limit': None, 'page_size': 18, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': [], 'sort': 'name:desc,size:asc', 'sort_key': [], 'sort_dir': [], 'verbose': False, 'include_stores': False, 'os_hash_value': None, 'os_hidden': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) exp_img_filters = { 'owner': 'test', 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', 'tag': 'fake tag', 'os_hidden': False } mocked_list.assert_called_once_with( page_size=18, sort='name:desc,size:asc', filters=exp_img_filters) utils.print_list.assert_called_once_with({}, ['ID', 'Name']) def test_do_image_list_with_property_filter(self): input = { 'limit': None, 'page_size': 1, 'visibility': True, 'member_status': 'Fake', 'owner': 'test', 'checksum': 'fake_checksum', 'tag': 'fake tag', 'properties': ['os_distro=NixOS', 'architecture=x86_64'], 'sort_key': ['name'], 'sort_dir': ['desc'], 'sort': None, 'verbose': False, 'include_stores': False, 'os_hash_value': None, 'os_hidden': False } args = self._make_args(input) with mock.patch.object(self.gc.images, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_image_list(self.gc, args) exp_img_filters = { 'owner': 'test', 'member_status': 'Fake', 'visibility': True, 'checksum': 'fake_checksum', 'tag': 'fake tag', 'os_distro': 'NixOS', 'architecture': 'x86_64', 'os_hidden': False } mocked_list.assert_called_once_with(page_size=1, sort_key=['name'], sort_dir=['desc'], filters=exp_img_filters) utils.print_list.assert_called_once_with({}, ['ID', 'Name']) def test_do_image_show_human_readable(self): args = self._make_args({'id': 'pass', 'page_size': 18, 'human_readable': True, 'max_column_width': 120}) with mock.patch.object(self.gc.images, 'get') as mocked_list: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['size'] = 1024 mocked_list.return_value = expect_image test_shell.do_image_show(self.gc, args) mocked_list.assert_called_once_with('pass') utils.print_dict.assert_called_once_with({'id': 'pass', 'size': '1kB'}, max_column_width=120) def test_do_image_show(self): args = self._make_args({'id': 'pass', 'page_size': 18, 'human_readable': False, 'max_column_width': 120}) with mock.patch.object(self.gc.images, 'get') as mocked_list: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['size'] = 1024 mocked_list.return_value = expect_image test_shell.do_image_show(self.gc, args) mocked_list.assert_called_once_with('pass') utils.print_dict.assert_called_once_with({'id': 'pass', 'size': 1024}, max_column_width=120) def _test_do_image_tasks(self, verbose=False, supported=True): args = self._make_args({'id': 'pass', 'verbose': verbose}) expected_columns = ["Message", "Status", "Updated at"] expected_output = { "tasks": [ { "image_id": "pass", "id": "task_1", "user_id": "user_1", "request_id": "request_id_1", "message": "fake_message", "status": "status", } ] } if verbose: columns_to_prepend = ['Image Id', 'Task Id'] columns_to_extend = ['User Id', 'Request Id', 'Result', 'Owner', 'Input', 'Expires at'] expected_columns = (columns_to_prepend + expected_columns + columns_to_extend) expected_output["tasks"][0]["Result"] = "Fake Result" expected_output["tasks"][0]["Owner"] = "Fake Owner" expected_output["tasks"][0]["Input"] = "Fake Input" expected_output["tasks"][0]["Expires at"] = "Fake Expiry" with mock.patch.object(self.gc.images, 'get_associated_image_tasks') as mocked_tasks: if supported: mocked_tasks.return_value = expected_output else: mocked_tasks.side_effect = exc.HTTPNotImplemented test_shell.do_image_tasks(self.gc, args) mocked_tasks.assert_called_once_with('pass') if supported: utils.print_dict_list.assert_called_once_with( expected_output['tasks'], expected_columns) def test_do_image_tasks_without_verbose(self): self._test_do_image_tasks() def test_do_image_tasks_with_verbose(self): self._test_do_image_tasks(verbose=True) def test_do_image_tasks_unsupported(self): with mock.patch('glanceclient.common.utils.exit') as mock_exit: self._test_do_image_tasks(supported=False) mock_exit.assert_called_once_with( 'Server does not support image tasks API (v2.12)') def test_usage(self): with mock.patch.object(self.gc.info, 'get_usage') as mock_usage: mock_usage.return_value = {'quota1': {'limit': 10, 'usage': 0}, 'quota2': {'limit': 20, 'usage': 5}} test_shell.do_usage(self.gc, []) utils.print_dict_list.assert_called_once_with( [{'quota': 'quota1', 'limit': 10, 'usage': 0}, {'quota': 'quota2', 'limit': 20, 'usage': 5}], ['Quota', 'Limit', 'Usage']) @mock.patch('sys.stdin', autospec=True) def test_do_image_create_no_user_props(self, mock_stdin): args = self._make_args({'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'file': None}) with mock.patch.object(self.gc.images, 'create') as mocked_create: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' mocked_create.return_value = expect_image # Ensure that the test stdin is not considered # to be supplying image data mock_stdin.isatty = lambda: True test_shell.do_image_create(self.gc, args) mocked_create.assert_called_once_with(name='IMG-01', disk_format='vhd', container_format='bare') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare'}) @mock.patch('sys.stdin', autospec=True) def test_do_image_create_for_none_multi_hash(self, mock_stdin): args = self._make_args({'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'file': None}) with mock.patch.object(self.gc.images, 'create') as mocked_create: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' expect_image['os_hash_algo'] = None expect_image['os_hash_value'] = None mocked_create.return_value = expect_image # Ensure that the test stdin is not considered # to be supplying image data mock_stdin.isatty = lambda: True test_shell.do_image_create(self.gc, args) mocked_create.assert_called_once_with(name='IMG-01', disk_format='vhd', container_format='bare') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'os_hash_algo': None, 'os_hash_value': None}) def test_do_image_create_with_multihash(self): self.mock_get_data_file.return_value = io.StringIO() try: with open(tempfile.mktemp(), 'w+') as f: f.write('Some data here') f.flush() f.seek(0) file_name = f.name temp_args = {'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'file': file_name, 'progress': False} args = self._make_args(temp_args) with mock.patch.object(self.gc.images, 'create') as mocked_create: with mock.patch.object(self.gc.images, 'get') as mocked_get: ignore_fields = ['self', 'access', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' expect_image['checksum'] = 'fake-checksum' expect_image['os_hash_algo'] = 'fake-hash_algo' expect_image['os_hash_value'] = 'fake-hash_value' mocked_create.return_value = expect_image mocked_get.return_value = expect_image test_shell.do_image_create(self.gc, args) temp_args.pop('file', None) mocked_create.assert_called_once_with(**temp_args) mocked_get.assert_called_once_with('pass') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'checksum': 'fake-checksum', 'os_hash_algo': 'fake-hash_algo', 'os_hash_value': 'fake-hash_value'}) finally: try: os.remove(f.name) except Exception: pass @mock.patch('sys.stdin', autospec=True) def test_do_image_create_hidden_image(self, mock_stdin): args = self._make_args({'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'file': None, 'os_hidden': True}) with mock.patch.object(self.gc.images, 'create') as mocked_create: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' expect_image['os_hidden'] = True mocked_create.return_value = expect_image # Ensure that the test stdin is not considered # to be supplying image data mock_stdin.isatty = lambda: True test_shell.do_image_create(self.gc, args) mocked_create.assert_called_once_with(name='IMG-01', disk_format='vhd', container_format='bare', os_hidden=True) utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'os_hidden': True}) def test_do_image_create_with_file(self): self.mock_get_data_file.return_value = io.StringIO() try: file_name = None with open(tempfile.mktemp(), 'w+') as f: f.write('Some data here') f.flush() f.seek(0) file_name = f.name temp_args = {'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'file': file_name, 'progress': False} args = self._make_args(temp_args) with mock.patch.object(self.gc.images, 'create') as mocked_create: with mock.patch.object(self.gc.images, 'get') as mocked_get: ignore_fields = ['self', 'access', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' mocked_create.return_value = expect_image mocked_get.return_value = expect_image test_shell.do_image_create(self.gc, args) temp_args.pop('file', None) mocked_create.assert_called_once_with(**temp_args) mocked_get.assert_called_once_with('pass') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare'}) finally: try: os.remove(f.name) except Exception: pass @mock.patch('sys.stdin', autospec=True) def test_do_image_create_with_unicode(self, mock_stdin): name = '\u041f\u0420\u0418\u0412\u0415\u0422\u0418\u041a' args = self._make_args({'name': name, 'file': None}) with mock.patch.object(self.gc.images, 'create') as mocked_create: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict((field, field) for field in ignore_fields) expect_image['id'] = 'pass' expect_image['name'] = name mocked_create.return_value = expect_image mock_stdin.isatty = lambda: True test_shell.do_image_create(self.gc, args) mocked_create.assert_called_once_with(name=name) utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': name}) @mock.patch('sys.stdin', autospec=True) def test_do_image_create_with_user_props(self, mock_stdin): args = self._make_args({'name': 'IMG-01', 'property': ['myprop=myval'], 'file': None, 'container_format': 'bare', 'disk_format': 'qcow2'}) with mock.patch.object(self.gc.images, 'create') as mocked_create: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['myprop'] = 'myval' mocked_create.return_value = expect_image # Ensure that the test stdin is not considered # to be supplying image data mock_stdin.isatty = lambda: True test_shell.do_image_create(self.gc, args) mocked_create.assert_called_once_with(name='IMG-01', myprop='myval', container_format='bare', disk_format='qcow2') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'myprop': 'myval'}) @mock.patch('glanceclient.common.utils.exit') @mock.patch('os.access') @mock.patch('sys.stdin', autospec=True) def test_neg_do_image_create_no_file_and_stdin_with_store( self, mock_stdin, mock_access, mock_utils_exit): expected_msg = ('--store option should only be provided with --file ' 'option or stdin.') mock_utils_exit.side_effect = self._mock_utils_exit mock_stdin.isatty = lambda: True mock_access.return_value = False args = self._make_args({'name': 'IMG-01', 'property': ['myprop=myval'], 'file': None, 'store': 'file1', 'container_format': 'bare', 'disk_format': 'qcow2'}) try: test_shell.do_image_create(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_do_image_create_invalid_store( self, mock_utils_exit): expected_msg = ("Store 'dummy' is not valid for this cloud. " "Valid values can be retrieved with stores-info " "command.") mock_utils_exit.side_effect = self._mock_utils_exit args = self._make_args({'name': 'IMG-01', 'property': ['myprop=myval'], 'file': "somefile.txt", 'store': 'dummy', 'container_format': 'bare', 'disk_format': 'qcow2'}) with mock.patch.object(self.gc.images, 'get_stores_info') as mock_stores_info: mock_stores_info.return_value = self.stores_info_response try: test_shell.do_image_create(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) # NOTE(rosmaita): have to explicitly set to None the declared but unused # arguments (the configparser does that for us normally) base_args = {'name': 'Mortimer', 'disk_format': 'raw', 'container_format': 'bare', 'progress': False, 'file': None, 'uri': None, 'remote_region': None, 'import_method': None} import_info_response = {'import-methods': { 'type': 'array', 'description': 'Import methods available.', 'value': ['glance-direct', 'web-download', 'copy-image', 'glance-download']}} def _mock_utils_exit(self, msg): sys.exit(msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('os.access') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_no_method_with_file_and_stdin( self, mock_stdin, mock_access, mock_utils_exit): expected_msg = ('You cannot use both --file and stdin with the ' 'glance-direct import method.') my_args = self.base_args.copy() my_args['file'] = 'some.file' args = self._make_args(my_args) mock_stdin.isatty = lambda: False mock_access.return_value = True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_create_via_import_copy_image( self, mock_utils_exit): expected_msg = ("Import method 'copy-image' cannot be used " "while creating the image.") mock_utils_exit.side_effect = self._mock_utils_exit my_args = self.base_args.copy() my_args.update( {'id': 'IMG-01', 'import_method': 'copy-image'}) args = self._make_args(my_args) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_create_via_import_stores_all_stores_specified( self, mock_utils_exit): expected_msg = ('Only one of --store, --stores and --all-stores can ' 'be provided') mock_utils_exit.side_effect = self._mock_utils_exit my_args = self.base_args.copy() my_args.update( {'id': 'IMG-01', 'import_method': 'glance-direct', 'stores': 'file1,file2', 'os_all_stores': True, 'file': 'some.mufile', 'disk_format': 'raw', 'container_format': 'bare', }) args = self._make_args(my_args) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_stores_without_file( self, mock_stdin, mock_utils_exit): expected_msg = ('--stores option should only be provided with --file ' 'option or stdin for the glance-direct import method.') mock_utils_exit.side_effect = self._mock_utils_exit mock_stdin.isatty = lambda: True my_args = self.base_args.copy() my_args.update( {'id': 'IMG-01', 'import_method': 'glance-direct', 'stores': 'file1,file2', 'disk_format': 'raw', 'container_format': 'bare', }) args = self._make_args(my_args) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: with mock.patch.object(self.gc.images, 'get_stores_info') as mocked_stores_info: mocked_stores_info.return_value = self.stores_info_response mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_all_stores_without_file( self, mock_stdin, mock_utils_exit): expected_msg = ('--all-stores option should only be provided with ' '--file option or stdin for the glance-direct import ' 'method.') mock_utils_exit.side_effect = self._mock_utils_exit mock_stdin.isatty = lambda: True my_args = self.base_args.copy() my_args.update( {'id': 'IMG-01', 'import_method': 'glance-direct', 'os_all_stores': True, 'disk_format': 'raw', 'container_format': 'bare', }) args = self._make_args(my_args) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('os.access') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_no_file_and_stdin_with_store( self, mock_stdin, mock_access, mock_utils_exit): expected_msg = ('--store option should only be provided with --file ' 'option or stdin for the glance-direct import method.') my_args = self.base_args.copy() my_args['import_method'] = 'glance-direct' my_args['store'] = 'file1' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_access.return_value = False mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: with mock.patch.object(self.gc.images, 'get_stores_info') as mocked_stores_info: mocked_stores_info.return_value = self.stores_info_response mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_no_uri_with_store( self, mock_stdin, mock_utils_exit): expected_msg = ('--store option should only be provided with --uri ' 'option for the web-download import method.') my_args = self.base_args.copy() my_args['import_method'] = 'web-download' my_args['store'] = 'file1' args = self._make_args(my_args) mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: with mock.patch.object(self.gc.images, 'get_stores_info') as mocked_stores_info: mocked_stores_info.return_value = self.stores_info_response mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('os.access') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_invalid_store( self, mock_stdin, mock_access, mock_utils_exit): expected_msg = ("Store 'dummy' is not valid for this cloud. " "Valid values can be retrieved with stores-info" " command.") my_args = self.base_args.copy() my_args['import_method'] = 'glance-direct' my_args['store'] = 'dummy' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_access.return_value = False mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: with mock.patch.object(self.gc.images, 'get_stores_info') as mocked_stores_info: mocked_stores_info.return_value = self.stores_info_response mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_no_method_passing_uri( self, mock_stdin, mock_utils_exit): expected_msg = ('You cannot use --uri without specifying an import ' 'method.') my_args = self.base_args.copy() my_args['uri'] = 'http://example.com/whatever' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_glance_direct_no_data( self, mock_stdin, mock_utils_exit): expected_msg = ('You must specify a --file or provide data via stdin ' 'for the glance-direct import method.') my_args = self.base_args.copy() my_args['import_method'] = 'glance-direct' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_glance_direct_with_uri( self, mock_stdin, mock_utils_exit): expected_msg = ('You cannot specify a --uri with the glance-direct ' 'import method.') my_args = self.base_args.copy() my_args['import_method'] = 'glance-direct' my_args['uri'] = 'https://example.com/some/stuff' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('os.access') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_glance_direct_with_file_and_uri( self, mock_stdin, mock_access, mock_utils_exit): expected_msg = ('You cannot specify a --uri with the glance-direct ' 'import method.') my_args = self.base_args.copy() my_args['import_method'] = 'glance-direct' my_args['uri'] = 'https://example.com/some/stuff' my_args['file'] = 'my.browncow' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_access.return_value = True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_glance_direct_with_data_and_uri( self, mock_stdin, mock_utils_exit): expected_msg = ('You cannot specify a --uri with the glance-direct ' 'import method.') my_args = self.base_args.copy() my_args['import_method'] = 'glance-direct' my_args['uri'] = 'https://example.com/some/stuff' args = self._make_args(my_args) mock_stdin.isatty = lambda: False mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_web_download_no_uri( self, mock_stdin, mock_utils_exit): expected_msg = ('URI is required for web-download import method. ' 'Please use \'--uri \'.') my_args = self.base_args.copy() my_args['import_method'] = 'web-download' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_create_via_import_stores_without_uri( self, mock_utils_exit): expected_msg = ('--stores option should only be provided with --uri ' 'option for the web-download import method.') mock_utils_exit.side_effect = self._mock_utils_exit my_args = self.base_args.copy() my_args.update( {'id': 'IMG-01', 'import_method': 'web-download', 'stores': 'file1,file2', 'disk_format': 'raw', 'container_format': 'bare', }) args = self._make_args(my_args) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: with mock.patch.object(self.gc.images, 'get_stores_info') as mocked_stores_info: mocked_stores_info.return_value = self.stores_info_response mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_create_via_import_all_stores_without_uri( self, mock_utils_exit): expected_msg = ('--all-stores option should only be provided with ' '--uri option for the web-download import ' 'method.') mock_utils_exit.side_effect = self._mock_utils_exit my_args = self.base_args.copy() my_args.update( {'id': 'IMG-01', 'import_method': 'web-download', 'os_all_stores': True, 'disk_format': 'raw', 'container_format': 'bare', }) args = self._make_args(my_args) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_web_download_no_uri_with_file( self, mock_stdin, mock_utils_exit): expected_msg = ('URI is required for web-download import method. ' 'Please use \'--uri \'.') my_args = self.base_args.copy() my_args['import_method'] = 'web-download' my_args['file'] = 'my.browncow' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_web_download_no_uri_with_data( self, mock_stdin, mock_utils_exit): expected_msg = ('URI is required for web-download import method. ' 'Please use \'--uri \'.') my_args = self.base_args.copy() my_args['import_method'] = 'web-download' my_args['file'] = 'my.browncow' args = self._make_args(my_args) mock_stdin.isatty = lambda: False mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_web_download_with_data_and_uri( self, mock_stdin, mock_utils_exit): expected_msg = ('You cannot pass data via stdin with the web-download ' 'import method.') my_args = self.base_args.copy() my_args['import_method'] = 'web-download' my_args['uri'] = 'https://example.com/some/stuff' args = self._make_args(my_args) mock_stdin.isatty = lambda: False mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_web_download_with_file_and_uri( self, mock_stdin, mock_utils_exit): expected_msg = ('You cannot specify a --file with the web-download ' 'import method.') my_args = self.base_args.copy() my_args['import_method'] = 'web-download' my_args['uri'] = 'https://example.com/some/stuff' my_args['file'] = 'my.browncow' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_glance_download_no_region_and_id( self, mock_stdin, mock_utils_exit): expected_msg = ('REMOTE GlANCE REGION and REMOTE IMAGE ID are ' 'required for glance-download import method. ' 'Please use --remote-region and ' '--remote-image-id .') my_args = self.base_args.copy() my_args['import_method'] = 'glance-download' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_glance_download_with_uri( self, mock_stdin, mock_utils_exit): expected_msg = ('You cannot specify a --uri with the ' 'glance-download import method.') my_args = self.base_args.copy() my_args['import_method'] = 'glance-download' my_args['remote_region'] = 'REGION2' my_args['remote_image_id'] = 'IMG2' my_args['uri'] = 'https://example.com/some/stuff' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_glance_download_with_file( self, mock_stdin, mock_utils_exit): expected_msg = ('You cannot specify a --file with the ' 'glance-download import method.') my_args = self.base_args.copy() my_args['import_method'] = 'glance-download' my_args['remote_region'] = 'REGION2' my_args['remote_image_id'] = 'IMG2' my_args['file'] = 'my.browncow' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_glance_download_with_data( self, mock_stdin, mock_utils_exit): expected_msg = ('You cannot pass data via stdin with the ' 'glance-download import method.') my_args = self.base_args.copy() my_args['import_method'] = 'glance-download' my_args['remote_region'] = 'REGION2' my_args['remote_image_id'] = 'IMG2' args = self._make_args(my_args) mock_stdin.isatty = lambda: False mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_bad_method( self, mock_stdin, mock_utils_exit): expected_msg = ('Import method \'swift-party-time\' is not valid ' 'for this cloud. Valid values can be retrieved with ' 'import-info command.') my_args = self.base_args.copy() my_args['import_method'] = 'swift-party-time' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_utils_exit.side_effect = self._mock_utils_exit with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_no_method_with_data_and_method_NA( self, mock_stdin, mock_utils_exit): expected_msg = ('Import method \'glance-direct\' is not valid ' 'for this cloud. Valid values can be retrieved with ' 'import-info command.') args = self._make_args(self.base_args) # need to fake some data, or this is "just like" a # create-image-record-only call mock_stdin.isatty = lambda: False mock_utils_exit.side_effect = self._mock_utils_exit my_import_info_response = deepcopy(self.import_info_response) my_import_info_response['import-methods']['value'] = ['web-download'] with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = my_import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') @mock.patch('sys.stdin', autospec=True) def test_neg_image_create_via_import_good_method_not_available( self, mock_stdin, mock_utils_exit): """Make sure the good method names aren't hard coded somewhere""" expected_msg = ('Import method \'glance-direct\' is not valid for ' 'this cloud. Valid values can be retrieved with ' 'import-info command.') my_args = self.base_args.copy() my_args['import_method'] = 'glance-direct' args = self._make_args(my_args) mock_stdin.isatty = lambda: True mock_utils_exit.side_effect = self._mock_utils_exit my_import_info_response = deepcopy(self.import_info_response) my_import_info_response['import-methods']['value'] = ['bad-bad-method'] with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = my_import_info_response try: test_shell.do_image_create_via_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.v2.shell.do_image_import') @mock.patch('glanceclient.v2.shell.do_image_stage') @mock.patch('sys.stdin', autospec=True) def test_image_create_via_import_no_method_with_stdin( self, mock_stdin, mock_do_stage, mock_do_import): """Backward compat -> handle this like a glance-direct""" mock_stdin.isatty = lambda: False self.mock_get_data_file.return_value = io.StringIO() args = self._make_args(self.base_args) with mock.patch.object(self.gc.images, 'create') as mocked_create: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: ignore_fields = ['self', 'access', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'via-stdin' expect_image['name'] = 'Mortimer' expect_image['disk_format'] = 'raw' expect_image['container_format'] = 'bare' mocked_create.return_value = expect_image mocked_get.return_value = expect_image mocked_info.return_value = self.import_info_response test_shell.do_image_create_via_import(self.gc, args) mocked_create.assert_called_once() mock_do_stage.assert_called_once() mock_do_import.assert_called_once() mocked_get.assert_called_with('via-stdin') utils.print_dict.assert_called_with({ 'id': 'via-stdin', 'name': 'Mortimer', 'disk_format': 'raw', 'container_format': 'bare'}) @mock.patch('glanceclient.v2.shell.do_image_import') @mock.patch('glanceclient.v2.shell.do_image_stage') @mock.patch('os.access') @mock.patch('sys.stdin', autospec=True) def test_image_create_via_import_no_method_passing_file( self, mock_stdin, mock_access, mock_do_stage, mock_do_import): """Backward compat -> handle this like a glance-direct""" mock_stdin.isatty = lambda: True self.mock_get_data_file.return_value = io.StringIO() mock_access.return_value = True my_args = self.base_args.copy() my_args['file'] = 'fake-image-file.browncow' args = self._make_args(my_args) with mock.patch.object(self.gc.images, 'create') as mocked_create: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: ignore_fields = ['self', 'access', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'via-file' expect_image['name'] = 'Mortimer' expect_image['disk_format'] = 'raw' expect_image['container_format'] = 'bare' mocked_create.return_value = expect_image mocked_get.return_value = expect_image mocked_info.return_value = self.import_info_response test_shell.do_image_create_via_import(self.gc, args) mocked_create.assert_called_once() mock_do_stage.assert_called_once() mock_do_import.assert_called_once() mocked_get.assert_called_with('via-file') utils.print_dict.assert_called_with({ 'id': 'via-file', 'name': 'Mortimer', 'disk_format': 'raw', 'container_format': 'bare'}) @mock.patch('glanceclient.v2.shell.do_image_import') @mock.patch('glanceclient.v2.shell.do_image_stage') @mock.patch('sys.stdin', autospec=True) def test_do_image_create_via_import_with_no_method_no_data( self, mock_stdin, mock_do_image_stage, mock_do_image_import): """Create an image record without calling do_stage or do_import""" img_create_args = {'name': 'IMG-11', 'os_architecture': 'powerpc', 'id': 'watch-out-for-ossn-0075', 'progress': False} client_args = {'import_method': None, 'file': None, 'uri': None} temp_args = img_create_args.copy() temp_args.update(client_args) args = self._make_args(temp_args) with mock.patch.object(self.gc.images, 'create') as mocked_create: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: ignore_fields = ['self', 'access', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['name'] = 'IMG-11' expect_image['id'] = 'watch-out-for-ossn-0075' expect_image['os_architecture'] = 'powerpc' mocked_create.return_value = expect_image mocked_get.return_value = expect_image mocked_info.return_value = self.import_info_response mock_stdin.isatty = lambda: True test_shell.do_image_create_via_import(self.gc, args) mocked_create.assert_called_once_with(**img_create_args) mocked_get.assert_called_with('watch-out-for-ossn-0075') mock_do_image_stage.assert_not_called() mock_do_image_import.assert_not_called() utils.print_dict.assert_called_with({ 'name': 'IMG-11', 'os_architecture': 'powerpc', 'id': 'watch-out-for-ossn-0075'}) @mock.patch('glanceclient.v2.shell.do_image_import') @mock.patch('glanceclient.v2.shell.do_image_stage') @mock.patch('sys.stdin', autospec=True) def test_do_image_create_via_import_with_web_download( self, mock_stdin, mock_do_image_stage, mock_do_image_import): temp_args = {'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'uri': 'http://example.com/image.qcow', 'import_method': 'web-download', 'progress': False} args = self._make_args(temp_args) with mock.patch.object(self.gc.images, 'create') as mocked_create: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: ignore_fields = ['self', 'access', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' expect_image['status'] = 'queued' mocked_create.return_value = expect_image mocked_get.return_value = expect_image mocked_info.return_value = self.import_info_response mock_stdin.isatty = lambda: True test_shell.do_image_create_via_import(self.gc, args) mock_do_image_stage.assert_not_called() mock_do_image_import.assert_called_once() mocked_create.assert_called_once_with(**temp_args) mocked_get.assert_called_with('pass') utils.print_dict.assert_called_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'status': 'queued'}) @mock.patch('glanceclient.v2.shell.do_image_import') @mock.patch('glanceclient.v2.shell.do_image_stage') @mock.patch('sys.stdin', autospec=True) def test_do_image_create_via_import_with_web_download_with_stores( self, mock_stdin, mock_do_image_stage, mock_do_image_import): temp_args = {'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'uri': 'http://example.com/image.qcow', 'import_method': 'web-download', 'progress': False, 'stores': 'file1,file2'} tmp2_args = {'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'uri': 'http://example.com/image.qcow', 'import_method': 'web-download', 'progress': False} args = self._make_args(temp_args) with mock.patch.object(self.gc.images, 'create') as mocked_create: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: with mock.patch.object(self.gc.images, 'get_stores_info') as m_stores_info: ignore_fields = ['self', 'access', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' expect_image['status'] = 'queued' mocked_create.return_value = expect_image mocked_get.return_value = expect_image mocked_info.return_value = self.import_info_response m_stores_info.return_value = self.stores_info_response mock_stdin.isatty = lambda: True test_shell.do_image_create_via_import(self.gc, args) mock_do_image_stage.assert_not_called() mock_do_image_import.assert_called_once() mocked_create.assert_called_once_with(**tmp2_args) mocked_get.assert_called_with('pass') utils.print_dict.assert_called_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'status': 'queued'}) def test_do_image_update_no_user_props(self): args = self._make_args({'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare'}) with mock.patch.object(self.gc.images, 'update') as mocked_update: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' mocked_update.return_value = expect_image test_shell.do_image_update(self.gc, args) mocked_update.assert_called_once_with('pass', None, name='IMG-01', disk_format='vhd', container_format='bare') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare'}) def test_do_image_update_hide_image(self): args = self._make_args({'id': 'pass', 'os_hidden': 'true'}) with mock.patch.object(self.gc.images, 'update') as mocked_update: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' expect_image['os_hidden'] = True mocked_update.return_value = expect_image test_shell.do_image_update(self.gc, args) mocked_update.assert_called_once_with('pass', None, os_hidden='true') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'os_hidden': True}) def test_do_image_update_revert_hide_image(self): args = self._make_args({'id': 'pass', 'os_hidden': 'false'}) with mock.patch.object(self.gc.images, 'update') as mocked_update: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' expect_image['container_format'] = 'bare' expect_image['os_hidden'] = False mocked_update.return_value = expect_image test_shell.do_image_update(self.gc, args) mocked_update.assert_called_once_with('pass', None, os_hidden='false') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'container_format': 'bare', 'os_hidden': False}) def test_do_image_update_with_user_props(self): args = self._make_args({'id': 'pass', 'name': 'IMG-01', 'property': ['myprop=myval']}) with mock.patch.object(self.gc.images, 'update') as mocked_update: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['myprop'] = 'myval' mocked_update.return_value = expect_image test_shell.do_image_update(self.gc, args) mocked_update.assert_called_once_with('pass', None, name='IMG-01', myprop='myval') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'myprop': 'myval'}) def test_do_image_update_with_remove_props(self): args = self._make_args({'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd', 'remove-property': ['container_format']}) with mock.patch.object(self.gc.images, 'update') as mocked_update: ignore_fields = ['self', 'access', 'file', 'schema'] expect_image = dict([(field, field) for field in ignore_fields]) expect_image['id'] = 'pass' expect_image['name'] = 'IMG-01' expect_image['disk_format'] = 'vhd' mocked_update.return_value = expect_image test_shell.do_image_update(self.gc, args) mocked_update.assert_called_once_with('pass', ['container_format'], name='IMG-01', disk_format='vhd') utils.print_dict.assert_called_once_with({ 'id': 'pass', 'name': 'IMG-01', 'disk_format': 'vhd'}) def test_do_explain(self): input = { 'page_size': 18, 'id': 'pass', 'schemas': 'test', 'model': 'test', } args = self._make_args(input) with mock.patch.object(utils, 'print_list'): test_shell.do_explain(self.gc, args) self.gc.schemas.get.assert_called_once_with('test') def test_do_location_add(self): gc = self.gc loc = {'url': 'http://foo.com/', 'metadata': {'foo': 'bar'}, 'validation_data': {'checksum': 'csum', 'os_hash_algo': 'algo', 'os_hash_value': 'value'}} args = {'id': 'pass', 'url': loc['url'], 'metadata': json.dumps(loc['metadata']), 'checksum': 'csum', 'hash_algo': 'algo', 'hash_value': 'value'} with mock.patch.object(gc.images, 'add_location') as mocked_addloc: expect_image = {'id': 'pass', 'locations': [loc]} mocked_addloc.return_value = expect_image test_shell.do_location_add(self.gc, self._make_args(args)) mocked_addloc.assert_called_once_with( 'pass', loc['url'], loc['metadata'], validation_data=loc['validation_data']) utils.print_dict.assert_called_once_with(expect_image) def test_do_location_delete(self): gc = self.gc loc_set = set(['http://foo/bar', 'http://spam/ham']) args = self._make_args({'id': 'pass', 'url': loc_set}) with mock.patch.object(gc.images, 'delete_locations') as mocked_rmloc: test_shell.do_location_delete(self.gc, args) mocked_rmloc.assert_called_once_with('pass', loc_set) def test_do_location_update(self): gc = self.gc loc = {'url': 'http://foo.com/', 'metadata': {'foo': 'bar'}} args = self._make_args({'id': 'pass', 'url': loc['url'], 'metadata': json.dumps(loc['metadata'])}) with mock.patch.object(gc.images, 'update_location') as mocked_modloc: expect_image = {'id': 'pass', 'locations': [loc]} mocked_modloc.return_value = expect_image test_shell.do_location_update(self.gc, args) mocked_modloc.assert_called_once_with('pass', loc['url'], loc['metadata']) utils.print_dict.assert_called_once_with(expect_image) def test_image_upload(self): args = self._make_args( {'id': 'IMG-01', 'file': 'test', 'size': 1024, 'progress': False}) with mock.patch.object(self.gc.images, 'upload') as mocked_upload: utils.get_data_file = mock.Mock(return_value='testfile') mocked_upload.return_value = None test_shell.do_image_upload(self.gc, args) mocked_upload.assert_called_once_with('IMG-01', 'testfile', 1024, backend=None) @mock.patch('glanceclient.common.utils.exit') def test_image_upload_invalid_store(self, mock_utils_exit): expected_msg = ("Store 'dummy' is not valid for this cloud. " "Valid values can be retrieved with stores-info " "command.") mock_utils_exit.side_effect = self._mock_utils_exit args = self._make_args( {'id': 'IMG-01', 'file': 'test', 'size': 1024, 'progress': False, 'store': 'dummy'}) with mock.patch.object(self.gc.images, 'get_stores_info') as mock_stores_info: mock_stores_info.return_value = self.stores_info_response try: test_shell.do_image_upload(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_import_not_available(self, mock_utils_exit): expected_msg = 'Target Glance does not support Image Import workflow' mock_utils_exit.side_effect = self._mock_utils_exit args = self._make_args( {'id': 'IMG-01', 'import_method': 'smarty-pants', 'uri': None}) with mock.patch.object(self.gc.images, 'import') as mocked_import: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.side_effect = exc.HTTPNotFound try: test_shell.do_image_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) mocked_import.assert_not_called() @mock.patch('glanceclient.common.utils.exit') def test_neg_image_import_bad_method(self, mock_utils_exit): expected_msg = ('Import method \'smarty-pants\' is not valid for this ' 'cloud. Valid values can be retrieved with ' 'import-info command.') mock_utils_exit.side_effect = self._mock_utils_exit args = self._make_args( {'id': 'IMG-01', 'import_method': 'smarty-pants', 'uri': None}) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_import_no_methods_configured(self, mock_utils_exit): expected_msg = ('Import method \'glance-direct\' is not valid for ' 'this cloud. Valid values can be retrieved with ' 'import-info command.') mock_utils_exit.side_effect = self._mock_utils_exit args = self._make_args( {'id': 'IMG-01', 'import_method': 'glance-direct', 'uri': None}) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = {"import-methods": {"value": []}} try: test_shell.do_image_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_import_glance_direct_image_not_uploading_status( self, mock_utils_exit): expected_msg = ('The \'glance-direct\' import method can only be ' 'applied to an image in status \'uploading\'') mock_utils_exit.side_effect = self._mock_utils_exit args = self._make_args( {'id': 'IMG-01', 'import_method': 'glance-direct', 'uri': None}) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: with mock.patch.object(self.gc.images, 'get') as mocked_get: mocked_get.return_value = {'status': 'queued', 'container_format': 'bare', 'disk_format': 'raw'} mocked_info.return_value = self.import_info_response try: test_shell.do_image_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_import_web_download_image_not_queued_status( self, mock_utils_exit): expected_msg = ('The \'web-download\' import method can only be ' 'applied to an image in status \'queued\'') mock_utils_exit.side_effect = self._mock_utils_exit args = self._make_args( {'id': 'IMG-01', 'import_method': 'web-download', 'uri': 'http://joes-image-shack.com/funky.qcow2'}) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: with mock.patch.object(self.gc.images, 'get') as mocked_get: mocked_get.return_value = {'status': 'uploading', 'container_format': 'bare', 'disk_format': 'raw'} mocked_info.return_value = self.import_info_response try: test_shell.do_image_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_import_image_no_container_format( self, mock_utils_exit): expected_msg = ('The \'container_format\' and \'disk_format\' ' 'properties must be set on an image before it can be ' 'imported.') mock_utils_exit.side_effect = self._mock_utils_exit args = self._make_args( {'id': 'IMG-01', 'import_method': 'web-download', 'uri': 'http://joes-image-shack.com/funky.qcow2'}) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: with mock.patch.object(self.gc.images, 'get') as mocked_get: mocked_get.return_value = {'status': 'uploading', 'disk_format': 'raw'} mocked_info.return_value = self.import_info_response try: test_shell.do_image_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_import_image_no_disk_format( self, mock_utils_exit): expected_msg = ('The \'container_format\' and \'disk_format\' ' 'properties must be set on an image before it can be ' 'imported.') mock_utils_exit.side_effect = self._mock_utils_exit args = self._make_args( {'id': 'IMG-01', 'import_method': 'web-download', 'uri': 'http://joes-image-shack.com/funky.qcow2'}) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: with mock.patch.object(self.gc.images, 'get') as mocked_get: mocked_get.return_value = {'status': 'uploading', 'container_format': 'bare'} mocked_info.return_value = self.import_info_response try: test_shell.do_image_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_image_import_invalid_store(self, mock_utils_exit): expected_msg = ("Store 'dummy' is not valid for this cloud. " "Valid values can be retrieved with stores-info " "command.") mock_utils_exit.side_effect = self._mock_utils_exit args = self._make_args( {'id': 'IMG-01', 'import_method': 'glance-direct', 'uri': None, 'store': 'dummy'}) with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_get.return_value = {'status': 'uploading', 'container_format': 'bare', 'disk_format': 'raw'} with mock.patch.object(self.gc.images, 'get_stores_info') as mock_stores_info: mocked_info.return_value = self.import_info_response mock_stores_info.return_value = self.stores_info_response try: test_shell.do_image_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) def test_image_import_glance_direct(self): args = self._make_args( {'id': 'IMG-01', 'import_method': 'glance-direct', 'uri': None}) with mock.patch.object(self.gc.images, 'image_import') as mock_import: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_get.return_value = {'status': 'uploading', 'container_format': 'bare', 'disk_format': 'raw'} mocked_info.return_value = self.import_info_response mock_import.return_value = None test_shell.do_image_import(self.gc, args) mock_import.assert_called_once_with( 'IMG-01', 'glance-direct', uri=None, remote_region=None, remote_image_id=None, remote_service_interface=None, backend=None, all_stores=None, allow_failure=True, stores=None) def test_image_import_web_download(self): args = self._make_args( {'id': 'IMG-01', 'uri': 'http://example.com/image.qcow', 'import_method': 'web-download'}) with mock.patch.object(self.gc.images, 'image_import') as mock_import: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_get.return_value = {'status': 'queued', 'container_format': 'bare', 'disk_format': 'raw'} mocked_info.return_value = self.import_info_response mock_import.return_value = None test_shell.do_image_import(self.gc, args) mock_import.assert_called_once_with( 'IMG-01', 'web-download', uri='http://example.com/image.qcow', remote_region=None, remote_image_id=None, remote_service_interface=None, all_stores=None, allow_failure=True, backend=None, stores=None) def test_image_import_glance_download(self): args = self._make_args( {'id': 'IMG-01', 'uri': None, 'remote_region': 'REGION2', 'remote_image_id': 'IMG-02', 'import_method': 'glance-download', 'remote_service_interface': 'public'}) with mock.patch.object(self.gc.images, 'image_import') as mock_import: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_get.return_value = {'status': 'queued', 'container_format': 'bare', 'disk_format': 'raw'} mocked_info.return_value = self.import_info_response mock_import.return_value = None test_shell.do_image_import(self.gc, args) mock_import.assert_called_once_with( 'IMG-01', 'glance-download', uri=None, remote_region='REGION2', remote_image_id='IMG-02', remote_service_interface='public', all_stores=None, allow_failure=True, backend=None, stores=None) @mock.patch('glanceclient.common.utils.print_image') def test_image_import_no_print_image(self, mocked_utils_print_image): args = self._make_args( {'id': 'IMG-02', 'uri': None, 'import_method': 'glance-direct', 'from_create': True}) with mock.patch.object(self.gc.images, 'image_import') as mock_import: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_get.return_value = {'status': 'uploading', 'container_format': 'bare', 'disk_format': 'raw'} mocked_info.return_value = self.import_info_response mock_import.return_value = None test_shell.do_image_import(self.gc, args) mock_import.assert_called_once_with( 'IMG-02', 'glance-direct', None, stores=None, all_stores=None, allow_failure=True, remote_region=None, remote_image_id=None, remote_service_interface=None, backend=None) mocked_utils_print_image.assert_not_called() @mock.patch('glanceclient.common.utils.print_image') @mock.patch('glanceclient.v2.shell._validate_backend') def test_image_import_multiple_stores(self, mocked_utils_print_image, msvb): args = self._make_args( {'id': 'IMG-02', 'uri': None, 'import_method': 'glance-direct', 'from_create': False, 'stores': 'site1,site2'}) with mock.patch.object(self.gc.images, 'image_import') as mock_import: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_get.return_value = {'status': 'uploading', 'container_format': 'bare', 'disk_format': 'raw'} mocked_info.return_value = self.import_info_response mock_import.return_value = None test_shell.do_image_import(self.gc, args) mock_import.assert_called_once_with( 'IMG-02', 'glance-direct', uri=None, remote_region=None, remote_image_id=None, remote_service_interface=None, all_stores=None, allow_failure=True, stores=['site1', 'site2'], backend=None) @mock.patch('glanceclient.common.utils.print_image') @mock.patch('glanceclient.v2.shell._validate_backend') def test_image_import_copy_image(self, mocked_utils_print_image, msvb): args = self._make_args( {'id': 'IMG-02', 'uri': None, 'import_method': 'copy-image', 'from_create': False, 'stores': 'file1,file2'}) with mock.patch.object(self.gc.images, 'image_import') as mock_import: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_get.return_value = {'status': 'active', 'container_format': 'bare', 'disk_format': 'raw'} mocked_info.return_value = self.import_info_response mock_import.return_value = None test_shell.do_image_import(self.gc, args) mock_import.assert_called_once_with( 'IMG-02', 'copy-image', uri=None, remote_region=None, remote_image_id=None, remote_service_interface=None, all_stores=None, allow_failure=True, stores=['file1', 'file2'], backend=None) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_import_copy_image_not_active( self, mock_utils_exit): expected_msg = ("The 'copy-image' import method can only be used on " "an image with status 'active'.") mock_utils_exit.side_effect = self._mock_utils_exit args = self._make_args( {'id': 'IMG-02', 'uri': None, 'import_method': 'copy-image', 'disk_format': 'raw', 'container_format': 'bare', 'from_create': False, 'stores': 'file1,file2'}) with mock.patch.object( self.gc.images, 'get_stores_info') as mocked_stores_info: with mock.patch.object(self.gc.images, 'get') as mocked_get: with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_stores_info.return_value = self.stores_info_response mocked_get.return_value = {'status': 'uploading', 'container_format': 'bare', 'disk_format': 'raw'} mocked_info.return_value = self.import_info_response try: test_shell.do_image_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) @mock.patch('glanceclient.common.utils.exit') def test_neg_image_import_stores_all_stores_not_specified( self, mock_utils_exit): expected_msg = ("Provide either --stores or --all-stores for " "'copy-image' import method.") mock_utils_exit.side_effect = self._mock_utils_exit my_args = self.base_args.copy() my_args.update( {'id': 'IMG-01', 'import_method': 'copy-image', 'disk_format': 'raw', 'container_format': 'bare', }) args = self._make_args(my_args) with mock.patch.object(self.gc.images, 'get_import_info') as mocked_info: mocked_info.return_value = self.import_info_response try: test_shell.do_image_import(self.gc, args) self.fail("utils.exit should have been called") except SystemExit: pass mock_utils_exit.assert_called_once_with(expected_msg) def test_image_download(self): args = self._make_args( {'id': 'IMG-01', 'file': 'test', 'progress': True, 'allow_md5_fallback': False}) with mock.patch.object(self.gc.images, 'data') as mocked_data, \ mock.patch.object(utils, '_extract_request_id'): mocked_data.return_value = utils.RequestIdProxy( [c for c in 'abcdef']) test_shell.do_image_download(self.gc, args) mocked_data.assert_called_once_with('IMG-01', allow_md5_fallback=False) # check that non-default value is being passed correctly args.allow_md5_fallback = True with mock.patch.object(self.gc.images, 'data') as mocked_data, \ mock.patch.object(utils, '_extract_request_id'): mocked_data.return_value = utils.RequestIdProxy( [c for c in 'abcdef']) test_shell.do_image_download(self.gc, args) mocked_data.assert_called_once_with('IMG-01', allow_md5_fallback=True) @mock.patch.object(utils, 'exit') @mock.patch('sys.stdout', autospec=True) def test_image_download_no_file_arg(self, mocked_stdout, mocked_utils_exit): # Indicate that no file name was given as command line argument args = self._make_args({'id': '1234', 'file': None, 'progress': False, 'allow_md5_fallback': False}) # Indicate that no file is specified for output redirection mocked_stdout.isatty = lambda: True test_shell.do_image_download(self.gc, args) mocked_utils_exit.assert_called_once_with( 'No redirection or local file specified for downloaded image' ' data. Please specify a local file with --file to save' ' downloaded image or redirect output to another source.') def test_do_image_delete(self): args = argparse.Namespace(id=['image1', 'image2']) with mock.patch.object(self.gc.images, 'delete') as mocked_delete: mocked_delete.return_value = 0 test_shell.do_image_delete(self.gc, args) self.assertEqual(2, mocked_delete.call_count) def test_do_image_deactivate(self): args = argparse.Namespace(id='image1') with mock.patch.object(self.gc.images, 'deactivate') as mocked_deactivate: mocked_deactivate.return_value = 0 test_shell.do_image_deactivate(self.gc, args) self.assertEqual(1, mocked_deactivate.call_count) def test_do_image_reactivate(self): args = argparse.Namespace(id='image1') with mock.patch.object(self.gc.images, 'reactivate') as mocked_reactivate: mocked_reactivate.return_value = 0 test_shell.do_image_reactivate(self.gc, args) self.assertEqual(1, mocked_reactivate.call_count) @mock.patch.object(utils, 'exit') @mock.patch.object(utils, 'print_err') def test_do_image_delete_with_invalid_ids(self, mocked_print_err, mocked_utils_exit): args = argparse.Namespace(id=['image1', 'image2']) with mock.patch.object(self.gc.images, 'delete') as mocked_delete: mocked_delete.side_effect = exc.HTTPNotFound test_shell.do_image_delete(self.gc, args) self.assertEqual(2, mocked_delete.call_count) self.assertEqual(2, mocked_print_err.call_count) mocked_utils_exit.assert_called_once_with() @mock.patch.object(utils, 'exit') def test_do_image_delete_from_store_not_found(self, mocked_utils_exit): args = argparse.Namespace(id='image1', store='store1') with mock.patch.object(self.gc.images, 'delete_from_store') as mocked_delete: mocked_delete.side_effect = exc.HTTPNotFound test_shell.do_stores_delete(self.gc, args) self.assertEqual(1, mocked_delete.call_count) mocked_utils_exit.assert_called_once_with('Multi Backend support ' 'is not enabled or ' 'Image/store not found.') def test_do_image_delete_from_store(self): args = argparse.Namespace(id='image1', store='store1') with mock.patch.object(self.gc.images, 'delete_from_store') as mocked_delete: test_shell.do_stores_delete(self.gc, args) mocked_delete.assert_called_once_with('store1', 'image1') @mock.patch.object(utils, 'exit') @mock.patch.object(utils, 'print_err') def test_do_image_delete_with_forbidden_ids(self, mocked_print_err, mocked_utils_exit): args = argparse.Namespace(id=['image1', 'image2']) with mock.patch.object(self.gc.images, 'delete') as mocked_delete: mocked_delete.side_effect = exc.HTTPForbidden test_shell.do_image_delete(self.gc, args) self.assertEqual(2, mocked_delete.call_count) self.assertEqual(2, mocked_print_err.call_count) mocked_utils_exit.assert_called_once_with() @mock.patch.object(utils, 'exit') @mock.patch.object(utils, 'print_err') def test_do_image_delete_with_image_in_use(self, mocked_print_err, mocked_utils_exit): args = argparse.Namespace(id=['image1', 'image2']) with mock.patch.object(self.gc.images, 'delete') as mocked_delete: mocked_delete.side_effect = exc.HTTPConflict test_shell.do_image_delete(self.gc, args) self.assertEqual(2, mocked_delete.call_count) self.assertEqual(2, mocked_print_err.call_count) mocked_utils_exit.assert_called_once_with() def test_do_image_delete_deleted(self): image_id = 'deleted-img' args = argparse.Namespace(id=[image_id]) with mock.patch.object(self.gc.images, 'delete') as mocked_delete: mocked_delete.side_effect = exc.HTTPNotFound self.assert_exits_with_msg(func=test_shell.do_image_delete, func_args=args) @mock.patch('sys.stdout', autospec=True) @mock.patch.object(utils, 'print_err') def test_do_image_download_with_forbidden_id(self, mocked_print_err, mocked_stdout): args = self._make_args({'id': 'IMG-01', 'file': None, 'progress': False, 'allow_md5_fallback': False}) mocked_stdout.isatty = lambda: False with mock.patch.object(self.gc.images, 'data') as mocked_data: mocked_data.side_effect = exc.HTTPForbidden try: test_shell.do_image_download(self.gc, args) self.fail('Exit not called') except SystemExit: pass self.assertEqual(1, mocked_data.call_count) self.assertEqual(1, mocked_print_err.call_count) @mock.patch('sys.stdout', autospec=True) @mock.patch.object(utils, 'print_err') def test_do_image_download_with_500(self, mocked_print_err, mocked_stdout): args = self._make_args({'id': 'IMG-01', 'file': None, 'progress': False, 'allow_md5_fallback': False}) mocked_stdout.isatty = lambda: False with mock.patch.object(self.gc.images, 'data') as mocked_data: mocked_data.side_effect = exc.HTTPInternalServerError try: test_shell.do_image_download(self.gc, args) self.fail('Exit not called') except SystemExit: pass self.assertEqual(1, mocked_data.call_count) self.assertEqual(1, mocked_print_err.call_count) def test_do_member_list(self): args = self._make_args({'image_id': 'IMG-01'}) with mock.patch.object(self.gc.image_members, 'list') as mocked_list: mocked_list.return_value = {} test_shell.do_member_list(self.gc, args) mocked_list.assert_called_once_with('IMG-01') columns = ['Image ID', 'Member ID', 'Status'] utils.print_list.assert_called_once_with({}, columns) def test_do_member_get(self): args = self._make_args({'image_id': 'IMG-01', 'member_id': 'MEM-01'}) with mock.patch.object(self.gc.image_members, 'get') as mock_get: mock_get.return_value = {} test_shell.do_member_get(self.gc, args) mock_get.assert_called_once_with('IMG-01', 'MEM-01') utils.print_dict.assert_called_once_with({}) def test_do_member_create(self): args = self._make_args({'image_id': 'IMG-01', 'member_id': 'MEM-01'}) with mock.patch.object(self.gc.image_members, 'create') as mock_create: mock_create.return_value = {} test_shell.do_member_create(self.gc, args) mock_create.assert_called_once_with('IMG-01', 'MEM-01') columns = ['Image ID', 'Member ID', 'Status'] utils.print_list.assert_called_once_with([{}], columns) def test_do_member_create_with_few_arguments(self): args = self._make_args({'image_id': None, 'member_id': 'MEM-01'}) msg = 'Unable to create member. Specify image_id and member_id' self.assert_exits_with_msg(func=test_shell.do_member_create, func_args=args, err_msg=msg) def test_do_member_update(self): input = { 'image_id': 'IMG-01', 'member_id': 'MEM-01', 'member_status': 'status', } args = self._make_args(input) with mock.patch.object(self.gc.image_members, 'update') as mock_update: mock_update.return_value = {} test_shell.do_member_update(self.gc, args) mock_update.assert_called_once_with('IMG-01', 'MEM-01', 'status') columns = ['Image ID', 'Member ID', 'Status'] utils.print_list.assert_called_once_with([{}], columns) def test_do_member_update_with_few_arguments(self): input = { 'image_id': 'IMG-01', 'member_id': 'MEM-01', 'member_status': None, } args = self._make_args(input) msg = 'Unable to update member. Specify image_id, member_id' \ ' and member_status' self.assert_exits_with_msg(func=test_shell.do_member_update, func_args=args, err_msg=msg) def test_do_member_delete(self): args = self._make_args({'image_id': 'IMG-01', 'member_id': 'MEM-01'}) with mock.patch.object(self.gc.image_members, 'delete') as mock_delete: test_shell.do_member_delete(self.gc, args) mock_delete.assert_called_once_with('IMG-01', 'MEM-01') def test_do_member_delete_with_few_arguments(self): args = self._make_args({'image_id': None, 'member_id': 'MEM-01'}) msg = 'Unable to delete member. Specify image_id and member_id' self.assert_exits_with_msg(func=test_shell.do_member_delete, func_args=args, err_msg=msg) def test_image_tag_update(self): args = self._make_args({'image_id': 'IMG-01', 'tag_value': 'tag01'}) with mock.patch.object(self.gc.image_tags, 'update') as mocked_update: self.gc.images.get = mock.Mock(return_value={}) mocked_update.return_value = None test_shell.do_image_tag_update(self.gc, args) mocked_update.assert_called_once_with('IMG-01', 'tag01') def test_image_tag_update_with_few_arguments(self): args = self._make_args({'image_id': None, 'tag_value': 'tag01'}) msg = 'Unable to update tag. Specify image_id and tag_value' self.assert_exits_with_msg(func=test_shell.do_image_tag_update, func_args=args, err_msg=msg) def test_image_tag_delete(self): args = self._make_args({'image_id': 'IMG-01', 'tag_value': 'tag01'}) with mock.patch.object(self.gc.image_tags, 'delete') as mocked_delete: mocked_delete.return_value = None test_shell.do_image_tag_delete(self.gc, args) mocked_delete.assert_called_once_with('IMG-01', 'tag01') def test_image_tag_delete_with_few_arguments(self): args = self._make_args({'image_id': 'IMG-01', 'tag_value': None}) msg = 'Unable to delete tag. Specify image_id and tag_value' self.assert_exits_with_msg(func=test_shell.do_image_tag_delete, func_args=args, err_msg=msg) def test_do_md_namespace_create(self): args = self._make_args({'namespace': 'MyNamespace', 'protected': True}) with mock.patch.object(self.gc.metadefs_namespace, 'create') as mocked_create: expect_namespace = { 'namespace': 'MyNamespace', 'protected': True } mocked_create.return_value = expect_namespace test_shell.do_md_namespace_create(self.gc, args) mocked_create.assert_called_once_with(namespace='MyNamespace', protected=True) utils.print_dict.assert_called_once_with(expect_namespace) def test_do_md_namespace_import(self): args = self._make_args({'file': 'test'}) expect_namespace = { 'namespace': 'MyNamespace', 'protected': True } with mock.patch.object(self.gc.metadefs_namespace, 'create') as mocked_create: mock_read = mock.Mock(return_value=json.dumps(expect_namespace)) mock_file = mock.Mock(read=mock_read) utils.get_data_file = mock.Mock(return_value=mock_file) mocked_create.return_value = expect_namespace test_shell.do_md_namespace_import(self.gc, args) mocked_create.assert_called_once_with(**expect_namespace) utils.print_dict.assert_called_once_with(expect_namespace) def test_do_md_namespace_import_invalid_json(self): args = self._make_args({'file': 'test'}) mock_read = mock.Mock(return_value='Invalid') mock_file = mock.Mock(read=mock_read) utils.get_data_file = mock.Mock(return_value=mock_file) self.assertRaises(SystemExit, test_shell.do_md_namespace_import, self.gc, args) def test_do_md_namespace_import_no_input(self): args = self._make_args({'file': None}) utils.get_data_file = mock.Mock(return_value=None) self.assertRaises(SystemExit, test_shell.do_md_namespace_import, self.gc, args) def test_do_md_namespace_update(self): args = self._make_args({'id': 'MyNamespace', 'protected': True}) with mock.patch.object(self.gc.metadefs_namespace, 'update') as mocked_update: expect_namespace = { 'namespace': 'MyNamespace', 'protected': True } mocked_update.return_value = expect_namespace test_shell.do_md_namespace_update(self.gc, args) mocked_update.assert_called_once_with('MyNamespace', id='MyNamespace', protected=True) utils.print_dict.assert_called_once_with(expect_namespace) def test_do_md_namespace_show(self): args = self._make_args({'namespace': 'MyNamespace', 'max_column_width': 80, 'resource_type': None}) with mock.patch.object(self.gc.metadefs_namespace, 'get') as mocked_get: expect_namespace = {'namespace': 'MyNamespace'} mocked_get.return_value = expect_namespace test_shell.do_md_namespace_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace') utils.print_dict.assert_called_once_with(expect_namespace, 80) def test_do_md_namespace_show_resource_type(self): args = self._make_args({'namespace': 'MyNamespace', 'max_column_width': 80, 'resource_type': 'RESOURCE'}) with mock.patch.object(self.gc.metadefs_namespace, 'get') as mocked_get: expect_namespace = {'namespace': 'MyNamespace'} mocked_get.return_value = expect_namespace test_shell.do_md_namespace_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace', resource_type='RESOURCE') utils.print_dict.assert_called_once_with(expect_namespace, 80) def test_do_md_namespace_list(self): args = self._make_args({'resource_type': None, 'visibility': None, 'page_size': None}) with mock.patch.object(self.gc.metadefs_namespace, 'list') as mocked_list: expect_namespaces = [{'namespace': 'MyNamespace'}] mocked_list.return_value = expect_namespaces test_shell.do_md_namespace_list(self.gc, args) mocked_list.assert_called_once_with(filters={}) utils.print_list.assert_called_once_with(expect_namespaces, ['namespace']) def test_do_md_namespace_list_page_size(self): args = self._make_args({'resource_type': None, 'visibility': None, 'page_size': 2}) with mock.patch.object(self.gc.metadefs_namespace, 'list') as mocked_list: expect_namespaces = [{'namespace': 'MyNamespace'}] mocked_list.return_value = expect_namespaces test_shell.do_md_namespace_list(self.gc, args) mocked_list.assert_called_once_with(filters={}, page_size=2) utils.print_list.assert_called_once_with(expect_namespaces, ['namespace']) def test_do_md_namespace_list_one_filter(self): args = self._make_args({'resource_types': ['OS::Compute::Aggregate'], 'visibility': None, 'page_size': None}) with mock.patch.object(self.gc.metadefs_namespace, 'list') as \ mocked_list: expect_namespaces = [{'namespace': 'MyNamespace'}] mocked_list.return_value = expect_namespaces test_shell.do_md_namespace_list(self.gc, args) mocked_list.assert_called_once_with(filters={ 'resource_types': ['OS::Compute::Aggregate']}) utils.print_list.assert_called_once_with(expect_namespaces, ['namespace']) def test_do_md_namespace_list_all_filters(self): args = self._make_args({'resource_types': ['OS::Compute::Aggregate'], 'visibility': 'public', 'page_size': None}) with mock.patch.object(self.gc.metadefs_namespace, 'list') as mocked_list: expect_namespaces = [{'namespace': 'MyNamespace'}] mocked_list.return_value = expect_namespaces test_shell.do_md_namespace_list(self.gc, args) mocked_list.assert_called_once_with(filters={ 'resource_types': ['OS::Compute::Aggregate'], 'visibility': 'public'}) utils.print_list.assert_called_once_with(expect_namespaces, ['namespace']) def test_do_md_namespace_list_unknown_filter(self): args = self._make_args({'resource_type': None, 'visibility': None, 'some_arg': 'some_value', 'page_size': None}) with mock.patch.object(self.gc.metadefs_namespace, 'list') as mocked_list: expect_namespaces = [{'namespace': 'MyNamespace'}] mocked_list.return_value = expect_namespaces test_shell.do_md_namespace_list(self.gc, args) mocked_list.assert_called_once_with(filters={}) utils.print_list.assert_called_once_with(expect_namespaces, ['namespace']) def test_do_md_namespace_delete(self): args = self._make_args({'namespace': 'MyNamespace', 'content': False}) with mock.patch.object(self.gc.metadefs_namespace, 'delete') as \ mocked_delete: test_shell.do_md_namespace_delete(self.gc, args) mocked_delete.assert_called_once_with('MyNamespace') def test_do_md_resource_type_associate(self): args = self._make_args({'namespace': 'MyNamespace', 'name': 'MyResourceType', 'prefix': 'PREFIX:'}) with mock.patch.object(self.gc.metadefs_resource_type, 'associate') as mocked_associate: expect_rt = { 'namespace': 'MyNamespace', 'name': 'MyResourceType', 'prefix': 'PREFIX:' } mocked_associate.return_value = expect_rt test_shell.do_md_resource_type_associate(self.gc, args) mocked_associate.assert_called_once_with('MyNamespace', **expect_rt) utils.print_dict.assert_called_once_with(expect_rt) def test_do_md_resource_type_deassociate(self): args = self._make_args({'namespace': 'MyNamespace', 'resource_type': 'MyResourceType'}) with mock.patch.object(self.gc.metadefs_resource_type, 'deassociate') as mocked_deassociate: test_shell.do_md_resource_type_deassociate(self.gc, args) mocked_deassociate.assert_called_once_with('MyNamespace', 'MyResourceType') def test_do_md_resource_type_list(self): args = self._make_args({}) with mock.patch.object(self.gc.metadefs_resource_type, 'list') as mocked_list: expect_objects = ['MyResourceType1', 'MyResourceType2'] mocked_list.return_value = expect_objects test_shell.do_md_resource_type_list(self.gc, args) self.assertEqual(1, mocked_list.call_count) def test_do_md_namespace_resource_type_list(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_resource_type, 'get') as mocked_get: expect_objects = [{'namespace': 'MyNamespace', 'object': 'MyObject'}] mocked_get.return_value = expect_objects test_shell.do_md_namespace_resource_type_list(self.gc, args) mocked_get.assert_called_once_with('MyNamespace') utils.print_list.assert_called_once_with(expect_objects, ['name', 'prefix', 'properties_target']) def test_do_md_property_create(self): args = self._make_args({'namespace': 'MyNamespace', 'name': "MyProperty", 'title': "Title", 'type': 'boolean', 'schema': '{}'}) with mock.patch.object(self.gc.metadefs_property, 'create') as mocked_create: expect_property = { 'namespace': 'MyNamespace', 'name': 'MyProperty', 'title': 'Title', 'type': 'boolean', } mocked_create.return_value = expect_property test_shell.do_md_property_create(self.gc, args) mocked_create.assert_called_once_with('MyNamespace', name='MyProperty', title='Title', type='boolean') utils.print_dict.assert_called_once_with(expect_property) def test_do_md_property_create_invalid_schema(self): args = self._make_args({'namespace': 'MyNamespace', 'name': "MyProperty", 'title': "Title", 'type': "boolean", 'schema': 'Invalid'}) self.assertRaises(SystemExit, test_shell.do_md_property_create, self.gc, args) def test_do_md_property_update(self): args = self._make_args({'namespace': 'MyNamespace', 'property': 'MyProperty', 'name': 'NewName', 'title': "Title", 'schema': '{}'}) with mock.patch.object(self.gc.metadefs_property, 'update') as mocked_update: expect_property = { 'namespace': 'MyNamespace', 'name': 'MyProperty', 'title': 'Title' } mocked_update.return_value = expect_property test_shell.do_md_property_update(self.gc, args) mocked_update.assert_called_once_with('MyNamespace', 'MyProperty', name='NewName', title='Title') utils.print_dict.assert_called_once_with(expect_property) def test_do_md_property_update_invalid_schema(self): args = self._make_args({'namespace': 'MyNamespace', 'property': 'MyProperty', 'name': "MyObject", 'title': "Title", 'schema': 'Invalid'}) self.assertRaises(SystemExit, test_shell.do_md_property_update, self.gc, args) def test_do_md_property_show(self): args = self._make_args({'namespace': 'MyNamespace', 'property': 'MyProperty', 'max_column_width': 80}) with mock.patch.object(self.gc.metadefs_property, 'get') as mocked_get: expect_property = { 'namespace': 'MyNamespace', 'property': 'MyProperty', 'title': 'Title' } mocked_get.return_value = expect_property test_shell.do_md_property_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace', 'MyProperty') utils.print_dict.assert_called_once_with(expect_property, 80) def test_do_md_property_delete(self): args = self._make_args({'namespace': 'MyNamespace', 'property': 'MyProperty'}) with mock.patch.object(self.gc.metadefs_property, 'delete') as mocked_delete: test_shell.do_md_property_delete(self.gc, args) mocked_delete.assert_called_once_with('MyNamespace', 'MyProperty') def test_do_md_namespace_property_delete(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_property, 'delete_all') as mocked_delete_all: test_shell.do_md_namespace_properties_delete(self.gc, args) mocked_delete_all.assert_called_once_with('MyNamespace') def test_do_md_property_list(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_property, 'list') as mocked_list: expect_objects = [{'namespace': 'MyNamespace', 'property': 'MyProperty', 'title': 'MyTitle'}] mocked_list.return_value = expect_objects test_shell.do_md_property_list(self.gc, args) mocked_list.assert_called_once_with('MyNamespace') utils.print_list.assert_called_once_with(expect_objects, ['name', 'title', 'type']) def test_do_md_object_create(self): args = self._make_args({'namespace': 'MyNamespace', 'name': "MyObject", 'schema': '{}'}) with mock.patch.object(self.gc.metadefs_object, 'create') as mocked_create: expect_object = { 'namespace': 'MyNamespace', 'name': 'MyObject' } mocked_create.return_value = expect_object test_shell.do_md_object_create(self.gc, args) mocked_create.assert_called_once_with('MyNamespace', name='MyObject') utils.print_dict.assert_called_once_with(expect_object) def test_do_md_object_create_invalid_schema(self): args = self._make_args({'namespace': 'MyNamespace', 'name': "MyObject", 'schema': 'Invalid'}) self.assertRaises(SystemExit, test_shell.do_md_object_create, self.gc, args) def test_do_md_object_update(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject', 'name': 'NewName', 'schema': '{}'}) with mock.patch.object(self.gc.metadefs_object, 'update') as mocked_update: expect_object = { 'namespace': 'MyNamespace', 'name': 'MyObject' } mocked_update.return_value = expect_object test_shell.do_md_object_update(self.gc, args) mocked_update.assert_called_once_with('MyNamespace', 'MyObject', name='NewName') utils.print_dict.assert_called_once_with(expect_object) def test_do_md_object_update_invalid_schema(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject', 'name': "MyObject", 'schema': 'Invalid'}) self.assertRaises(SystemExit, test_shell.do_md_object_update, self.gc, args) def test_do_md_object_show(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject', 'max_column_width': 80}) with mock.patch.object(self.gc.metadefs_object, 'get') as mocked_get: expect_object = { 'namespace': 'MyNamespace', 'object': 'MyObject' } mocked_get.return_value = expect_object test_shell.do_md_object_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace', 'MyObject') utils.print_dict.assert_called_once_with(expect_object, 80) def test_do_md_object_property_show(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject', 'property': 'MyProperty', 'max_column_width': 80}) with mock.patch.object(self.gc.metadefs_object, 'get') as mocked_get: expect_object = {'name': 'MyObject', 'properties': { 'MyProperty': {'type': 'string'} }} mocked_get.return_value = expect_object test_shell.do_md_object_property_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace', 'MyObject') utils.print_dict.assert_called_once_with({'type': 'string', 'name': 'MyProperty'}, 80) def test_do_md_object_property_show_non_existing(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject', 'property': 'MyProperty', 'max_column_width': 80}) with mock.patch.object(self.gc.metadefs_object, 'get') as mocked_get: expect_object = {'name': 'MyObject', 'properties': {}} mocked_get.return_value = expect_object self.assertRaises(SystemExit, test_shell.do_md_object_property_show, self.gc, args) mocked_get.assert_called_once_with('MyNamespace', 'MyObject') def test_do_md_object_delete(self): args = self._make_args({'namespace': 'MyNamespace', 'object': 'MyObject'}) with mock.patch.object(self.gc.metadefs_object, 'delete') as mocked_delete: test_shell.do_md_object_delete(self.gc, args) mocked_delete.assert_called_once_with('MyNamespace', 'MyObject') def test_do_md_namespace_objects_delete(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_object, 'delete_all') as mocked_delete_all: test_shell.do_md_namespace_objects_delete(self.gc, args) mocked_delete_all.assert_called_once_with('MyNamespace') def test_do_md_object_list(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_object, 'list') as mocked_list: expect_objects = [{'namespace': 'MyNamespace', 'object': 'MyObject'}] mocked_list.return_value = expect_objects test_shell.do_md_object_list(self.gc, args) mocked_list.assert_called_once_with('MyNamespace') utils.print_list.assert_called_once_with( expect_objects, ['name', 'description'], field_settings={ 'description': {'align': 'l', 'max_width': 50}}) def test_do_md_tag_create(self): args = self._make_args({'namespace': 'MyNamespace', 'name': 'MyTag'}) with mock.patch.object(self.gc.metadefs_tag, 'create') as mocked_create: expect_tag = { 'namespace': 'MyNamespace', 'name': 'MyTag' } mocked_create.return_value = expect_tag test_shell.do_md_tag_create(self.gc, args) mocked_create.assert_called_once_with('MyNamespace', 'MyTag') utils.print_dict.assert_called_once_with(expect_tag) def test_do_md_tag_update(self): args = self._make_args({'namespace': 'MyNamespace', 'tag': 'MyTag', 'name': 'NewTag'}) with mock.patch.object(self.gc.metadefs_tag, 'update') as mocked_update: expect_tag = { 'namespace': 'MyNamespace', 'name': 'NewTag' } mocked_update.return_value = expect_tag test_shell.do_md_tag_update(self.gc, args) mocked_update.assert_called_once_with('MyNamespace', 'MyTag', name='NewTag') utils.print_dict.assert_called_once_with(expect_tag) def test_do_md_tag_show(self): args = self._make_args({'namespace': 'MyNamespace', 'tag': 'MyTag', 'sort_dir': 'desc'}) with mock.patch.object(self.gc.metadefs_tag, 'get') as mocked_get: expect_tag = { 'namespace': 'MyNamespace', 'tag': 'MyTag' } mocked_get.return_value = expect_tag test_shell.do_md_tag_show(self.gc, args) mocked_get.assert_called_once_with('MyNamespace', 'MyTag') utils.print_dict.assert_called_once_with(expect_tag) def test_do_md_tag_delete(self): args = self._make_args({'namespace': 'MyNamespace', 'tag': 'MyTag'}) with mock.patch.object(self.gc.metadefs_tag, 'delete') as mocked_delete: test_shell.do_md_tag_delete(self.gc, args) mocked_delete.assert_called_once_with('MyNamespace', 'MyTag') def test_do_md_namespace_tags_delete(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_tag, 'delete_all') as mocked_delete_all: test_shell.do_md_namespace_tags_delete(self.gc, args) mocked_delete_all.assert_called_once_with('MyNamespace') def test_do_md_tag_list(self): args = self._make_args({'namespace': 'MyNamespace'}) with mock.patch.object(self.gc.metadefs_tag, 'list') as mocked_list: expect_tags = [{'namespace': 'MyNamespace', 'tag': 'MyTag'}] mocked_list.return_value = expect_tags test_shell.do_md_tag_list(self.gc, args) mocked_list.assert_called_once_with('MyNamespace') utils.print_list.assert_called_once_with( expect_tags, ['name'], field_settings={ 'description': {'align': 'l', 'max_width': 50}}) def test_do_md_tag_create_multiple(self): args = self._make_args({'namespace': 'MyNamespace', 'delim': ',', 'names': 'MyTag1, MyTag2', 'append': False}) with mock.patch.object( self.gc.metadefs_tag, 'create_multiple') as mocked_create_tags: expect_tags = [{'tags': [{'name': 'MyTag1'}, {'name': 'MyTag2'}]}] mocked_create_tags.return_value = expect_tags test_shell.do_md_tag_create_multiple(self.gc, args) mocked_create_tags.assert_called_once_with( 'MyNamespace', tags=['MyTag1', 'MyTag2'], append=False) utils.print_list.assert_called_once_with( expect_tags, ['name'], field_settings={ 'description': {'align': 'l', 'max_width': 50}}) def test_do_md_tag_create_multiple_with_append(self): args = self._make_args({'namespace': 'MyNamespace', 'delim': ',', 'names': 'MyTag1, MyTag2', 'append': True}) with mock.patch.object( self.gc.metadefs_tag, 'create_multiple') as mocked_create_tags: expect_tags = [{'tags': [{'name': 'MyTag1'}, {'name': 'MyTag2'}]}] mocked_create_tags.return_value = expect_tags test_shell.do_md_tag_create_multiple(self.gc, args) mocked_create_tags.assert_called_once_with( 'MyNamespace', tags=['MyTag1', 'MyTag2'], append=True) utils.print_list.assert_called_once_with( expect_tags, ['name'], field_settings={ 'description': {'align': 'l', 'max_width': 50}}) def _test_do_cache_list(self, supported=True): args = self._make_args({}) expected_output = { "cached_images": [ { "image_id": "pass", "last_accessed": 0, "last_modified": 0, "size": "fake_size", "hits": "fake_hits", } ], "queued_images": ['fake_image'] } with mock.patch.object(self.gc.cache, 'list') as mocked_cache_list: if supported: mocked_cache_list.return_value = expected_output else: mocked_cache_list.side_effect = exc.HTTPNotImplemented test_shell.do_cache_list(self.gc, args) mocked_cache_list.assert_called() if supported: utils.print_cached_images.assert_called_once_with( expected_output) def test_do_cache_list(self): self._test_do_cache_list() def test_do_cache_list_unsupported(self): self.assertRaises(exc.HTTPNotImplemented, self._test_do_cache_list, supported=False) def test_do_cache_list_endpoint_not_provided(self): args = self._make_args({}) self.gc.endpoint_provided = False with mock.patch('glanceclient.common.utils.exit') as mock_exit: test_shell.do_cache_list(self.gc, args) mock_exit.assert_called_once_with( 'Direct server endpoint needs to be provided. Do ' 'not use loadbalanced or catalog endpoints.') def _test_cache_queue(self, supported=True, forbidden=False,): args = argparse.Namespace(id=['image1']) with mock.patch.object(self.gc.cache, 'queue') as mocked_cache_queue: if supported: mocked_cache_queue.return_value = None else: mocked_cache_queue.side_effect = exc.HTTPNotImplemented if forbidden: mocked_cache_queue.side_effect = exc.HTTPForbidden test_shell.do_cache_queue(self.gc, args) if supported: mocked_cache_queue.assert_called_once_with('image1') def test_do_cache_queue(self): self._test_cache_queue() def test_do_cache_queue_unsupported(self): with mock.patch( 'glanceclient.common.utils.print_err') as mock_print_err: self._test_cache_queue(supported=False) mock_print_err.assert_called_once_with( "'HTTP HTTPNotImplemented': Unable to queue image " "'image1' for caching.") def test_do_cache_queue_forbidden(self): with mock.patch( 'glanceclient.common.utils.print_err') as mock_print_err: self._test_cache_queue(forbidden=True) mock_print_err.assert_called_once_with( "You are not permitted to queue the image 'image1' for " "caching.") def test_do_cache_queue_endpoint_not_provided(self): args = argparse.Namespace(id=['image1']) self.gc.endpoint_provided = False with mock.patch('glanceclient.common.utils.exit') as mock_exit: test_shell.do_cache_queue(self.gc, args) mock_exit.assert_called_once_with( 'Direct server endpoint needs to be provided. Do ' 'not use loadbalanced or catalog endpoints.') def _test_cache_delete(self, supported=True, forbidden=False,): args = argparse.Namespace(id=['image1']) with mock.patch.object(self.gc.cache, 'delete') as mocked_cache_delete: if supported: mocked_cache_delete.return_value = None else: mocked_cache_delete.side_effect = exc.HTTPNotImplemented if forbidden: mocked_cache_delete.side_effect = exc.HTTPForbidden test_shell.do_cache_delete(self.gc, args) if supported: mocked_cache_delete.assert_called_once_with('image1') def test_do_cache_delete(self): self._test_cache_delete() def test_do_cache_delete_unsupported(self): with mock.patch( 'glanceclient.common.utils.print_err') as mock_print_err: self._test_cache_delete(supported=False) mock_print_err.assert_called_once_with( "'HTTP HTTPNotImplemented': Unable to delete image " "'image1' from cache.") def test_do_cache_delete_forbidden(self): with mock.patch( 'glanceclient.common.utils.print_err') as mock_print_err: self._test_cache_delete(forbidden=True) mock_print_err.assert_called_once_with( "You are not permitted to " "delete the image 'image1' from cache.") def test_do_cache_delete_endpoint_not_provided(self): args = argparse.Namespace(id=['image1']) self.gc.endpoint_provided = False with mock.patch('glanceclient.common.utils.exit') as mock_exit: test_shell.do_cache_delete(self.gc, args) mock_exit.assert_called_once_with( 'Direct server endpoint needs to be provided. Do ' 'not use loadbalanced or catalog endpoints.') def _test_cache_clear(self, target='both', supported=True, forbidden=False,): args = self._make_args({'target': target}) with mock.patch.object(self.gc.cache, 'clear') as mocked_cache_clear: if supported: mocked_cache_clear.return_value = None else: mocked_cache_clear.side_effect = exc.HTTPNotImplemented if forbidden: mocked_cache_clear.side_effect = exc.HTTPForbidden test_shell.do_cache_clear(self.gc, args) if supported: mocked_cache_clear.mocked_cache_clear(target) def test_do_cache_clear_all(self): self._test_cache_clear() def test_do_cache_clear_queued_only(self): self._test_cache_clear(target='queue') def test_do_cache_clear_cached_only(self): self._test_cache_clear(target='cache') def test_do_cache_clear_unsupported(self): with mock.patch( 'glanceclient.common.utils.print_err') as mock_print_err: self._test_cache_clear(supported=False) mock_print_err.assert_called_once_with( "'HTTP HTTPNotImplemented': Unable to delete image(s) " "from cache.") def test_do_cache_clear_forbidden(self): with mock.patch( 'glanceclient.common.utils.print_err') as mock_print_err: self._test_cache_clear(forbidden=True) mock_print_err.assert_called_once_with( "You are not permitted to " "delete image(s) from cache.") def test_do_cache_clear_endpoint_not_provided(self): args = self._make_args({'target': 'both'}) self.gc.endpoint_provided = False with mock.patch('glanceclient.common.utils.exit') as mock_exit: test_shell.do_cache_clear(self.gc, args) mock_exit.assert_called_once_with( 'Direct server endpoint needs to be provided. Do ' 'not use loadbalanced or catalog endpoints.') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_tags.py0000664000175000017500000000467400000000000025173 0ustar00zuulzuul00000000000000# 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. import testtools from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import image_tags IMAGE = '3a4560a1-e585-443e-9b39-553b46ec92d1' TAG = 'tag01' data_fixtures = { '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, tag_value=TAG): { 'DELETE': ( {}, None, ), 'PUT': ( {}, { 'image_id': IMAGE, 'tag_value': TAG } ), } } schema_fixtures = { 'tag': { 'GET': ( {}, {'name': 'image', 'properties': {'image_id': {}, 'tags': {}}} ) } } class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(data_fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, image_tags.Controller) def test_update_image_tag(self): image_id = IMAGE tag_value = TAG self.controller.update(image_id, tag_value) expect = [ ('PUT', '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, tag_value=TAG), {}, None)] self.assertEqual(expect, self.api.calls) def test_delete_image_tag(self): image_id = IMAGE tag_value = TAG self.controller.delete(image_id, tag_value) expect = [ ('DELETE', '/v2/images/{image}/tags/{tag_value}'.format(image=IMAGE, tag_value=TAG), {}, None)] self.assertEqual(expect, self.api.calls) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_tasks.py0000664000175000017500000002572200000000000025357 0ustar00zuulzuul00000000000000# Copyright 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. import testtools from glanceclient.tests.unit.v2 import base from glanceclient.tests import utils from glanceclient.v2 import tasks _OWNED_TASK_ID = 'a4963502-acc7-42ba-ad60-5aa0962b7faf' _OWNER_ID = '6bd473f0-79ae-40ad-a927-e07ec37b642f' _FAKE_OWNER_ID = '63e7f218-29de-4477-abdc-8db7c9533188' _PENDING_ID = '3a4560a1-e585-443e-9b39-553b46ec92d1' _PROCESSING_ID = '6f99bf80-2ee6-47cf-acfe-1f1fabb7e810' fixtures = { '/v2/tasks?limit=%d' % tasks.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'tasks': [ { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, { 'id': _PROCESSING_ID, 'type': 'import', 'status': 'processing', }, ]}, ), }, '/v2/tasks?limit=1': { 'GET': ( {}, { 'tasks': [ { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, ], 'next': ('/v2/tasks?limit=1&' 'marker=3a4560a1-e585-443e-9b39-553b46ec92d1'), }, ), }, ('/v2/tasks?limit=1&marker=3a4560a1-e585-443e-9b39-553b46ec92d1'): { 'GET': ( {}, {'tasks': [ { 'id': _PROCESSING_ID, 'type': 'import', 'status': 'pending', }, ]}, ), }, '/v2/tasks/3a4560a1-e585-443e-9b39-553b46ec92d1': { 'GET': ( {}, { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, ), 'PATCH': ( {}, '', ), }, '/v2/tasks/e7e59ff6-fa2e-4075-87d3-1a1398a07dc3': { 'GET': ( {}, { 'id': 'e7e59ff6-fa2e-4075-87d3-1a1398a07dc3', 'type': 'import', 'status': 'pending', }, ), 'PATCH': ( {}, '', ), }, '/v2/tasks': { 'POST': ( {}, { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', 'input': '{"import_from": "file:///", ' '"import_from_format": "qcow2"}' }, ), }, '/v2/tasks?limit=%d&owner=%s' % (tasks.DEFAULT_PAGE_SIZE, _OWNER_ID): { 'GET': ( {}, {'tasks': [ { 'id': _OWNED_TASK_ID, }, ]}, ), }, '/v2/tasks?limit=%d&status=processing' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ { 'id': _OWNED_TASK_ID, }, ]}, ), }, '/v2/tasks?limit=%d&type=import' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ { 'id': _OWNED_TASK_ID, }, ]}, ), }, '/v2/tasks?limit=%d&type=fake' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ ]}, ), }, '/v2/tasks?limit=%d&status=fake' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ ]}, ), }, '/v2/tasks?limit=%d&type=import' % (tasks.DEFAULT_PAGE_SIZE): { 'GET': ( {}, {'tasks': [ { 'id': _OWNED_TASK_ID, }, ]}, ), }, '/v2/tasks?limit=%d&owner=%s' % (tasks.DEFAULT_PAGE_SIZE, _FAKE_OWNER_ID): { 'GET': ({}, {'tasks': []}, ), }, '/v2/tasks?limit=%d&sort_key=type' % tasks.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'tasks': [ { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, { 'id': _PROCESSING_ID, 'type': 'import', 'status': 'processing', }, ]}, ), }, '/v2/tasks?limit=%d&sort_dir=asc&sort_key=id' % tasks.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'tasks': [ { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, { 'id': _PROCESSING_ID, 'type': 'import', 'status': 'processing', }, ]}, ), }, '/v2/tasks?limit=%d&sort_dir=desc&sort_key=id' % tasks.DEFAULT_PAGE_SIZE: { 'GET': ( {}, {'tasks': [ { 'id': _PROCESSING_ID, 'type': 'import', 'status': 'processing', }, { 'id': _PENDING_ID, 'type': 'import', 'status': 'pending', }, ]}, ), }, } schema_fixtures = { 'task': { 'GET': ( {}, { 'name': 'task', 'properties': { 'id': {}, 'type': {}, 'status': {}, 'input': {}, 'result': {}, 'message': {}, }, 'additionalProperties': False, } ) } } class TestController(testtools.TestCase): def setUp(self): super(TestController, self).setUp() self.api = utils.FakeAPI(fixtures) self.schema_api = utils.FakeSchemaAPI(schema_fixtures) self.controller = base.BaseController(self.api, self.schema_api, tasks.Controller) def test_list_tasks(self): tasks = self.controller.list() self.assertEqual(_PENDING_ID, tasks[0].id) self.assertEqual('import', tasks[0].type) self.assertEqual('pending', tasks[0].status) self.assertEqual(_PROCESSING_ID, tasks[1].id) self.assertEqual('import', tasks[1].type) self.assertEqual('processing', tasks[1].status) def test_list_tasks_paginated(self): tasks = self.controller.list(page_size=1) self.assertEqual(_PENDING_ID, tasks[0].id) self.assertEqual('import', tasks[0].type) self.assertEqual(_PROCESSING_ID, tasks[1].id) self.assertEqual('import', tasks[1].type) def test_list_tasks_with_status(self): filters = {'filters': {'status': 'processing'}} tasks = self.controller.list(**filters) self.assertEqual(_OWNED_TASK_ID, tasks[0].id) def test_list_tasks_with_wrong_status(self): filters = {'filters': {'status': 'fake'}} tasks = self.controller.list(**filters) self.assertEqual(0, len(tasks)) def test_list_tasks_with_type(self): filters = {'filters': {'type': 'import'}} tasks = self.controller.list(**filters) self.assertEqual(_OWNED_TASK_ID, tasks[0].id) def test_list_tasks_with_wrong_type(self): filters = {'filters': {'type': 'fake'}} tasks = self.controller.list(**filters) self.assertEqual(0, len(tasks)) def test_list_tasks_for_owner(self): filters = {'filters': {'owner': _OWNER_ID}} tasks = self.controller.list(**filters) self.assertEqual(_OWNED_TASK_ID, tasks[0].id) def test_list_tasks_for_fake_owner(self): filters = {'filters': {'owner': _FAKE_OWNER_ID}} tasks = self.controller.list(**filters) self.assertEqual(tasks, []) def test_list_tasks_filters_encoding(self): filters = {"owner": u"ni\xf1o"} try: self.controller.list(filters=filters) except KeyError: # NOTE(flaper87): It raises KeyError because there's # no fixture supporting this query: # /v2/tasks?owner=ni%C3%B1o&limit=20 # We just want to make sure filters are correctly encoded. pass self.assertEqual(b"ni\xc3\xb1o", filters["owner"]) def test_list_tasks_with_marker(self): tasks = self.controller.list(marker=_PENDING_ID, page_size=1) self.assertEqual(1, len(tasks)) self.assertEqual(_PROCESSING_ID, tasks[0]['id']) def test_list_tasks_with_single_sort_key(self): tasks = self.controller.list(sort_key='type') self.assertEqual(2, len(tasks)) self.assertEqual(_PENDING_ID, tasks[0].id) def test_list_tasks_with_invalid_sort_key(self): self.assertRaises(ValueError, self.controller.list, sort_key='invalid') def test_list_tasks_with_desc_sort_dir(self): tasks = self.controller.list(sort_key='id', sort_dir='desc') self.assertEqual(2, len(tasks)) self.assertEqual(_PENDING_ID, tasks[1].id) def test_list_tasks_with_asc_sort_dir(self): tasks = self.controller.list(sort_key='id', sort_dir='asc') self.assertEqual(2, len(tasks)) self.assertEqual(_PENDING_ID, tasks[0].id) def test_list_tasks_with_invalid_sort_dir(self): self.assertRaises(ValueError, self.controller.list, sort_dir='invalid') def test_get_task(self): task = self.controller.get(_PENDING_ID) self.assertEqual(_PENDING_ID, task.id) self.assertEqual('import', task.type) def test_create_task(self): properties = { 'type': 'import', 'input': {'import_from_format': 'ovf', 'import_from': 'swift://cloud.foo/myaccount/mycontainer/path'}, } task = self.controller.create(**properties) self.assertEqual(_PENDING_ID, task.id) self.assertEqual('import', task.type) def test_create_task_invalid_property(self): properties = { 'type': 'import', 'bad_prop': 'value', } self.assertRaises(TypeError, self.controller.create, **properties) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/v2/test_versions.py0000664000175000017500000000455600000000000026104 0ustar00zuulzuul00000000000000# Copyright 2015 OpenStack Foundation # Copyright 2015 Huawei 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. import testtools from glanceclient.tests import utils from glanceclient.v2 import versions fixtures = { '/versions': { 'GET': ( {}, {"versions": [ { "status": "EXPERIMENTAL", "id": "v3.0", "links": [ { "href": "http://10.229.45.145:9292/v3/", "rel": "self" } ] }, { "status": "CURRENT", "id": "v2.3", "links": [ { "href": "http://10.229.45.145:9292/v2/", "rel": "self" } ] }, { "status": "SUPPORTED", "id": "v1.0", "links": [ { "href": "http://10.229.45.145:9292/v1/", "rel": "self" } ] } ]} ) } } class TestVersions(testtools.TestCase): def setUp(self): super(TestVersions, self).setUp() self.api = utils.FakeAPI(fixtures) self.controller = versions.VersionController(self.api) def test_version_list(self): version = list(self.controller.list()) self.assertEqual('v3.0', version[0]['id']) self.assertEqual('EXPERIMENTAL', version[0]['status']) self.assertEqual([{"href": "http://10.229.45.145:9292/v3/", "rel": "self"}], version[0]['links']) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1688740210.0821638 python-glanceclient-4.4.0/glanceclient/tests/unit/var/0000775000175000017500000000000000000000000023052 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/var/badcert.crt0000664000175000017500000000000000000000000025156 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/var/ca.crt0000664000175000017500000000432000000000000024146 0ustar00zuulzuul00000000000000-----BEGIN CERTIFICATE----- MIIGVTCCBD2gAwIBAgIUEstxpjoCFDZo8K1Mmz7QIpYwSXIwDQYJKoZIhvcNAQEL BQAwgbgxCzAJBgNVBAYTAkFVMREwDwYDVQQIDAhTdGF0ZSBDQTELMAkGA1UEBwwC Q0ExGTAXBgNVBAoMEE9wZW5TdGFjayBDQSBPcmcxGjAYBgNVBAsMEU9wZW5TdGFj ayBUZXN0IENBMS0wKwYDVQQDDCRPcGVuc3RhY2sgVGVzdCBDZXJ0aWZpY2F0ZSBB dXRob3JpdHkxIzAhBgkqhkiG9w0BCQEWFGFkbWluQGNhLmV4YW1wbGUuY29tMCAX DTIwMDQwNzEyMjYwMVoYDzI5OTkwMTAxMTIyNjAxWjCBuDELMAkGA1UEBhMCQVUx ETAPBgNVBAgMCFN0YXRlIENBMQswCQYDVQQHDAJDQTEZMBcGA1UECgwQT3BlblN0 YWNrIENBIE9yZzEaMBgGA1UECwwRT3BlblN0YWNrIFRlc3QgQ0ExLTArBgNVBAMM JE9wZW5zdGFjayBUZXN0IENlcnRpZmljYXRlIEF1dGhvcml0eTEjMCEGCSqGSIb3 DQEJARYUYWRtaW5AY2EuZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4IC DwAwggIKAoICAQD6KfGpVSJsmhTgdbdovaaa3Qe4PeP+Dg9Y7muSggVQVlqUp3YB xUSo1RDoLyu1ci+KrNODr+kkD/Cbo8yJQCxqTzUpCw3tadFhUJOWvPaqdcYTA8R9 p7Xjvw3Me7Q7xr4l0pCUIiz/kwxYxk+GCQyXzpXZm14zz+Qm8gz37eoW2jJfoyzA dB9Tp609Id7C6VHFCWZ2Zsa4+Ua/q+Pn7vLNJ61C2H5sfus8dtcGDzViDWwnWyHw spyR79rciV2yA3xeq09RvIx2SB1tc3S2Rxw7SmKeYcnkv6YplvhIG5QpErvp+URh De2wbbxzjFzJqFQO0Yh8IgTBIvQI02++lA8ZX84UDaxmrT92m8GfqQvb96em/H1k RKJTq0QqSC+BbGDeFxHkuOTJiOZm5Bnivpo0TAPwX6YqpadXARAFWw+fJiHCuFGr 6ltD7zgRnx6SR5WNRNWmTZQNx7wC2Bm0cJ2ec0Asn+bl93RVloaNtbFJhkaN555G GnUDLvxiwIN7aMGviJLte/qIhkKTtxD7zxyk+PQhokPAiz8J9P8INDd3GzMvcRPX ufDoXjSGjSLzjVPMhkFxXaHHylBdEAtHxROKz5wJnHqCnKlyyyv0nGBPQxFjT+rb G0HFn1JjodPBLrwooPttDgkEnq5yBpDkhFuYdZbgwjvQ4p5qrCp3EbDJ/QIDAQAB o1MwUTAdBgNVHQ4EFgQUkfKdH+sf7F/69HDbvtZ/ggVDCt4wHwYDVR0jBBgwFoAU kfKdH+sf7F/69HDbvtZ/ggVDCt4wDwYDVR0TAQH/BAUwAwEB/zANBgkqhkiG9w0B AQsFAAOCAgEAsUIwMa+2lR2a77AaVDDJikt4twylxJudJ4WEerFWOJeshCUCEGZF udlkLDMl9/f7XjpWPH2jc8c1xoxO63GZiLumk8mO9VzleGW6O03fQ9z6pOE1ueSg WnqHQYLbb5KizbiLen4xpa13cjZ2KFKJBkBEn2yOXXSOGP/yuWf1nQ1QumHCFFxf SRrJaaVqR8Ow6yPIjeCFT0IyeoGP5ihlxTvgSo+HeQx1wuYcBIG6clx4BGEgUa2N kaUF6v6EBl/mfX6YkhzvDygaS3men1XRgAkNFPXN9L7XVQdAh2hJSenq73seCJcc lD6Pr4U3VdWaTNYNZqpySspfJ6vp4XGNJFWZiaHPC5CALjgMzljdq9Xohedl2v0i zFZ4T3Zd+RT1941yD5rqlTnaqscpPnZpEUkULjH63v42/vLRSVkZOb6uSdOTL/c3 bxDr4ZbN6cPY5As+XADPgOALuiQql+bRYOZOQL6i5lwtepfvmT6YGZZMk0WKhDcD C/cWX7z7834T2yYez2OmFdTr1UCmGd9IqTTQ01JTgr02y5lCN5J4KlG0SQBnQeQB Pj+gi1WElCIsBIX67WNCss5bpzn+T9cvMD5w2uG94ceT7jbIQ8LQ5x90kn+4HKr0 PnD937DKLY+HPbm5l9CIhmsX+mWUOcqqWSvxBWeJSk4Qk60K3G/oQeY= -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/var/certificate.crt0000664000175000017500000000405600000000000026053 0ustar00zuulzuul00000000000000-----BEGIN CERTIFICATE----- MIIF3TCCA8UCFApiIYk0jePQYtuj9aOTINDiado4MA0GCSqGSIb3DQEBCwUAMIG4 MQswCQYDVQQGEwJBVTERMA8GA1UECAwIU3RhdGUgQ0ExCzAJBgNVBAcMAkNBMRkw FwYDVQQKDBBPcGVuU3RhY2sgQ0EgT3JnMRowGAYDVQQLDBFPcGVuU3RhY2sgVGVz dCBDQTEtMCsGA1UEAwwkT3BlbnN0YWNrIFRlc3QgQ2VydGlmaWNhdGUgQXV0aG9y aXR5MSMwIQYJKoZIhvcNAQkBFhRhZG1pbkBjYS5leGFtcGxlLmNvbTAgFw0yMDA0 MDcxMjMxMjFaGA8yOTk5MDEwMTEyMzEyMVowgZoxCzAJBgNVBAYTAlVTMQswCQYD VQQIDAJDQTEPMA0GA1UEBwwGU3RhdGUxMRswGQYDVQQKDBJPcGVuU3RhY2sgVGVz dCBPcmcxHDAaBgNVBAsME09wZW5TdGFjayBUZXN0IFVuaXQxEDAOBgNVBAMMBzAu MC4wLjAxIDAeBgkqhkiG9w0BCQEWEWFkbWluQGV4YW1wbGUuY29tMIICIjANBgkq hkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAvwpYRmNTJsZASu0XKwcRNRU7JlMlWwFM 3x5qMMb9h77v/CxPbMl4rjdFhPSqHm2Cc5J9dtihfRZFnwmfOlp2lahJMpC6xVad QU5tDChKICTM1MFwjThrK1dK17wIzuOFVCUESWU7JpTTbT7GD05w/kozcEC8IzVu V43TY5srByXtJs8J/m+G7rh2FI1+9a56xAQAlztYp6lWpzZpxohhgt2BFoqNNHal zdTI368+lk/OkzTrQTXnXATZjFAm95q4I3z9uumAJlaUBzf0qNadYPOAKhdLOK1l y4WsmBl9DGhUVTC4177k+gK6sEXIZV3bgAWjhgALF84HqAYVxesrEj64HBFGRxYO iL7+CJQr27MGBsEqqzi7I4BkI2chIoG80XAORH+mGzv4ToB+in2LPNKWAy9A5X7h uszZdg+O/pwjRwTqRpsNLpTQ/eeONuOJmQTlYNwRdNCVRQqkddOiZdP0McEuZw/r b5hgbos/HQnpD1604MNOC2xPK8uqGtHJkDyevRGeOpQH1FyJhWEDNDt/+T1O1C+2 egvM1sOu6bJrrI4oo1Co2x+Fp2/ak/cx72n2+7KgpxnAQRwIpChh54X3MLGr8Zc2 2m+yghIzABiDNW486S4xeCxa07sqOa5noFNt8rj5ylwHWVwmaW0rQxcTS6BKavop D+GsTBM0niECAwEAATANBgkqhkiG9w0BAQsFAAOCAgEA5c6wtD6NX7JxZpSLnm7R AjfEVA1uVWugbkkk9w96r0bWWnMHgJTuDqIrxfXURvHYKsh65BIYfajtlTddaPdx 0j+++8EO5zTzmosARNQ+gxUNZws6/cA8EDsrIRrv6HrO2Y+v0V8ZsaAZCxkC51gh iTW3oMzPrAup7R2Bp0KXbqe+bWUWN7fHUs6klHtYdI1BXBMLn5DJQvGEfXgZnyQI OpJEo5OcVluVx9XbiNN3XpWk77UjoR75CLMdA6s+FA8OL0B86VanjaedOa0ZVOP6 A+GeAvGJ8InYgLDOpDRV4pM8BXEAUJT2c59bVpTjZ3u3VLQ8cXgOqSdM7gBgiYio A7S3yCTaHsMLbRP6iehw/sjyey8VHvvltZ9c9p+aMHpGK202aeCfCNTZx17649Nu 7DVvLKO+zUvlOvW0eEnj/A6U8sZmoSnU2vPq3OIxZWDXihC5lEHpqbw6Qqwbo5Yd T048fo7NlF3fVOh5pjPHPwexlHwDq/MP+xFexDI8sHNQEx7Sc0OpEXZinVSLtQEY Zu0+0U/0wC2XLbyoI0NUIbJHKAjXUYnQnuGkshQFzth2TtRvVsF4rq6ckcSrcxsu x9iKUQkW9f9Okjf7hj1vuv1/ouRHPc9JBaNDHRcHNyRTz6PmBZTWDLxzI3NZ0e0p 9l4gwJ1ojNB2abF4LOEf5bU= -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/var/expired-cert.crt0000664000175000017500000000417300000000000026164 0ustar00zuulzuul00000000000000-----BEGIN CERTIFICATE----- MIIGFTCCA/2gAwIBAgIBATANBgkqhkiG9w0BAQUFADCBuDEZMBcGA1UEChMQT3Bl bnN0YWNrIENBIE9yZzEaMBgGA1UECxMRT3BlbnN0YWNrIFRlc3QgQ0ExIzAhBgkq hkiG9w0BCQEWFGFkbWluQGNhLmV4YW1wbGUuY29tMREwDwYDVQQHEwhTdGF0ZSBD QTELMAkGA1UECBMCQ0ExCzAJBgNVBAYTAkFVMS0wKwYDVQQDEyRPcGVuc3RhY2sg VGVzdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkwHhcNMTIxMTE1MTcwNjMzWhcNMTIx MTE2MTcwNjMzWjCBqDEbMBkGA1UEChMST3BlbnN0YWNrIFRlc3QgT3JnMRwwGgYD VQQLExNPcGVuc3RhY2sgVGVzdCBVbml0MSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl eGFtcGxlLmNvbTEPMA0GA1UEBxMGU3RhdGUxMQswCQYDVQQIEwJDQTELMAkGA1UE BhMCVVMxHjAcBgNVBAMTFW9wZW5zdGFjay5leGFtcGxlLmNvbTCCAiIwDQYJKoZI hvcNAQEBBQADggIPADCCAgoCggIBANn9w82sGN+iALSlZ5/Odd5iJ3MAJ5BoalMG kfUECGMewd7lE5+6ok1+vqVbYjd+F56aSkIJFR/ck51EYG2diGM5E5zjdiLcyB9l dKB5PmaB2P9dHyomy+sMONqhw5uEsWKIfPbtjzGRhjJL0bIYwptGr4JPraZy8R3d HWbTO3SlnFkjHHtfoKuZtRJq5OD1hXM8J9IEsBC90zw7RWCTw1iKllLfKITPUi7O i8ITjUyTVKR2e56XRtmxGgGsGyZpcYrmhRuLo9jyL9m3VuNzsfwDvCqn7cnZIOQa VO4hNZdO+33PINCC+YVNOGYwqfBuKxYvHJSbMfOZ6JDK98v65pWLBN7PObYIjQFH uJyK5DuQMqvyRIcrtfLUalepD+PQaCn4ajgXjpqBz4t0pMte8jh0i4clLwvT0elT PtA+MMos3hIGjJgEHTvLdCff9qlkjHlW7lg45PYn7S0Z7dqtBWD7Ys2B+AWp/skt hRr7YZeegLfHVJVkMFL6Ojs98161W2FLmEA+5nejzjx7kWlJsg9aZPbBnN87m6iK RHI+VkqSpBHm10iMlp4Nn30RtOj0wQhxoZjtEouGeRobHN5ULwpAfNEpKMMZf5bt 604JjOP9Pn+WzsvzGDeXjgxUP55PIR+EpHkvS5h1YQ+9RV5J669e2J9T4gnc0Abg t3jJvtp1AgMBAAGjODA2MDQGA1UdEQQtMCuCEGFsdDEuZXhhbXBsZS5jb22BDm9z QGV4YW1wbGUuY29tggcwLjAuMC4wMA0GCSqGSIb3DQEBBQUAA4ICAQBkKUA4lhsS zjcuh77wtAIP9SN5Se4CheTRDXKDeuwWB6VQDzdJdtqSnWNF6sVEA97vhNTSjaBD hfrtX9FZ+ImADlOf01t4Dakhsmje/DEPiQHaCy9P5fGtGIGRlWUyTmyQoV1LDLM5 wgB1V5Oz2iDat2AdvUb0OFP0O1M887OgPpfUDQJEUTVAs5JS+6P/6RPyFh/dHWiX UGoM0nMvTwsLWT4CZ9NdIChecVwBFqXjNytPY53tKbCWp77d/oGUg5Pb6EBD3xSW AeMJ6PuafDRgm/He8nOtZnUd+53Ha59yzSGnSopu5WqrUa/xD+ZiK6dX7LsH/M8y Hz0rh7w22qNHUxNaC3hrhx1BxX4au6z4kpKXIlAWH7ViRzVZ8XkwqqrndqWPWOFk 1emLLJ1dfT8FXdgpHenkUiktAf5qZhUWbF6nr9at+c4T7ZrLHSekux2r29kD9BJw O2gSSclxKlMPwirUC0P4J/2WP72kCbf6AEfKU2siT12E6/xOmgen9lVYKckBiLbb rJ97L1ieJI8GZTGExjtE9Lo+XVsv28D2XLU8vNCODs0xPZCr2TLNS/6YcnVy6594 vpvU7fbNFAyxG4sjQC0wHoN6rn+kd1kzfprmBHKTx3W7y+hzjb+W7iS2EZn20k+N l3+dFHnWayuCdqcFwIl3m8i8FupFihz9+A== -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/var/privatekey.key0000664000175000017500000000625700000000000025761 0ustar00zuulzuul00000000000000-----BEGIN RSA PRIVATE KEY----- MIIJKgIBAAKCAgEAvwpYRmNTJsZASu0XKwcRNRU7JlMlWwFM3x5qMMb9h77v/CxP bMl4rjdFhPSqHm2Cc5J9dtihfRZFnwmfOlp2lahJMpC6xVadQU5tDChKICTM1MFw jThrK1dK17wIzuOFVCUESWU7JpTTbT7GD05w/kozcEC8IzVuV43TY5srByXtJs8J /m+G7rh2FI1+9a56xAQAlztYp6lWpzZpxohhgt2BFoqNNHalzdTI368+lk/OkzTr QTXnXATZjFAm95q4I3z9uumAJlaUBzf0qNadYPOAKhdLOK1ly4WsmBl9DGhUVTC4 177k+gK6sEXIZV3bgAWjhgALF84HqAYVxesrEj64HBFGRxYOiL7+CJQr27MGBsEq qzi7I4BkI2chIoG80XAORH+mGzv4ToB+in2LPNKWAy9A5X7huszZdg+O/pwjRwTq RpsNLpTQ/eeONuOJmQTlYNwRdNCVRQqkddOiZdP0McEuZw/rb5hgbos/HQnpD160 4MNOC2xPK8uqGtHJkDyevRGeOpQH1FyJhWEDNDt/+T1O1C+2egvM1sOu6bJrrI4o o1Co2x+Fp2/ak/cx72n2+7KgpxnAQRwIpChh54X3MLGr8Zc22m+yghIzABiDNW48 6S4xeCxa07sqOa5noFNt8rj5ylwHWVwmaW0rQxcTS6BKavopD+GsTBM0niECAwEA AQKCAgEAtK70Dp6iZmnbJQJYhzmH7MzHxNee3RO9wMjjZn7OCzVrhPXjqOBkY2Gj PryoqV6pouVKBL2e/s+xyVkwX+Bvh9xCXrDD9SCWWs3yFS2F7iDgGdlaujZCJhvJ jYEqU4Kc95iLFV/JMhRQY2KbsJ5gACHtxJ11U1eVpPlelTaM25XjVnE64opY9C9C fu3Uxkjfk8S1SlO25dwjOMMeB8e1cjBNhyRDqPsOlj5KPkVgzIlut4u1dVemGkH7 /9lPAaAzyFzPHZj6u0fneWxS2d0hvDCRZz3gxxo4zOUA+FojCzkhifEq4eKKbmtm ZpGZl0XN9Kdgobwowbr7Qs9+iFKDyHtcDLFwWh0ShNgagkrmgk/e1MfPWW+6lbTa rqadFaMXdk4dfSaYqF+tJN+xgrtgerLCf8Z4u3eY/RZjwRXyWXZcGxQdfAtfckvl vb1Qv7CnZMIHlHxYiORUQ/ZmYhms/vQp56f7DjVwp5kqg9ONhkSvYQT4Npu7tARO y5r1IoGVXgZJjvD1suN8kOf0FTfAXoJAiwyFT7VZraYf51kHCefLelz1a6VOnK8L r5RIPlbAH/BBots1Gon9BwBzPOBwlrHdQO6CfKW+ypWkAHv5jT4U/xg2hTuFgJIb hargYTPyHswtVNnwBucpoXHyvFzyuju/4dsvxYYIp+ntmHfd+oECggEBAOIgK6u0 wMyT9UFh9Ft4d3vk9AZZkZd9Y/1gN+X8bJFnzi3wuulokppaynAUA7pJnrnQj0OA 3kyEgYh6ugS22+nRTrgyKMXbfi7J4i6teUL7oqi5WukmaGNZfdmYHdcgEX7vvDbi WKEe2KetFvdYCrx6HTBQHxpFiE3jk2jkXygee2lHEwAIasWit3dX3SPAiLJXebqP I1h2QhcHZKGnxgd+ATuyFY/YDObrZkoe9JuiheLXEd+5JshKjTetbs+vYrBaFRio 8JlMrIxsBYj/ALaN/CUqEhoKfxwbHP3mV7xHiUDHwBbvz+DDZiTmSVXA1NIN0b4E izJq/1wnhZ8QUbsCggEBANhHjPRRHQecAWgR3Aehwlrw0H0UXbKaouaZP/OMV5LW sSlzh3SUsxZbz99sXd7/UAqCXpsHhTaCRq8nhorUgOSV1GqeaP89l+bJnNU7rmww TWqosTdTK+T8LGJvO5IwBbCzuYfGWQlvS28877Sbf+w0PfV1jzlHnwFVMaegV2On x2VZ9KGftUUDfgQ7URhMOd1Zm5uHI/oaaFQjGAbgAcXNJe9Giv/hlQEPOXtojBDk 9dFC/WWjesVm1Iv2gthPso2AnXcp8ifUTg0cXgGg3KiGFhNTen1xIe/AvTBasmpQ Ymmx0uq1Y/vsjf9DGyL72Lu6atZwB//8QXoyKdFbM9MCggEBAL2Mag8M/XB/tl6Q Vd03JjFcwpFwE3MBUQfb1/+ZkQhyE4q++G8fkYSCBp/cpyNJAxyPjwfuxmktycc1 2SiKf92H7ozIvxTb4PInmMm38KYNeVQly+cUovxkz/HOaXUjFIdrPkJjihfFW6dy mIXN73H+iukswGWtU4y276JFjN58bsbZJTwp0hbJRzFrHZwSkIOugAO6aM6Gku/q 6pf3ozA0l6QKq7hgSrBnMt9/A1xS6Bg2YG1BLxlGJQo+/1xokDlzyataMhTPCPTM t/cWiup8Kpico3/gvJw6vhq3M2RIMu1yg7q2W3L1WHIl9+NCOSO7Ic4+0M/6kQQW vRORAnECggEBAJP+WfBwdKnZUYkR93rtcF3kPPXp8redYuziXsVb+izLZg0UNdNL UURybMrYj19hWzblwLDas4f6Gz4NkN38zXodIG4YmYZWclQFD6FFpnP3lXHvntxZ uEaHXCO7M4sz+yDPypui2RhApOCoVOpEIYPSt7b3y5qJbL9vuXuXl1Tk4Od0Z5YU /+gKnLduk25J8qqJf5YsIi0o1s0D+pPxwqTEXTnfDoxLozdHYLEWeAmzcpXP/i8H b6IWXEit1RkJaAe1w4pgFIi2mPYVvCnnFjbnEcIFtGKUAIHbZFnrJfzjpoPmn4nl t1YSp5PNKouEw+iphiPYI1FCHtfr7XuJqesCggEALuK1WEx+pDgN8YL89Lg/OGUH jXJ/Es/BJXBcx+MaKuNs47sb8rc3Z7uMqkENRviqMmHn4DjpsnNvyQ8tAP8dOpPW tubKRKC2YLsSju2Qocl+b8D9HMZwoBzzApvJBxPMXNBxikE56nPmlkteNHHeBD+0 oKfpXxjvCyOUwoNllRlw7HIzNxgZOR64tKe8CdFO5Zb/lxxgRy854XoFwaMfAYiR VbgzpHqvoPYC5UwDNQrkFxh5UBc5VlwRSmQ7NTfLFLWA6lT2iNSEZ9U1kVlhIs77 dXoeEoOPy2KRFUcsOrVKEag8zWEogi40dHW1Iw5e4+vbHbF6RAnyQBmrBjIH/A== -----END RSA PRIVATE KEY----- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/var/wildcard-certificate.crt0000664000175000017500000000645400000000000027646 0ustar00zuulzuul00000000000000#Certificate: # Data: # Version: 1 (0x0) # Serial Number: 13493453254446411258 (0xbb42603e589dedfa) # Signature Algorithm: sha1WithRSAEncryption # Issuer: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=*.pong.example.com/emailAddress=admin@example.com # Validity # Not Before: Aug 21 17:29:18 2013 GMT # Not After : Jul 28 17:29:18 2113 GMT # Subject: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=*.pong.example.com/emailAddress=admin@example.com # Subject Public Key Info: # Public Key Algorithm: rsaEncryption # Public-Key: (4096 bit) # Modulus: # 00:d4:bb:3a:c4:a0:06:54:31:23:5d:b0:78:5a:be: # 45:44:ae:a1:89:86:11:d8:ca:a8:33:b0:4f:f3:e1: # 46:1e:85:a3:2a:9c:a4:e0:c2:14:34:4f:91:df:dc: # . # . # . # Exponent: 65537 (0x10001) # Signature Algorithm: sha1WithRSAEncryption # 9f:cc:08:5d:19:ee:54:31:a3:57:d7:3c:89:89:c0:69:41:dd: # 46:f8:73:68:ec:46:b9:fa:f5:df:f6:d9:58:35:d8:53:94:88: # bd:36:a6:23:9e:0c:0d:89:62:35:91:49:b6:14:f4:43:69:3c: # . # . # . -----BEGIN CERTIFICATE----- MIIFyjCCA7ICCQC7QmA+WJ3t+jANBgkqhkiG9w0BAQUFADCBpTELMAkGA1UEBhMC VVMxCzAJBgNVBAgMAkNBMQ8wDQYDVQQHDAZTdGF0ZTExGzAZBgNVBAoMEk9wZW5z dGFjayBUZXN0IE9yZzEcMBoGA1UECwwTT3BlbnN0YWNrIFRlc3QgVW5pdDEbMBkG A1UEAwwSKi5wb25nLmV4YW1wbGUuY29tMSAwHgYJKoZIhvcNAQkBFhFhZG1pbkBl eGFtcGxlLmNvbTAgFw0xMzA4MjExNzI5MThaGA8yMTEzMDcyODE3MjkxOFowgaUx CzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTEPMA0GA1UEBwwGU3RhdGUxMRswGQYD VQQKDBJPcGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsME09wZW5zdGFjayBUZXN0 IFVuaXQxGzAZBgNVBAMMEioucG9uZy5leGFtcGxlLmNvbTEgMB4GCSqGSIb3DQEJ ARYRYWRtaW5AZXhhbXBsZS5jb20wggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK AoICAQDUuzrEoAZUMSNdsHhavkVErqGJhhHYyqgzsE/z4UYehaMqnKTgwhQ0T5Hf 3GmlIBt4I96/3cxj0qSLrdR81fM+5Km8lIlVHwVn1y6LKcMlaUC4K+sgDLcjhZfb f9+fMkcur3WlNzKpAEaIosWwsu6YvYc+W/nPBpKxMbOZ4fZiPMEo8Pxmw7sl/6hn lBOJj7dpZOZpHhVPZgzYNVoyfKCZiwgdxH4JEYa+EQos87+2Nwhs7bCgrTLLppCU vpobwZV5w4O0D6INpUfBmsr4IAuXeFWZa61vZYqhaVbAbTTlUzOLGh7Z2uz9gt75 iSR2J0e2xntVaUIYLIAUNOO2edk8NMAuIOGr2EIyC7i2O/BTti2YjGNO7SsEClxi IFKjYahylHmNrS1Q/oMAcJppmhz+oOCmKOMmAZXYAH1A3gs/sWphJpgv/MWt6Ji2 4VpFaJ+o4bHILlqIpuvL4GLIOkmxVP639khaumgKtgNIUTKJ/V6t/J31WARfxKxl BQTTzV/Be+84YJiiddx8eunU8AorPyAJFzsDPTJpFUB4Q5BwAeDGCySgxJpUqM2M TETBycdiVToM4SWkRsOZgZxQ+AVfkkqDct2Bat2lg9epcIez8PrsohQjQbmiqUUL 2c3de4kLYzIWF8EN3P2Me/7b06jbn4c7Fly/AN6tJOG23BzhHQIDAQABMA0GCSqG SIb3DQEBBQUAA4ICAQCfzAhdGe5UMaNX1zyJicBpQd1G+HNo7Ea5+vXf9tlYNdhT lIi9NqYjngwNiWI1kUm2FPRDaTwC0kLxk5zBPzF7bcf0SwJCeDjmlUpY7YenS0DA XmIbg8FvgOlp69Ikrqz98Y4pB9H4O81WdjxNBBbHjrufAXxZYnh5rXrVsXeSJ8jN MYGWlSv4xwFGfRX53b8VwXFjGjAkH8SQGtRV2w9d0jF8OzFwBA4bKk4EplY0yBPR 2d7Y3RVrDnOVfV13F8CZxJ5fu+6QamUwIaTjpyqflE1L52KTy+vWPYR47H2u2bhD IeZRufJ8adNIOtH32EcENkusQjLrb3cTXGW00TljhFXd22GqL5d740u+GEKHtWh+ 9OKPTMZK8yK7d5EyS2agTVWmXU6HfpAKz9+AEOnVYErpnggNZjkmJ9kD185rGlSZ Vvo429hXoUAHNbd+8zda3ufJnJf5q4ZEl8+hp8xsvraUy83XLroVZRsKceldmAM8 swt6n6w5gRKg4xTH7KFrd+KNptaoY3SsVrnJuaSOPenrUXbZzaI2Q35CId93+8NP mXVIWdPO1msdZNiCYInRIGycK+oifUZPtAaJdErg8rt8NSpHzYKQ0jfjAGiVHBjK s0J2TjoKB3jtlrw2DAmFWKeMGNp//1Rm6kfQCCXWftn+TA7XEJhcjyDBVciugA== -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/unit/var/wildcard-san-certificate.crt0000664000175000017500000000507700000000000030425 0ustar00zuulzuul00000000000000#Certificate: # Data: # Version: 3 (0x2) # Serial Number: 11990626514780340979 (0xa66743493fdcc2f3) # Signature Algorithm: sha1WithRSAEncryption # Issuer: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=0.0.0.0 # Validity # Not Before: Dec 10 15:31:22 2013 GMT # Not After : Nov 16 15:31:22 2113 GMT # Subject: C=US, ST=CA, L=State1, O=Openstack Test Org, OU=Openstack Test Unit, CN=0.0.0.0 # Subject Public Key Info: # Public Key Algorithm: rsaEncryption # Public-Key: (2048 bit) # Modulus: # 00:ca:6b:07:73:53:24:45:74:05:a5:2a:27:bd:3e: # . # . # . # Exponent: 65537 (0x10001) # X509v3 extensions: # X509v3 Key Usage: # Key Encipherment, Data Encipherment # X509v3 Extended Key Usage: # TLS Web Server Authentication # X509v3 Subject Alternative Name: # DNS:foo.example.net, DNS:*.example.com # Signature Algorithm: sha1WithRSAEncryption # 7e:41:69:da:f4:3c:06:d6:83:c6:f2:db:df:37:f1:ac:fa:f5: # . # . # . -----BEGIN CERTIFICATE----- MIIDxDCCAqygAwIBAgIJAKZnQ0k/3MLzMA0GCSqGSIb3DQEBBQUAMHgxCzAJBgNV BAYTAlVTMQswCQYDVQQIEwJDQTEPMA0GA1UEBxMGU3RhdGUxMRswGQYDVQQKExJP cGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsTE09wZW5zdGFjayBUZXN0IFVuaXQx EDAOBgNVBAMTBzAuMC4wLjAwIBcNMTMxMjEwMTUzMTIyWhgPMjExMzExMTYxNTMx MjJaMHgxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJDQTEPMA0GA1UEBxMGU3RhdGUx MRswGQYDVQQKExJPcGVuc3RhY2sgVGVzdCBPcmcxHDAaBgNVBAsTE09wZW5zdGFj ayBUZXN0IFVuaXQxEDAOBgNVBAMTBzAuMC4wLjAwggEiMA0GCSqGSIb3DQEBAQUA A4IBDwAwggEKAoIBAQDKawdzUyRFdAWlKie9Pn10j7frffN+z1gEMluK2CtDEwv9 kbD4uS/Kz4dujfTx03mdyNfiMVlOM+YJm/qeLLSdJyFyvZ9Y3WmJ+vT2RGlMMhLd /wEnMRrTYLL39pwI6z+gyw+4D78Pyv/OXy02IA6WtVEefYSx1vmVngb3pL+iBzhO 8CZXNI6lqrFhh+Hr4iMkYMtY1vTnwezAL6p64E/ZAFNPYCEJlacESTLQ4VZYniHc QTgnE1czlI1vxlIk1KDXAzUGeeopZecRih9qlTxtOpklqEciQEE+sHtPcvyvdRE9 Bdyx5rNSALLIcXs0ViJE1RPlw3fjdBoDIOygqvX1AgMBAAGjTzBNMAsGA1UdDwQE AwIEMDATBgNVHSUEDDAKBggrBgEFBQcDATApBgNVHREEIjAggg9mb28uZXhhbXBs ZS5uZXSCDSouZXhhbXBsZS5jb20wDQYJKoZIhvcNAQEFBQADggEBAH5Badr0PAbW g8by29838az69Raul5IkpZQ5V3O1NaNNWxvmF1q8zFFqqGK5ktXJAwGiwnYEBb30 Zfrr+eFIEERzBthSJkWlP8NG+2ooMyg50femp+asAvW+KYYefJW8KaXTsznMsAFy z1agcWVYVZ4H9PwunEYn/rM1krLEe4Cagsw5nmf8VqZg+hHtw930q8cRzgDsZdfA jVK6dWdmzmLCUTL1GKCeNriDw1jIeFvNufC+Q3orH7xBx4VL+NV5ORWdNY/B8q1b mFHdzbuZX6v39+2ww6aZqG2orfxUocc/5Ox6fXqenKPI3moeHS6Ktesq7sEQSJ6H QZFsTuT/124= -----END CERTIFICATE----- ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/tests/utils.py0000664000175000017500000001534200000000000023022 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. import copy import io import json import testtools from urllib import parse from glanceclient.v2 import schemas class FakeAPI(object): def __init__(self, fixtures): self.fixtures = fixtures self.calls = [] def _request(self, method, url, headers=None, data=None, content_length=None): call = build_call_record(method, sort_url_by_query_keys(url), headers or {}, data) if content_length is not None: call = tuple(list(call) + [content_length]) self.calls.append(call) fixture = self.fixtures[sort_url_by_query_keys(url)][method] data = fixture[1] if isinstance(fixture[1], str): try: data = json.loads(fixture[1]) except ValueError: data = io.StringIO(fixture[1]) return FakeResponse(fixture[0], fixture[1]), data def get(self, *args, **kwargs): return self._request('GET', *args, **kwargs) def post(self, *args, **kwargs): return self._request('POST', *args, **kwargs) def put(self, *args, **kwargs): return self._request('PUT', *args, **kwargs) def patch(self, *args, **kwargs): return self._request('PATCH', *args, **kwargs) def delete(self, *args, **kwargs): return self._request('DELETE', *args, **kwargs) def head(self, *args, **kwargs): return self._request('HEAD', *args, **kwargs) class FakeSchemaAPI(FakeAPI): def get(self, *args, **kwargs): _, raw_schema = self._request('GET', *args, **kwargs) return schemas.Schema(raw_schema) class RawRequest(object): def __init__(self, headers, body=None, version=1.0, status=200, reason="Ok"): """A crafted request object used for testing. :param headers: dict representing HTTP response headers :param body: file-like object :param version: HTTP Version :param status: Response status code :param reason: Status code related message. """ self.body = body self.status = status self.reason = reason self.version = version self.headers = headers def getheaders(self): return copy.deepcopy(self.headers).items() def getheader(self, key, default): return self.headers.get(key, default) def read(self, amt): return self.body.read(amt) class FakeResponse(object): def __init__(self, headers=None, body=None, version=1.0, status_code=200, reason="Ok"): """A crafted response object used for testing. :param headers: dict representing HTTP response headers :param body: file-like object :param version: HTTP Version :param status: Response status code :param reason: Status code related message. """ self.body = body self.reason = reason self.version = version self.headers = headers self.headers['x-openstack-request-id'] = 'req-1234' self.status_code = status_code self.raw = RawRequest(headers, body=body, reason=reason, version=version, status=status_code) @property def status(self): return self.status_code @property def ok(self): return (self.status_code < 400 or self.status_code >= 600) def read(self, amt): return self.body.read(amt) def close(self): pass @property def content(self): if hasattr(self.body, "read"): return self.body.read() return self.body @property def text(self): if isinstance(self.content, bytes): return self.content.decode('utf-8') return self.content def json(self, **kwargs): return self.body and json.loads(self.text) or "" def iter_content(self, chunk_size=1, decode_unicode=False): while True: chunk = self.raw.read(chunk_size) if not chunk: break yield chunk def release_conn(self, **kwargs): pass class TestCase(testtools.TestCase): TEST_REQUEST_BASE = { 'config': {'danger_mode': False}, 'verify': True} class FakeTTYStdout(io.StringIO): """A Fake stdout that try to emulate a TTY device as much as possible.""" def isatty(self): return True def write(self, data): # When a CR (carriage return) is found reset file. if data.startswith('\r'): self.seek(0) data = data[1:] return io.StringIO.write(self, data) class FakeNoTTYStdout(FakeTTYStdout): """A Fake stdout that is not a TTY device.""" def isatty(self): return False def sort_url_by_query_keys(url): """A helper function which sorts the keys of the query string of a url. For example, an input of '/v2/tasks?sort_key=id&sort_dir=asc&limit=10' returns '/v2/tasks?limit=10&sort_dir=asc&sort_key=id'. This is to prevent non-deterministic ordering of the query string causing problems with unit tests. :param url: url which will be ordered by query keys :returns url: url with ordered query keys """ parsed = parse.urlparse(url) queries = parse.parse_qsl(parsed.query, True) sorted_query = sorted(queries, key=lambda x: x[0]) encoded_sorted_query = parse.urlencode(sorted_query, True) url_parts = (parsed.scheme, parsed.netloc, parsed.path, parsed.params, encoded_sorted_query, parsed.fragment) return parse.urlunparse(url_parts) def build_call_record(method, url, headers, data): """Key the request body be ordered if it's a dict type.""" if isinstance(data, dict): data = sorted(data.items()) if isinstance(data, str): # NOTE(flwang): For image update, the data will be a 'list' which # contains operation dict, such as: [{"op": "remove", "path": "/a"}] try: data = json.loads(data) except ValueError: return (method, url, headers or {}, data) data = [sorted(d.items()) for d in data] return (method, url, headers or {}, data) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1688740210.0821638 python-glanceclient-4.4.0/glanceclient/v1/0000775000175000017500000000000000000000000020467 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v1/__init__.py0000664000175000017500000000126200000000000022601 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 glanceclient.v1.client import Client # noqa ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1688740210.0861638 python-glanceclient-4.4.0/glanceclient/v1/apiclient/0000775000175000017500000000000000000000000022437 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v1/apiclient/__init__.py0000664000175000017500000000000000000000000024536 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v1/apiclient/base.py0000664000175000017500000004166600000000000023740 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. """ ######################################################################## # # THIS MODULE IS DEPRECATED # # Please refer to # https://etherpad.openstack.org/p/kilo-glanceclient-library-proposals for # the discussion leading to this deprecation. # # We recommend checking out the python-openstacksdk project # (https://launchpad.net/python-openstacksdk) instead. # ######################################################################## # E1102: %s is not callable # pylint: disable=E1102 import abc import copy from oslo_utils import strutils import urllib.parse from glanceclient._i18n import _ from glanceclient.v1.apiclient import exceptions 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. """ try: if obj.uuid: return obj.uuid except AttributeError: pass try: return obj.id except AttributeError: return 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=None, 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'. If response_key is None - all response body will be used. :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] if response_key is not None else body # 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=None): """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'. If response_key is None - all response body will be used. """ body = self.client.get(url).json() data = body[response_key] if response_key is not None else body return self.resource_class(self, data, 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=None, 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., 'server'. If response_key is None - all response body will be used. :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() data = body[response_key] if response_key is not None else body if return_raw: return data return self.resource_class(self, data) 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'. If response_key is None - all response body will be used. """ 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'. If response_key is None - all response body will be used. """ 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 %(name)s matching %(args)s.") % { 'name': self.resource_class.__name__, 'args': 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) query = urllib.parse.urlencode(kwargs) if kwargs else '', return self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), 'query': '?%s' % query, }, 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) query = urllib.parse.urlencode(kwargs) if kwargs else '', rl = self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), 'query': '?%s' % query, }, self.collection_key) num = len(rl) if num == 0: msg = _("No %(name)s matching %(args)s.") % { 'name': self.resource_class.__name__, 'args': kwargs } raise exceptions.NotFound(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 Resource(object): """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): """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 """ self.manager = manager self._info = info self._add_details(info) self._loaded = loaded def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') 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.HUMAN_ID: name = getattr(self, self.NAME_ATTR, None) if name is not None: return strutils.to_slug(name) return None def _add_details(self, info): for (k, v) in info.items(): try: setattr(self, k, v) self._info[k] = v except AttributeError: # In this case we already defined the attribute on the class pass def __getattr__(self, k): if k not in self.__dict__: # NOTE(bcwaldon): disallow lazy-loading if already loaded once if not self.is_loaded(): self.get() return self.__getattr__(k) raise AttributeError(k) else: return self.__dict__[k] def get(self): """Support for lazy loading details. Some clients, such as novaclient have the option to lazy load the details, details which can be loaded with this function. """ # 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) self._add_details( {'x_request_id': self.manager.client.last_request_id}) 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 is_loaded(self): return self._loaded def set_loaded(self, val): self._loaded = val def to_dict(self): return copy.deepcopy(self._info) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v1/apiclient/exceptions.py0000664000175000017500000003122300000000000025173 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. """ ######################################################################## # # THIS MODULE IS DEPRECATED # # Please refer to # https://etherpad.openstack.org/p/kilo-glanceclient-library-proposals for # the discussion leading to this deprecation. # # We recommend checking out the python-openstacksdk project # (https://launchpad.net/python-openstacksdk) instead. # ######################################################################## import inspect import sys from glanceclient._i18n import _ class ClientException(Exception): """The base exception class for all exceptions this library raises.""" pass 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 ConnectionError(ClientException): """Cannot connect to API service.""" pass class ConnectionRefused(ConnectionError): """Connection refused while trying to 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 an AuthSystem that is not installed.""" def __init__(self, auth_system): super(AuthSystemNotFound, self).__init__( _("AuthSystemNotFound: %r") % 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: %r") % 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 HTTPRedirection(HttpError): """HTTP Redirection.""" message = _("HTTP Redirection") 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 MultipleChoices(HTTPRedirection): """HTTP 300 - Multiple Choices. Indicates multiple options for the resource that the client may follow. """ http_status = 300 message = _("Multiple Choices") 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 """ req_id = response.headers.get("x-openstack-request-id") # NOTE(hdd) true for older versions of nova and cinder if not req_id: req_id = response.headers.get("x-compute-request-id") kwargs = { "http_status": response.status_code, "response": response, "method": method, "url": url, "request_id": req_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 isinstance(body, dict): error = body.get(list(body)[0]) if isinstance(error, dict): kwargs["message"] = (error.get("message") or error.get("faultstring")) kwargs["details"] = (error.get("details") or str(body)) elif content_type.startswith("text/"): kwargs["details"] = getattr(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=1688740181.0 python-glanceclient-4.4.0/glanceclient/v1/apiclient/utils.py0000664000175000017500000000623100000000000024153 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. ######################################################################## # # THIS MODULE IS DEPRECATED # # Please refer to # https://etherpad.openstack.org/p/kilo-glanceclient-library-proposals for # the discussion leading to this deprecation. # # We recommend checking out the python-openstacksdk project # (https://launchpad.net/python-openstacksdk) instead. # ######################################################################## from oslo_utils import encodeutils from oslo_utils import uuidutils from glanceclient._i18n import _ from glanceclient.v1.apiclient import exceptions def find_resource(manager, name_or_id, **find_args): """Look for resource in a given manager. Used as a helper for the _find_* methods. Example: .. code-block:: python def _find_hypervisor(cs, hypervisor): #Get a hypervisor by name or ID. return cliutils.find_resource(cs.hypervisors, hypervisor) """ # first try to get entity as integer id try: return manager.get(int(name_or_id)) except (TypeError, ValueError, exceptions.NotFound): pass # now try to get entity as uuid try: tmp_id = encodeutils.safe_decode(name_or_id) if uuidutils.is_uuid_like(tmp_id): return manager.get(tmp_id) except (TypeError, ValueError, exceptions.NotFound): pass # for str id which is not uuid if getattr(manager, 'is_alphanum_id_allowed', False): try: return manager.get(name_or_id) except exceptions.NotFound: pass try: try: return manager.find(human_id=name_or_id, **find_args) except exceptions.NotFound: pass # finally try to find entity by name try: resource = getattr(manager, 'resource_class', None) name_attr = resource.NAME_ATTR if resource else 'name' kwargs = {name_attr: name_or_id} kwargs.update(find_args) return manager.find(**kwargs) except exceptions.NotFound: msg = _("No %(name)s with a name or " "ID of '%(name_or_id)s' exists.") % { "name": manager.resource_class.__name__.lower(), "name_or_id": name_or_id } raise exceptions.CommandError(msg) except exceptions.NoUniqueMatch: msg = _("Multiple %(name)s matches found for " "'%(name_or_id)s', use an ID to be more specific.") % { "name": manager.resource_class.__name__.lower(), "name_or_id": name_or_id } raise exceptions.CommandError(msg) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v1/client.py0000664000175000017500000000340700000000000022323 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 glanceclient.common import http from glanceclient.common import utils from glanceclient.v1 import image_members from glanceclient.v1 import images from glanceclient.v1 import versions class Client(object): """Client for the OpenStack Images v1 API. :param string endpoint: A user-supplied endpoint URL for the glance service. :param string token: Token for authentication. :param integer timeout: Allows customization of the timeout for client http requests. (optional) :param string language_header: Set Accept-Language header to be sent in requests to glance. """ def __init__(self, endpoint=None, **kwargs): """Initialize a new client for the Images v1 API.""" endpoint, self.version = utils.endpoint_version_from_url(endpoint, 1.0) self.http_client = http.get_http_client(endpoint=endpoint, **kwargs) self.images = images.ImageManager(self.http_client) self.image_members = image_members.ImageMemberManager(self.http_client) self.versions = versions.VersionManager(self.http_client) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v1/image_members.py0000664000175000017500000000677200000000000023651 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 glanceclient.v1.apiclient import base class ImageMember(base.Resource): def __repr__(self): return "" % self._info @property def id(self): return self.member_id def delete(self): self.manager.delete(self) class ImageMemberManager(base.ManagerWithFind): resource_class = ImageMember def get(self, image, member_id): image_id = base.getid(image) url = '/v1/images/%s/members/%s' % (image_id, member_id) resp, body = self.client.get(url) member = body['member'] member['image_id'] = image_id return ImageMember(self, member, loaded=True) def list(self, image=None, member=None): out = [] if image and member: try: out.append(self.get(image, member)) # TODO(bcwaldon): narrow this down to 404 except Exception: pass elif image: out.extend(self._list_by_image(image)) elif member: out.extend(self._list_by_member(member)) else: # TODO(bcwaldon): figure out what is appropriate to do here as we # are unable to provide the requested response pass return out def _list_by_image(self, image): image_id = base.getid(image) url = '/v1/images/%s/members' % image_id resp, body = self.client.get(url) out = [] for member in body['members']: member['image_id'] = image_id out.append(ImageMember(self, member, loaded=True)) return out def _list_by_member(self, member): member_id = base.getid(member) url = '/v1/shared-images/%s' % member_id resp, body = self.client.get(url) out = [] for member in body['shared_images']: member['member_id'] = member_id out.append(ImageMember(self, member, loaded=True)) return out def delete(self, image_id, member_id): self._delete("/v1/images/%s/members/%s" % (image_id, member_id)) def create(self, image, member_id, can_share=False): """Creates an image.""" url = '/v1/images/%s/members/%s' % (base.getid(image), member_id) body = {'member': {'can_share': can_share}} self.client.put(url, data=body) def replace(self, image, members): memberships = [] for member in members: try: obj = { 'member_id': member.member_id, 'can_share': member.can_share, } except AttributeError: obj = {'member_id': member['member_id']} if 'can_share' in member: obj['can_share'] = member['can_share'] memberships.append(obj) url = '/v1/images/%s/members' % base.getid(image) self.client.put(url, data={'memberships': memberships}) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v1/images.py0000664000175000017500000003327600000000000022321 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. import copy from oslo_utils import encodeutils from oslo_utils import strutils import urllib.parse from glanceclient.common import utils from glanceclient.v1.apiclient import base UPDATE_PARAMS = ('name', 'disk_format', 'container_format', 'min_disk', 'min_ram', 'owner', 'size', 'is_public', 'protected', 'location', 'checksum', 'copy_from', 'properties', # NOTE(bcwaldon: an attempt to update 'deleted' will be # ignored, but we need to support it for backwards- # compatibility with the legacy client library 'deleted') CREATE_PARAMS = UPDATE_PARAMS + ('id', 'store') DEFAULT_PAGE_SIZE = 20 SORT_DIR_VALUES = ('asc', 'desc') SORT_KEY_VALUES = ('name', 'status', 'container_format', 'disk_format', 'size', 'id', 'created_at', 'updated_at') OS_REQ_ID_HDR = 'x-openstack-request-id' class Image(base.Resource): def __repr__(self): return "" % self._info def update(self, **fields): self.manager.update(self, **fields) def delete(self, **kwargs): return self.manager.delete(self) def data(self, **kwargs): return self.manager.data(self, **kwargs) class ImageManager(base.ManagerWithFind): resource_class = Image def _list(self, url, response_key, obj_class=None, body=None): resp, body = self.client.get(url) if obj_class is None: obj_class = self.resource_class data = body[response_key] return ([obj_class(self, res, loaded=True) for res in data if res], resp) def _image_meta_from_headers(self, headers): meta = {'properties': {}} safe_decode = encodeutils.safe_decode for key, value in headers.items(): # NOTE(flaper87): this is a compatibility fix # for urllib3 >= 1.11. Please, refer to this # bug for more info: # https://bugs.launchpad.net/python-glanceclient/+bug/1487645 key = key.lower() value = safe_decode(value, incoming='utf-8') if key.startswith('x-image-meta-property-'): _key = safe_decode(key[22:], incoming='utf-8') meta['properties'][_key] = value elif key.startswith('x-image-meta-'): _key = safe_decode(key[13:], incoming='utf-8') meta[_key] = value for key in ['is_public', 'protected', 'deleted']: if key in meta: meta[key] = strutils.bool_from_string(meta[key]) return self._format_image_meta_for_user(meta) def _image_meta_to_headers(self, fields): headers = {} fields_copy = copy.deepcopy(fields) # NOTE(flaper87): Convert to str, headers # that are not instance of basestring. All # headers will be encoded later, before the # request is sent. def to_str(value): if not isinstance(value, str): return str(value) return value for key, value in fields_copy.pop('properties', {}).items(): headers['x-image-meta-property-%s' % key] = to_str(value) for key, value in fields_copy.items(): headers['x-image-meta-%s' % key] = to_str(value) return headers @staticmethod def _format_image_meta_for_user(meta): for key in ['size', 'min_ram', 'min_disk']: if key in meta: try: meta[key] = int(meta[key]) if meta[key] else 0 except ValueError: pass return meta def get(self, image, **kwargs): """Get the metadata for a specific image. :param image: image object or id to look up :rtype: :class:`Image` """ image_id = base.getid(image) resp, body = self.client.head('/v1/images/%s' % urllib.parse.quote(str(image_id))) meta = self._image_meta_from_headers(resp.headers) return_request_id = kwargs.get('return_req_id', None) if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) return Image(self, meta) def data(self, image, do_checksum=True, **kwargs): """Get the raw data for a specific image. :param image: image object or id to look up :param do_checksum: Enable/disable checksum validation :rtype: iterable containing image data """ image_id = base.getid(image) resp, body = self.client.get('/v1/images/%s' % urllib.parse.quote(str(image_id))) content_length = int(resp.headers.get('content-length', 0)) checksum = resp.headers.get('x-image-meta-checksum', None) if do_checksum and checksum is not None: body = utils.integrity_iter(body, checksum) return_request_id = kwargs.get('return_req_id', None) if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) return utils.IterableWithLength(body, content_length) def _build_params(self, parameters): params = {'limit': parameters.get('page_size', DEFAULT_PAGE_SIZE)} if 'marker' in parameters: params['marker'] = parameters['marker'] sort_key = parameters.get('sort_key') if sort_key is not None: if sort_key in SORT_KEY_VALUES: params['sort_key'] = sort_key else: raise ValueError('sort_key must be one of the following: %s.' % ', '.join(SORT_KEY_VALUES)) sort_dir = parameters.get('sort_dir') if sort_dir is not None: if sort_dir in SORT_DIR_VALUES: params['sort_dir'] = sort_dir else: raise ValueError('sort_dir must be one of the following: %s.' % ', '.join(SORT_DIR_VALUES)) filters = parameters.get('filters', {}) properties = filters.pop('properties', {}) for key, value in properties.items(): params['property-%s' % key] = value params.update(filters) if parameters.get('owner') is not None: params['is_public'] = None if 'is_public' in parameters: params['is_public'] = parameters['is_public'] return params def list(self, **kwargs): """Get a list of images. :param page_size: number of items to request in each paginated request :param limit: maximum number of images to return :param marker: begin returning images that appear later in the image list than that represented by this image id :param filters: dict of direct comparison filters that mimics the structure of an image object :param owner: If provided, only images with this owner (tenant id) will be listed. An empty string ('') matches ownerless images. :param return_req_id: If an empty list is provided, populate this list with the request ID value from the header x-openstack-request-id :rtype: list of :class:`Image` """ absolute_limit = kwargs.get('limit') page_size = kwargs.get('page_size', DEFAULT_PAGE_SIZE) owner = kwargs.get('owner', None) def filter_owner(owner, image): # If client side owner 'filter' is specified # only return images that match 'owner'. if owner is None: # Do not filter based on owner return False if (not hasattr(image, 'owner')) or image.owner is None: # ownerless image return not (owner == '') else: return not (image.owner == owner) def paginate(qp, return_request_id=None): for param, value in qp.items(): if isinstance(value, str): # Note(flaper87) Url encoding should # be moved inside http utils, at least # shouldn't be here. # # Making sure all params are str before # trying to encode them qp[param] = encodeutils.safe_decode(value) url = '/v1/images/detail?%s' % urllib.parse.urlencode(qp) images, resp = self._list(url, "images") if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) for image in images: yield image return_request_id = kwargs.get('return_req_id', None) params = self._build_params(kwargs) seen = 0 while True: seen_last_page = 0 filtered = 0 for image in paginate(params, return_request_id): last_image = image.id if filter_owner(owner, image): # Note(kragniz): ignore this image filtered += 1 continue if (absolute_limit is not None and seen + seen_last_page >= absolute_limit): # Note(kragniz): we've seen enough images return else: seen_last_page += 1 yield image seen += seen_last_page if seen_last_page + filtered == 0: # Note(kragniz): we didn't get any images in the last page return if absolute_limit is not None and seen >= absolute_limit: # Note(kragniz): reached the limit of images to return return if page_size and seen_last_page + filtered < page_size: # Note(kragniz): we've reached the last page of the images return # Note(kragniz): there are more images to come params['marker'] = last_image seen_last_page = 0 def delete(self, image, **kwargs): """Delete an image.""" url = "/v1/images/%s" % base.getid(image) resp, body = self.client.delete(url) return_request_id = kwargs.get('return_req_id', None) if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) def create(self, **kwargs): """Create an image. TODO(bcwaldon): document accepted params """ image_data = kwargs.pop('data', None) if image_data is not None: image_size = utils.get_file_size(image_data) if image_size is not None: kwargs.setdefault('size', image_size) fields = {} for field in kwargs: if field in CREATE_PARAMS: fields[field] = kwargs[field] elif field == 'return_req_id': continue else: msg = 'create() got an unexpected keyword argument \'%s\'' raise TypeError(msg % field) copy_from = fields.pop('copy_from', None) hdrs = self._image_meta_to_headers(fields) if copy_from is not None: hdrs['x-glance-api-copy-from'] = copy_from resp, body = self.client.post('/v1/images', headers=hdrs, data=image_data) return_request_id = kwargs.get('return_req_id', None) if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) return Image(self, self._format_image_meta_for_user(body['image'])) def update(self, image, **kwargs): """Update an image. TODO(bcwaldon): document accepted params """ image_data = kwargs.pop('data', None) if image_data is not None: image_size = utils.get_file_size(image_data) if image_size is not None: kwargs.setdefault('size', image_size) hdrs = {} purge_props = 'false' purge_props_bool = kwargs.pop('purge_props', None) if purge_props_bool: purge_props = 'true' hdrs['x-glance-registry-purge-props'] = purge_props fields = {} for field in kwargs: if field in UPDATE_PARAMS: fields[field] = kwargs[field] elif field == 'return_req_id': continue else: msg = 'update() got an unexpected keyword argument \'%s\'' raise TypeError(msg % field) copy_from = fields.pop('copy_from', None) hdrs.update(self._image_meta_to_headers(fields)) if copy_from is not None: hdrs['x-glance-api-copy-from'] = copy_from url = '/v1/images/%s' % base.getid(image) resp, body = self.client.put(url, headers=hdrs, data=image_data) return_request_id = kwargs.get('return_req_id', None) if return_request_id is not None: return_request_id.append(resp.headers.get(OS_REQ_ID_HDR, None)) return Image(self, self._format_image_meta_for_user(body['image'])) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v1/shell.py0000664000175000017500000004466400000000000022166 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. import copy import functools import os import sys from oslo_utils import encodeutils from oslo_utils import strutils from glanceclient.common import progressbar from glanceclient.common import utils from glanceclient import exc import glanceclient.v1.images CONTAINER_FORMATS = ('Acceptable formats: ami, ari, aki, bare, ovf, ova,' 'docker.') DISK_FORMATS = ('Acceptable formats: ami, ari, aki, vhd, vhdx, vmdk, raw, ' 'qcow2, vdi, iso, and ploop.') DATA_FIELDS = ('location', 'copy_from', 'file') _bool_strict = functools.partial(strutils.bool_from_string, strict=True) @utils.arg('--name', metavar='', help='Filter images to those that have this name.') @utils.arg('--status', metavar='', help='Filter images to those that have this status.') @utils.arg('--changes-since', metavar='', help='Filter images to those that changed since the given time' ', which will include the deleted images.') @utils.arg('--container-format', metavar='', help='Filter images to those that have this container format. ' + CONTAINER_FORMATS) @utils.arg('--disk-format', metavar='', help='Filter images to those that have this disk format. ' + DISK_FORMATS) @utils.arg('--size-min', metavar='', type=int, help='Filter images to those with a size greater than this.') @utils.arg('--size-max', metavar='', type=int, help='Filter images to those with a size less than this.') @utils.arg('--property-filter', metavar='', help="Filter images by a user-defined image property.", action='append', dest='properties', default=[]) @utils.arg('--page-size', metavar='', default=None, type=int, help='Number of images to request in each paginated request.') @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--sort-key', default='name', choices=glanceclient.v1.images.SORT_KEY_VALUES, help='Sort image list by specified field.') @utils.arg('--sort-dir', default='asc', choices=glanceclient.v1.images.SORT_DIR_VALUES, help='Sort image list in specified direction.') @utils.arg('--is-public', type=_bool_strict, metavar='{True,False}', help=('Allows the user to select a listing of public or non ' 'public images.')) @utils.arg('--owner', default=None, metavar='', help='Display only images owned by this tenant id. Filtering ' 'occurs on the client side so may be inefficient. This option ' 'is mainly intended for admin use. Use an empty string (\'\') ' 'to list images with no owner. Note: This option overrides ' 'the --is-public argument if present. Note: the v2 API ' 'supports more efficient server-side owner based filtering.') @utils.arg('--all-tenants', action='store_true', default=False, help=('Allows the admin user to list all images ' 'irrespective of the image\'s owner or is_public value.')) def do_image_list(gc, args): """List images you can access.""" filter_keys = ['name', 'status', 'container_format', 'disk_format', 'size_min', 'size_max', 'is_public', 'changes_since'] filter_items = [(key, getattr(args, key)) for key in filter_keys] filters = dict([item for item in filter_items if item[1] is not None]) if 'changes_since' in filters: filters['changes-since'] = filters.pop('changes_since') if args.properties: property_filter_items = [p.split('=', 1) for p in args.properties] if any(len(pair) != 2 for pair in property_filter_items): utils.exit('Argument --property-filter requires properties in the' ' format KEY=VALUE') filters['properties'] = dict(property_filter_items) kwargs = {'filters': filters} if args.page_size is not None: kwargs['page_size'] = args.page_size kwargs['sort_key'] = args.sort_key kwargs['sort_dir'] = args.sort_dir kwargs['owner'] = args.owner if args.all_tenants is True: kwargs['is_public'] = None images = gc.images.list(**kwargs) if args.human_readable: def convert_size(image): image.size = utils.make_size_human_readable(image.size) return image images = (convert_size(image) for image in images) columns = ['ID', 'Name', 'Disk Format', 'Container Format', 'Size', 'Status'] utils.print_list(images, columns) def _image_show(image, human_readable=False, max_column_width=80): # Flatten image properties dict for display info = copy.deepcopy(image._info) if human_readable: info['size'] = utils.make_size_human_readable(info['size']) for (k, v) in info.pop('properties').items(): info['Property \'%s\'' % k] = v utils.print_dict(info, max_column_width=max_column_width) def _set_data_field(fields, args): if 'location' not in fields and 'copy_from' not in fields: fields['data'] = utils.get_data_file(args) @utils.arg('image', metavar='', help='Name or ID of image to describe.') @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--max-column-width', metavar='', default=80, help='The max column width of the printed table.') def do_image_show(gc, args): """Describe a specific image.""" image_id = utils.find_resource(gc.images, args.image).id image = gc.images.get(image_id) _image_show(image, args.human_readable, max_column_width=int(args.max_column_width)) @utils.arg('--file', metavar='', help='Local file to save downloaded image data to. ' 'If this is not specified and there is no redirection ' 'the image data will not be saved.') @utils.arg('image', metavar='', help='Name or ID of image to download.') @utils.arg('--progress', action='store_true', default=False, help='Show download progress bar.') def do_image_download(gc, args): """Download a specific image.""" image = utils.find_resource(gc.images, args.image) body = image.data() if args.progress: body = progressbar.VerboseIteratorWrapper(body, len(body)) if not (sys.stdout.isatty() and args.file is None): utils.save_image(body, args.file) else: print('No redirection or local file specified for downloaded image ' 'data. Please specify a local file with --file to save ' 'downloaded image or redirect output to another source.') @utils.arg('--id', metavar='', help='ID of image to reserve.') @utils.arg('--name', metavar='', help='Name of image.') @utils.arg('--store', metavar='', help='Store to upload image to.') @utils.arg('--disk-format', metavar='', help='Disk format of image. ' + DISK_FORMATS) @utils.arg('--container-format', metavar='', help='Container format of image. ' + CONTAINER_FORMATS) @utils.arg('--owner', metavar='', help='Tenant who should own image.') @utils.arg('--size', metavar='', type=int, help=('Size of image data (in bytes). Only used with' ' \'--location\' and \'--copy_from\'.')) @utils.arg('--min-disk', metavar='', type=int, help='Minimum size of disk needed to boot image (in gigabytes).') @utils.arg('--min-ram', metavar='', type=int, help='Minimum amount of ram needed to boot image (in megabytes).') @utils.arg('--location', metavar='', help=('URL where the data for this image already resides. For ' 'example, if the image data is stored in swift, you could ' 'specify \'swift+http://tenant%%3Aaccount:key@auth_url/' 'v2.0/container/obj\'. ' '(Note: \'%%3A\' is \':\' URL encoded.)')) @utils.arg('--file', metavar='', help=('Local file that contains disk image to be uploaded during' ' creation. Alternatively, images can be passed to the client' ' via stdin.')) @utils.arg('--checksum', metavar='', help=('Hash of image data used Glance can use for verification.' ' Provide a md5 checksum here.')) @utils.arg('--copy-from', metavar='', help=('Similar to \'--location\' in usage, but this indicates that' ' the Glance server should immediately copy the data and' ' store it in its configured image store.')) @utils.arg('--is-public', type=_bool_strict, metavar='{True,False}', help='Make image accessible to the public.') @utils.arg('--is-protected', type=_bool_strict, metavar='{True,False}', help='Prevent image from being deleted.') @utils.arg('--property', metavar="", action='append', default=[], help=("Arbitrary property to associate with image. " "May be used multiple times.")) @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--progress', action='store_true', default=False, help='Show upload progress bar.') @utils.on_data_require_fields(DATA_FIELDS) def do_image_create(gc, args): """Create a new image.""" # Filter out None values fields = dict(filter(lambda x: x[1] is not None, vars(args).items())) fields['is_public'] = fields.get('is_public') if 'is_protected' in fields: fields['protected'] = fields.pop('is_protected') raw_properties = fields.pop('property') fields['properties'] = {} for datum in raw_properties: key, value = datum.split('=', 1) fields['properties'][key] = value # Filter out values we can't use CREATE_PARAMS = glanceclient.v1.images.CREATE_PARAMS fields = dict(filter(lambda x: x[0] in CREATE_PARAMS, fields.items())) _set_data_field(fields, args) # Only show progress bar for local image files if fields.get('data') and args.progress: filesize = utils.get_file_size(fields['data']) if filesize is not None: # NOTE(kragniz): do not show a progress bar if the size of the # input is unknown (most likely a piped input) fields['data'] = progressbar.VerboseFileWrapper( fields['data'], filesize ) image = gc.images.create(**fields) _image_show(image, args.human_readable) def _is_image_data_provided(args): """Return True if some image data has probably been provided by the user""" # NOTE(kragniz): Check stdin works, then check is there is any data # on stdin or a filename has been provided with --file try: os.fstat(0) except OSError: return False return not sys.stdin.isatty() or args.file or args.copy_from @utils.arg('image', metavar='', help='Name or ID of image to modify.') @utils.arg('--name', metavar='', help='Name of image.') @utils.arg('--disk-format', metavar='', help='Disk format of image. ' + DISK_FORMATS) @utils.arg('--container-format', metavar='', help='Container format of image. ' + CONTAINER_FORMATS) @utils.arg('--owner', metavar='', help='Tenant who should own image.') @utils.arg('--size', metavar='', type=int, help='Size of image data (in bytes).') @utils.arg('--min-disk', metavar='', type=int, help='Minimum size of disk needed to boot image (in gigabytes).') @utils.arg('--min-ram', metavar='', type=int, help='Minimum amount of ram needed to boot image (in megabytes).') @utils.arg('--location', metavar='', help=('URL where the data for this image already resides. For ' 'example, if the image data is stored in swift, you could ' 'specify \'swift+http://tenant%%3Aaccount:key@auth_url/' 'v2.0/container/obj\'. ' '(Note: \'%%3A\' is \':\' URL encoded.) ' 'This option only works for images in \'queued\' status.')) @utils.arg('--file', metavar='', help=('Local file that contains disk image to be uploaded during' ' update. Alternatively, images can be passed to the client' ' via stdin.')) @utils.arg('--checksum', metavar='', help='Hash of image data used Glance can use for verification.') @utils.arg('--copy-from', metavar='', help=('Similar to \'--location\' in usage, but this indicates that' ' the Glance server should immediately copy the data and' ' store it in its configured image store.' ' This option only works for images in \'queued\' status.')) @utils.arg('--is-public', type=_bool_strict, metavar='{True,False}', help='Make image accessible to the public.') @utils.arg('--is-protected', type=_bool_strict, metavar='{True,False}', help='Prevent image from being deleted.') @utils.arg('--property', metavar="", action='append', default=[], help=("Arbitrary property to associate with image. " "May be used multiple times.")) @utils.arg('--purge-props', action='store_true', default=False, help=("If this flag is present, delete all image properties " "not explicitly set in the update request. Otherwise, " "those properties not referenced are preserved.")) @utils.arg('--human-readable', action='store_true', default=False, help='Print image size in a human-friendly format.') @utils.arg('--progress', action='store_true', default=False, help='Show upload progress bar.') def do_image_update(gc, args): """Update a specific image.""" # Filter out None values fields = dict(filter(lambda x: x[1] is not None, vars(args).items())) image_arg = fields.pop('image') image = utils.find_resource(gc.images, image_arg) if 'is_protected' in fields: fields['protected'] = fields.pop('is_protected') raw_properties = fields.pop('property') fields['properties'] = {} for datum in raw_properties: key, value = datum.split('=', 1) fields['properties'][key] = value # Filter out values we can't use UPDATE_PARAMS = glanceclient.v1.images.UPDATE_PARAMS fields = dict(filter(lambda x: x[0] in UPDATE_PARAMS, fields.items())) if image.status == 'queued': _set_data_field(fields, args) if args.progress: filesize = utils.get_file_size(fields['data']) fields['data'] = progressbar.VerboseFileWrapper( fields['data'], filesize ) elif _is_image_data_provided(args): # NOTE(kragniz): Exit with an error if the status is not queued # and image data was provided utils.exit('Unable to upload image data to an image which ' 'is %s.' % image.status) image = gc.images.update(image, purge_props=args.purge_props, **fields) _image_show(image, args.human_readable) @utils.arg('images', metavar='', nargs='+', help='Name or ID of image(s) to delete.') def do_image_delete(gc, args): """Delete specified image(s).""" for args_image in args.images: image = utils.find_resource(gc.images, args_image) if image and image.status == "deleted": msg = "No image with an ID of '%s' exists." % image.id raise exc.CommandError(msg) try: if args.verbose: print('Requesting image delete for %s ...' % encodeutils.safe_decode(args_image), end=' ') gc.images.delete(image) if args.verbose: print('[Done]') except exc.HTTPException as e: if args.verbose: print('[Fail]') print('%s: Unable to delete image %s' % (e, args_image)) @utils.arg('--image-id', metavar='', help='Filter results by an image ID.') @utils.arg('--tenant-id', metavar='', help='Filter results by a tenant ID.') def do_member_list(gc, args): """Describe sharing permissions by image or tenant.""" if args.image_id and args.tenant_id: utils.exit('Unable to filter members by both --image-id and' ' --tenant-id.') elif args.image_id: kwargs = {'image': args.image_id} elif args.tenant_id: kwargs = {'member': args.tenant_id} else: utils.exit('Unable to list all members. Specify --image-id or' ' --tenant-id') members = gc.image_members.list(**kwargs) columns = ['Image ID', 'Member ID', 'Can Share'] utils.print_list(members, columns) @utils.arg('image', metavar='', help='Image to add member to.') @utils.arg('tenant_id', metavar='', help='Tenant to add as member.') @utils.arg('--can-share', action='store_true', default=False, help='Allow the specified tenant to share this image.') def do_member_create(gc, args): """Share a specific image with a tenant.""" image = utils.find_resource(gc.images, args.image) gc.image_members.create(image, args.tenant_id, args.can_share) @utils.arg('image', metavar='', help='Image from which to remove member.') @utils.arg('tenant_id', metavar='', help='Tenant to remove as member.') def do_member_delete(gc, args): """Remove a shared image from a tenant.""" image_id = utils.find_resource(gc.images, args.image).id gc.image_members.delete(image_id, args.tenant_id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v1/versions.py0000664000175000017500000000162700000000000022717 0ustar00zuulzuul00000000000000# Copyright 2015 OpenStack Foundation # Copyright 2015 Huawei 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 glanceclient.v1.apiclient import base class VersionManager(base.ManagerWithFind): def list(self): """List all versions.""" url = '/versions' resp, body = self.client.get(url) return body.get('versions', None) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1688740210.0861638 python-glanceclient-4.4.0/glanceclient/v2/0000775000175000017500000000000000000000000020470 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/__init__.py0000664000175000017500000000117100000000000022601 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from glanceclient.v2.client import Client # noqa ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/cache.py0000664000175000017500000000414400000000000022110 0ustar00zuulzuul00000000000000# Copyright 2021 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 glanceclient.common import utils from glanceclient import exc TARGET_VALUES = ('both', 'cache', 'queue') class Controller(object): def __init__(self, http_client): self.http_client = http_client def is_supported(self, version): if utils.has_version(self.http_client, version): return True else: raise exc.HTTPNotImplemented( 'Glance does not support image caching API (v2.14)') @utils.add_req_id_to_object() def list(self): if self.is_supported('v2.14'): url = '/v2/cache' resp, body = self.http_client.get(url) return body, resp @utils.add_req_id_to_object() def delete(self, image_id): if self.is_supported('v2.14'): resp, body = self.http_client.delete('/v2/cache/%s' % image_id) return body, resp @utils.add_req_id_to_object() def clear(self, target): if self.is_supported('v2.14'): url = '/v2/cache' headers = {} if target != "both": headers = {'x-image-cache-clear-target': target} resp, body = self.http_client.delete(url, headers=headers) return body, resp @utils.add_req_id_to_object() def queue(self, image_id): if self.is_supported('v2.14'): url = '/v2/cache/%s' % image_id resp, body = self.http_client.put(url) return body, resp ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/client.py0000664000175000017500000000570200000000000022324 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 glanceclient.common import http from glanceclient.common import utils from glanceclient.v2 import cache from glanceclient.v2 import image_members from glanceclient.v2 import image_tags from glanceclient.v2 import images from glanceclient.v2 import info from glanceclient.v2 import metadefs from glanceclient.v2 import schemas from glanceclient.v2 import tasks from glanceclient.v2 import versions class Client(object): """Client for the OpenStack Images v2 API. :param string endpoint: A user-supplied endpoint URL for the glance service. :param string token: Token for authentication. :param integer timeout: Allows customization of the timeout for client http requests. (optional) :param string language_header: Set Accept-Language header to be sent in requests to glance. """ def __init__(self, endpoint=None, **kwargs): self.endpoint_provided = endpoint is not None endpoint, self.version = utils.endpoint_version_from_url(endpoint, 2.0) self.http_client = http.get_http_client(endpoint=endpoint, **kwargs) self.schemas = schemas.Controller(self.http_client) self.images = images.Controller(self.http_client, self.schemas) self.image_tags = image_tags.Controller(self.http_client, self.schemas) self.image_members = image_members.Controller(self.http_client, self.schemas) self.info = info.Controller(self.http_client, self.schemas) self.tasks = tasks.Controller(self.http_client, self.schemas) self.metadefs_resource_type = ( metadefs.ResourceTypeController(self.http_client, self.schemas)) self.metadefs_property = ( metadefs.PropertyController(self.http_client, self.schemas)) self.metadefs_object = ( metadefs.ObjectController(self.http_client, self.schemas)) self.metadefs_tag = ( metadefs.TagController(self.http_client, self.schemas)) self.metadefs_namespace = ( metadefs.NamespaceController(self.http_client, self.schemas)) self.versions = versions.VersionController(self.http_client) self.cache = cache.Controller(self.http_client) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/image_members.py0000664000175000017500000000462400000000000023644 0ustar00zuulzuul00000000000000# 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. import warlock from glanceclient.common import utils from glanceclient.v2 import schemas MEMBER_STATUS_VALUES = ('accepted', 'rejected', 'pending') class Controller(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('member') return warlock.model_factory(schema.raw(), schemas.SchemaBasedModel) @utils.add_req_id_to_generator() def list(self, image_id): url = '/v2/images/%s/members' % image_id resp, body = self.http_client.get(url) for member in body['members']: yield self.model(member), resp @utils.add_req_id_to_object() def get(self, image_id, member_id): url = '/v2/images/%s/members/%s' % (image_id, member_id) resp, member = self.http_client.get(url) return self.model(member), resp @utils.add_req_id_to_object() def delete(self, image_id, member_id): resp, body = self.http_client.delete('/v2/images/%s/members/%s' % (image_id, member_id)) return (resp, body), resp @utils.add_req_id_to_object() def update(self, image_id, member_id, member_status): url = '/v2/images/%s/members/%s' % (image_id, member_id) body = {'status': member_status} resp, updated_member = self.http_client.put(url, data=body) return self.model(updated_member), resp @utils.add_req_id_to_object() def create(self, image_id, member_id): url = '/v2/images/%s/members' % image_id body = {'member': member_id} resp, created_member = self.http_client.post(url, data=body) return self.model(created_member), resp ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/image_schema.py0000664000175000017500000001766600000000000023464 0ustar00zuulzuul00000000000000# Copyright 2015 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. _doc_url = "https://docs.openstack.org/python-glanceclient/latest/cli/property-keys.html" # noqa # NOTE(flaper87): Keep a copy of the current default schema so that # we can react on cases where there's no connection to an OpenStack # deployment. See #1481729 _BASE_SCHEMA = { "additionalProperties": { "type": "string" }, "name": "image", "links": [{ "href": "{self}", "rel": "self" }, { "href": "{file}", "rel": "enclosure" }, { "href": "{schema}", "rel": "describedby" }], "properties": { "container_format": { "enum": [None, "ami", "ari", "aki", "bare", "ovf", "ova", "docker"], "type": ["null", "string"], "description": "Format of the container" }, "min_ram": { "type": "integer", "description": "Amount of ram (in MB) required to boot image." }, "ramdisk_id": { "pattern": ("^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}" "-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}" "-([0-9a-fA-F]){12}$"), "type": ["null", "string"], "description": ("ID of image stored in Glance that should be " "used as the ramdisk when booting an AMI-style " "image."), "is_base": False }, "locations": { "items": { "required": ["url", "metadata"], "type": "object", "properties": { "url": { "type": "string", "maxLength": 255 }, "metadata": { "type": "object" } } }, "type": "array", "description": ("A set of URLs to access the image " "file kept in external store") }, "file": { "readOnly": True, "type": "string", "description": "An image file url" }, "owner": { "type": ["null", "string"], "description": "Owner of the image", "maxLength": 255 }, "id": { "pattern": ("^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}" "-([0-9a-fA-F]){4}-([0-9a-fA-F]){4}" "-([0-9a-fA-F]){12}$"), "type": "string", "description": "An identifier for the image" }, "size": { "readOnly": True, "type": ["null", "integer"], "description": "Size of image file in bytes" }, "os_distro": { "type": "string", "description": ("Common name of operating system distribution " "as specified in %s" % _doc_url), "is_base": False }, "self": { "readOnly": True, "type": "string", "description": "An image self url" }, "disk_format": { "enum": [None, "ami", "ari", "aki", "vhd", "vhdx", "vmdk", "raw", "qcow2", "vdi", "iso", "ploop"], "type": ["null", "string"], "description": "Format of the disk" }, "os_version": { "type": "string", "description": "Operating system version as specified by the" " distributor", "is_base": False }, "direct_url": { "readOnly": True, "type": "string", "description": "URL to access the image file kept in external" " store" }, "schema": { "readOnly": True, "type": "string", "description": "An image schema url" }, "status": { "readOnly": True, "enum": ["queued", "saving", "active", "killed", "deleted", "uploading", "importing", "pending_delete", "deactivated"], "type": "string", "description": "Status of the image" }, "tags": { "items": { "type": "string", "maxLength": 255 }, "type": "array", "description": "List of strings related to the image" }, "kernel_id": { "pattern": "^([0-9a-fA-F]){8}-([0-9a-fA-F]){4}-([0-9a-fA-F])" "{4}-([0-9a-fA-F]){4}-([0-9a-fA-F]){12}$", "type": ["null", "string"], "description": "ID of image stored in Glance that should be " "used as the kernel when booting an " "AMI-style image.", "is_base": False }, "visibility": { "enum": ["public", "private", "community", "shared"], "type": "string", "description": "Scope of image accessibility" }, "updated_at": { "readOnly": True, "type": "string", "description": "Date and time of the last image modification" }, "min_disk": { "type": "integer", "description": "Amount of disk space (in GB) required to boot " "image." }, "virtual_size": { "readOnly": True, "type": ["null", "integer"], "description": "Virtual size of image in bytes" }, "instance_uuid": { "type": "string", "description": "Metadata which can be used to record which " "instance this image is associated with. " "(Informational only, does not create an " "instance snapshot.)", "is_base": False }, "name": { "type": ["null", "string"], "description": "Descriptive name for the image", "maxLength": 255 }, "checksum": { "readOnly": True, "type": ["null", "string"], "description": "md5 hash of image contents.", "maxLength": 32 }, "os_hash_algo": { "readOnly": True, "type": ["null", "string"], "description": "Algorithm to calculate os_hash_value", "maxLength": 32 }, "os_hash_value": { "readOnly": True, "type": ["null", "string"], "description": "Hexdigest of the image contents using the " "algorithm specified by the os_hash_algo", "maxLength": 32 }, "created_at": { "readOnly": True, "type": "string", "description": "Date and time of image registration" }, "protected": { "type": "boolean", "description": "If true, image will not be deletable." }, "os_hidden": { "type": "boolean", "description": "If true, image will not appear in default " "image list response." }, "architecture": { "type": "string", "description": ("Operating system architecture as specified " "in %s" % _doc_url), "is_base": False } } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/image_tags.py0000664000175000017500000000357400000000000023153 0ustar00zuulzuul00000000000000# 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. import warlock from glanceclient.common import utils from glanceclient.v2 import schemas class Controller(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('image') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def update(self, image_id, tag_value): """Update an image with the given tag. :param image_id: image to be updated with the given tag. :param tag_value: value of the tag. """ url = '/v2/images/%s/tags/%s' % (image_id, tag_value) resp, body = self.http_client.put(url) return (resp, body), resp @utils.add_req_id_to_object() def delete(self, image_id, tag_value): """Delete the tag associated with the given image. :param image_id: Image whose tag to be deleted. :param tag_value: tag value to be deleted. """ url = '/v2/images/%s/tags/%s' % (image_id, tag_value) resp, body = self.http_client.delete(url) return (resp, body), resp ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/images.py0000664000175000017500000005533300000000000022320 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. import hashlib import json from oslo_utils import encodeutils from requests import codes import urllib.parse import warlock from glanceclient.common import utils from glanceclient import exc from glanceclient.v2 import schemas DEFAULT_PAGE_SIZE = 200 SORT_DIR_VALUES = ('asc', 'desc') SORT_KEY_VALUES = ('name', 'status', 'container_format', 'disk_format', 'size', 'id', 'created_at', 'updated_at') class Controller(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('image') warlock_model = warlock.model_factory( schema.raw(), base_class=schemas.SchemaBasedModel) return warlock_model @utils.memoized_property def unvalidated_model(self): """A model which does not validate the image against the v2 schema.""" schema = self.schema_client.get('image') warlock_model = warlock.model_factory( schema.raw(), base_class=schemas.SchemaBasedModel) warlock_model.validate = lambda *args, **kwargs: None return warlock_model @staticmethod def _wrap(value): if isinstance(value, str): return [value] return value @staticmethod def _validate_sort_param(sort): """Validates sorting argument for invalid keys and directions values. :param sort: comma-separated list of sort keys with optional <:dir> after each key """ for sort_param in sort.strip().split(','): key, _sep, dir = sort_param.partition(':') if dir and dir not in SORT_DIR_VALUES: msg = ('Invalid sort direction: %(sort_dir)s.' ' It must be one of the following: %(available)s.' ) % {'sort_dir': dir, 'available': ', '.join(SORT_DIR_VALUES)} raise exc.HTTPBadRequest(msg) if key not in SORT_KEY_VALUES: msg = ('Invalid sort key: %(sort_key)s.' ' It must be one of the following: %(available)s.' ) % {'sort_key': key, 'available': ', '.join(SORT_KEY_VALUES)} raise exc.HTTPBadRequest(msg) return sort @utils.add_req_id_to_generator() def list(self, **kwargs): """Retrieve a listing of Image objects. :param page_size: Number of images to request in each paginated request. :returns: generator over list of Images. """ limit = kwargs.get('limit') # NOTE(flaper87): Don't use `get('page_size', DEFAULT_SIZE)` otherwise, # it could be possible to send invalid data to the server by passing # page_size=None. page_size = kwargs.get('page_size') or DEFAULT_PAGE_SIZE def paginate(url, page_size, limit=None): next_url = url req_id_hdr = {} while True: if limit and page_size > limit: # NOTE(flaper87): Avoid requesting 2000 images when limit # is 1 next_url = next_url.replace("limit=%s" % page_size, "limit=%s" % limit) resp, body = self.http_client.get(next_url, headers=req_id_hdr) # NOTE(rsjethani): Store curent request id so that it can be # used in subsequent requests. Refer bug #1525259 req_id_hdr['x-openstack-request-id'] = \ utils._extract_request_id(resp) for image in body['images']: # NOTE(bcwaldon): remove 'self' for now until we have # an elegant way to pass it into the model constructor # without conflict. image.pop('self', None) # We do not validate the model when listing. # This prevents side-effects of injecting invalid # schema values via v1. yield self.unvalidated_model(**image), resp if limit: limit -= 1 if limit <= 0: return try: next_url = body['next'] except KeyError: return filters = kwargs.get('filters', {}) # NOTE(flaper87): We paginate in the client, hence we use # the page_size as Glance's limit. filters['limit'] = page_size tags = filters.pop('tag', []) tags_url_params = [] for tag in tags: if not isinstance(tag, str): raise exc.HTTPBadRequest("Invalid tag value %s" % tag) tags_url_params.append({'tag': encodeutils.safe_encode(tag)}) for param, value in filters.items(): if isinstance(value, str): filters[param] = encodeutils.safe_encode(value) url = '/v2/images?%s' % urllib.parse.urlencode(filters) for param in tags_url_params: url = '%s&%s' % (url, urllib.parse.urlencode(param)) if 'sort' in kwargs: if 'sort_key' in kwargs or 'sort_dir' in kwargs: raise exc.HTTPBadRequest("The 'sort' argument is not supported" " with 'sort_key' or 'sort_dir'.") url = '%s&sort=%s' % (url, self._validate_sort_param( kwargs['sort'])) else: sort_dir = self._wrap(kwargs.get('sort_dir', [])) sort_key = self._wrap(kwargs.get('sort_key', [])) if len(sort_key) != len(sort_dir) and len(sort_dir) > 1: raise exc.HTTPBadRequest( "Unexpected number of sort directions: " "either provide a single sort direction or an equal " "number of sort keys and sort directions.") for key in sort_key: url = '%s&sort_key=%s' % (url, key) for dir in sort_dir: url = '%s&sort_dir=%s' % (url, dir) if isinstance(kwargs.get('marker'), str): url = '%s&marker=%s' % (url, kwargs['marker']) for image, resp in paginate(url, page_size, limit): yield image, resp @utils.add_req_id_to_object() def _get(self, image_id, header=None): url = '/v2/images/%s' % image_id header = header or {} resp, body = self.http_client.get(url, headers=header) # NOTE(bcwaldon): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict body.pop('self', None) return self.unvalidated_model(**body), resp def get(self, image_id): return self._get(image_id) @utils.add_req_id_to_object() def get_associated_image_tasks(self, image_id): """Get the tasks associated with an image. :param image_id: ID of the image :raises: exc.HTTPNotImplemented if Glance is not new enough to support this API (v2.12). """ # NOTE (abhishekk): Verify that /v2i/images/%s/tasks is supported by # glance if utils.has_version(self.http_client, 'v2.12'): url = '/v2/images/%s/tasks' % image_id resp, body = self.http_client.get(url) body.pop('self', None) return body, resp else: raise exc.HTTPNotImplemented( 'This operation is not supported by Glance.') @utils.add_req_id_to_object() def data(self, image_id, do_checksum=True, allow_md5_fallback=False): """Retrieve data of an image. When do_checksum is enabled, validation proceeds as follows: 1. if the image has a 'os_hash_value' property, the algorithm specified in the image's 'os_hash_algo' property will be used to validate against the 'os_hash_value' value. If the specified hash algorithm is not available AND allow_md5_fallback is True, then continue to step #2 2. else if the image has a checksum property, MD5 is used to validate against the 'checksum' value. (If MD5 is not available to the client, the download fails.) 3. else if the download response has a 'content-md5' header, MD5 is used to validate against the header value. (If MD5 is not available to the client, the download fails.) 4. if none of 1-3 obtain, the data is **not validated** (this is compatible with legacy behavior) :param image_id: ID of the image to download :param do_checksum: Enable/disable checksum validation :param allow_md5_fallback: Use the MD5 checksum for validation if the algorithm specified by the image's 'os_hash_algo' property is not available :returns: An iterable body or ``None`` """ if do_checksum: # doing this first to prevent race condition if image record # is deleted during the image download url = '/v2/images/%s' % image_id resp, image_meta = self.http_client.get(url) meta_checksum = image_meta.get('checksum', None) meta_hash_value = image_meta.get('os_hash_value', None) meta_hash_algo = image_meta.get('os_hash_algo', None) url = '/v2/images/%s/file' % image_id resp, body = self.http_client.get(url) if resp.status_code == codes.no_content: return None, resp checksum = resp.headers.get('content-md5', None) content_length = int(resp.headers.get('content-length', 0)) check_md5sum = do_checksum if do_checksum and meta_hash_value is not None: try: hasher = hashlib.new(str(meta_hash_algo)) body = utils.serious_integrity_iter(body, hasher, meta_hash_value) check_md5sum = False except ValueError as ve: if (str(ve).startswith('unsupported hash type') and allow_md5_fallback): check_md5sum = True else: raise if do_checksum and check_md5sum: if meta_checksum is not None: body = utils.integrity_iter(body, meta_checksum) elif checksum is not None: body = utils.integrity_iter(body, checksum) else: # NOTE(rosmaita): this preserves legacy behavior to return the # image data when checksumming is requested but there's no # 'content-md5' header in the response. Just want to make it # clear that we're doing this on purpose. pass return utils.IterableWithLength(body, content_length), resp @utils.add_req_id_to_object() def upload(self, image_id, image_data, image_size=None, u_url=None, backend=None): """Upload the data for an image. :param image_id: ID of the image to upload data for. :param image_data: File-like object supplying the data to upload. :param image_size: Unused - present for backwards compatibility :param u_url: Upload url to upload the data to. :param backend: Backend store to upload image to. """ url = u_url or '/v2/images/%s/file' % image_id hdrs = {'Content-Type': 'application/octet-stream'} if backend is not None: hdrs['x-image-meta-store'] = backend body = image_data resp, body = self.http_client.put(url, headers=hdrs, data=body) return (resp, body), resp @utils.add_req_id_to_object() def get_import_info(self): """Get Import info from discovery endpoint.""" url = '/v2/info/import' resp, body = self.http_client.get(url) return body, resp @utils.add_req_id_to_object() def get_stores_info(self): """Get available stores info from discovery endpoint.""" url = '/v2/info/stores' resp, body = self.http_client.get(url) return body, resp @utils.add_req_id_to_object() def get_stores_info_detail(self): """Get available stores info from discovery endpoint.""" url = '/v2/info/stores/detail' resp, body = self.http_client.get(url) return body, resp @utils.add_req_id_to_object() def delete_from_store(self, store_id, image_id): """Delete image data from specific store.""" url = ('/v2/stores/%(store)s/%(image)s' % {'store': store_id, 'image': image_id}) resp, body = self.http_client.delete(url) return body, resp @utils.add_req_id_to_object() def stage(self, image_id, image_data, image_size=None): """Upload the data to image staging. :param image_id: ID of the image to upload data for. :param image_data: File-like object supplying the data to upload. :param image_size: Unused - present for backwards compatibility """ url = '/v2/images/%s/stage' % image_id resp, body = self.upload(image_id, image_data, u_url=url) return body, resp @utils.add_req_id_to_object() def image_import(self, image_id, method='glance-direct', uri=None, remote_region=None, remote_image_id=None, remote_service_interface=None, backend=None, stores=None, allow_failure=True, all_stores=None): """Import Image via method.""" headers = {} url = '/v2/images/%s/import' % image_id data = {'method': {'name': method}} if stores: data['stores'] = stores if allow_failure: data['all_stores_must_succeed'] = False if backend is not None: headers['x-image-meta-store'] = backend if all_stores: data['all_stores'] = True if allow_failure: data['all_stores_must_succeed'] = False if remote_region and remote_image_id: if remote_service_interface: data['method']['glance_service_interface'] = \ remote_service_interface data['method']['glance_region'] = remote_region data['method']['glance_image_id'] = remote_image_id if uri: if method == 'web-download': data['method']['uri'] = uri else: raise exc.HTTPBadRequest('URI is only supported with method: ' '"web-download"') resp, body = self.http_client.post(url, data=data, headers=headers) return body, resp @utils.add_req_id_to_object() def delete(self, image_id): """Delete an image.""" url = '/v2/images/%s' % image_id resp, body = self.http_client.delete(url) return (resp, body), resp @utils.add_req_id_to_object() def create(self, **kwargs): """Create an image.""" headers = {} url = '/v2/images' backend = kwargs.pop('backend', None) if backend is not None: headers['x-image-meta-store'] = backend image = self.model() for (key, value) in kwargs.items(): try: setattr(image, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) resp, body = self.http_client.post(url, headers=headers, data=image) # NOTE(esheffield): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_object() def deactivate(self, image_id): """Deactivate an image.""" url = '/v2/images/%s/actions/deactivate' % image_id resp, body = self.http_client.post(url) return (resp, body), resp @utils.add_req_id_to_object() def reactivate(self, image_id): """Reactivate an image.""" url = '/v2/images/%s/actions/reactivate' % image_id resp, body = self.http_client.post(url) return (resp, body), resp def update(self, image_id, remove_props=None, **kwargs): """Update attributes of an image. :param image_id: ID of the image to modify. :param remove_props: List of property names to remove :param kwargs: Image attribute names and their new values. """ unvalidated_image = self.get(image_id) image = self.model(**unvalidated_image) for (key, value) in kwargs.items(): try: setattr(image, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) if remove_props: cur_props = image.keys() new_props = kwargs.keys() # NOTE(esheffield): Only remove props that currently exist on the # image and are NOT in the properties being updated / added props_to_remove = set(cur_props).intersection( set(remove_props).difference(new_props)) for key in props_to_remove: delattr(image, key) url = '/v2/images/%s' % image_id hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'} resp, _ = self.http_client.patch(url, headers=hdrs, data=image.patch) # Get request id from `patch` request so it can be passed to the # following `get` call req_id_hdr = { 'x-openstack-request-id': utils._extract_request_id(resp)} # NOTE(bcwaldon): calling image.patch doesn't clear the changes, so # we need to fetch the image again to get a clean history. This is # an obvious optimization for warlock return self._get(image_id, req_id_hdr) def _get_image_with_locations_or_fail(self, image_id): image = self.get(image_id) if getattr(image, 'locations', None) is None: raise exc.HTTPBadRequest('The administrator has disabled ' 'API access to image locations') return image @utils.add_req_id_to_object() def _send_image_update_request(self, image_id, patch_body): url = '/v2/images/%s' % image_id hdrs = {'Content-Type': 'application/openstack-images-v2.1-json-patch'} resp, body = self.http_client.patch(url, headers=hdrs, data=json.dumps(patch_body)) return (resp, body), resp def add_location(self, image_id, url, metadata, validation_data=None): """Add a new location entry to an image's list of locations. It is an error to add a URL that is already present in the list of locations. :param image_id: ID of image to which the location is to be added. :param url: URL of the location to add. :param metadata: Metadata associated with the location. :param validation_data: Validation data for the image. :returns: The updated image """ add_patch = [{'op': 'add', 'path': '/locations/-', 'value': {'url': url, 'metadata': metadata}}] if validation_data: add_patch[0]['value']['validation_data'] = validation_data response = self._send_image_update_request(image_id, add_patch) # Get request id from the above update request and pass the same to # following get request req_id_hdr = {'x-openstack-request-id': response.request_ids[0]} return self._get(image_id, req_id_hdr) def delete_locations(self, image_id, url_set): """Remove one or more location entries of an image. :param image_id: ID of image from which locations are to be removed. :param url_set: set of URLs of location entries to remove. :returns: None """ image = self._get_image_with_locations_or_fail(image_id) current_urls = [l['url'] for l in image.locations] missing_locs = url_set.difference(set(current_urls)) if missing_locs: raise exc.HTTPNotFound('Unknown URL(s): %s' % list(missing_locs)) # NOTE: warlock doesn't generate the most efficient patch for remove # operations (it shifts everything up and deletes the tail elements) so # we do it ourselves. url_indices = [current_urls.index(url) for url in url_set] url_indices.sort(reverse=True) patches = [{'op': 'remove', 'path': '/locations/%s' % url_idx} for url_idx in url_indices] return self._send_image_update_request(image_id, patches) def update_location(self, image_id, url, metadata): """Update an existing location entry in an image's list of locations. The URL specified must be already present in the image's list of locations. :param image_id: ID of image whose location is to be updated. :param url: URL of the location to update. :param metadata: Metadata associated with the location. :returns: The updated image """ image = self._get_image_with_locations_or_fail(image_id) url_map = dict([(l['url'], l) for l in image.locations]) if url not in url_map: raise exc.HTTPNotFound('Unknown URL: %s, the URL must be one of' ' existing locations of current image' % url) if url_map[url]['metadata'] == metadata: return image url_map[url]['metadata'] = metadata patches = [{'op': 'replace', 'path': '/locations', 'value': list(url_map.values())}] response = self._send_image_update_request(image_id, patches) # Get request id from the above update request and pass the same to # following get request req_id_hdr = {'x-openstack-request-id': response.request_ids[0]} return self._get(image_id, req_id_hdr) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/info.py0000664000175000017500000000153100000000000021775 0ustar00zuulzuul00000000000000# Copyright 2022 Red Hat, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. class Controller: def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client def get_usage(self, **kwargs): resp, body = self.http_client.get('/v2/info/usage') return body['usage'] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/metadefs.py0000664000175000017500000005301000000000000022631 0ustar00zuulzuul00000000000000# Copyright 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. from oslo_utils import encodeutils import urllib.parse import warlock from glanceclient.common import utils from glanceclient.v2 import schemas DEFAULT_PAGE_SIZE = 20 SORT_DIR_VALUES = ('asc', 'desc') SORT_KEY_VALUES = ('created_at', 'namespace') class NamespaceController(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('metadefs/namespace') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def create(self, **kwargs): """Create a namespace. :param kwargs: Unpacked namespace object. """ url = '/v2/metadefs/namespaces' try: namespace = self.model(kwargs) except (warlock.InvalidOperation, ValueError) as e: raise TypeError(encodeutils.exception_to_unicode(e)) resp, body = self.http_client.post(url, data=namespace) body.pop('self', None) return self.model(**body), resp def update(self, namespace_name, **kwargs): """Update a namespace. :param namespace_name: Name of a namespace (old one). :param kwargs: Unpacked namespace object. """ namespace = self.get(namespace_name) for (key, value) in kwargs.items(): try: setattr(namespace, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) # Remove read-only parameters. read_only = ['schema', 'updated_at', 'created_at'] for elem in read_only: if elem in namespace: del namespace[elem] url = '/v2/metadefs/namespaces/%(namespace)s' % { 'namespace': namespace_name} # Pass the original wrapped value to http client. resp, _ = self.http_client.put(url, data=namespace.wrapped) # Get request id from `put` request so it can be passed to the # following `get` call req_id_hdr = { 'x-openstack-request-id': utils._extract_request_id(resp) } return self._get(namespace.namespace, header=req_id_hdr) def get(self, namespace, **kwargs): return self._get(namespace, **kwargs) @utils.add_req_id_to_object() def _get(self, namespace, header=None, **kwargs): """Get one namespace.""" query_params = urllib.parse.urlencode(kwargs) if kwargs: query_params = '?%s' % query_params url = '/v2/metadefs/namespaces/%(namespace)s%(query_params)s' % { 'namespace': namespace, 'query_params': query_params} header = header or {} resp, body = self.http_client.get(url, headers=header) # NOTE(bcwaldon): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_generator() def list(self, **kwargs): """Retrieve a listing of Namespace objects. :param page_size: Number of items to request in each paginated request :param limit: Use to request a specific page size. Expect a response to a limited request to return between zero and limit items. :param marker: Specifies the namespace of the last-seen namespace. The typical pattern of limit and marker is to make an initial limited request and then to use the last namespace from the response as the marker parameter in a subsequent limited request. :param sort_key: The field to sort on (for example, 'created_at') :param sort_dir: The direction to sort ('asc' or 'desc') :returns: generator over list of Namespaces """ ori_validate_fun = self.model.validate empty_fun = lambda *args, **kwargs: None def paginate(url): resp, body = self.http_client.get(url) for namespace in body['namespaces']: # NOTE(bcwaldon): remove 'self' for now until we have # an elegant way to pass it into the model constructor # without conflict. namespace.pop('self', None) yield self.model(**namespace), resp # NOTE(zhiyan): In order to resolve the performance issue # of JSON schema validation for image listing case, we # don't validate each image entry but do it only on first # image entry for each page. self.model.validate = empty_fun # NOTE(zhiyan); Reset validation function. self.model.validate = ori_validate_fun try: next_url = body['next'] except KeyError: return else: for namespace, resp in paginate(next_url): yield namespace, resp filters = kwargs.get('filters', {}) filters = {} if filters is None else filters if not kwargs.get('page_size'): filters['limit'] = DEFAULT_PAGE_SIZE else: filters['limit'] = kwargs['page_size'] if 'marker' in kwargs: filters['marker'] = kwargs['marker'] sort_key = kwargs.get('sort_key') if sort_key is not None: if sort_key in SORT_KEY_VALUES: filters['sort_key'] = sort_key else: raise ValueError('sort_key must be one of the following: %s.' % ', '.join(SORT_KEY_VALUES)) sort_dir = kwargs.get('sort_dir') if sort_dir is not None: if sort_dir in SORT_DIR_VALUES: filters['sort_dir'] = sort_dir else: raise ValueError('sort_dir must be one of the following: %s.' % ', '.join(SORT_DIR_VALUES)) for param, value in filters.items(): if isinstance(value, list): filters[param] = encodeutils.safe_encode(','.join(value)) elif isinstance(value, str): filters[param] = encodeutils.safe_encode(value) url = '/v2/metadefs/namespaces?%s' % urllib.parse.urlencode(filters) for namespace, resp in paginate(url): yield namespace, resp @utils.add_req_id_to_object() def delete(self, namespace): """Delete a namespace.""" url = '/v2/metadefs/namespaces/%(namespace)s' % { 'namespace': namespace} resp, body = self.http_client.delete(url) return (resp, body), resp class ResourceTypeController(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('metadefs/resource_type') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def associate(self, namespace, **kwargs): """Associate a resource type with a namespace.""" try: res_type = self.model(kwargs) except (warlock.InvalidOperation, ValueError) as e: raise TypeError(encodeutils.exception_to_unicode(e)) url = '/v2/metadefs/namespaces/%(namespace)s/resource_types' % { 'namespace': namespace} resp, body = self.http_client.post(url, data=res_type) body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_object() def deassociate(self, namespace, resource): """Deassociate a resource type with a namespace.""" url = ('/v2/metadefs/namespaces/%(namespace)s/' 'resource_types/%(resource)s') % { 'namespace': namespace, 'resource': resource} resp, body = self.http_client.delete(url) return (resp, body), resp @utils.add_req_id_to_generator() def list(self): """Retrieve a listing of available resource types. :returns: generator over list of resource_types """ url = '/v2/metadefs/resource_types' resp, body = self.http_client.get(url) for resource_type in body['resource_types']: yield self.model(**resource_type), resp @utils.add_req_id_to_generator() def get(self, namespace): url = '/v2/metadefs/namespaces/%(namespace)s/resource_types' % { 'namespace': namespace} resp, body = self.http_client.get(url) body.pop('self', None) for resource_type in body['resource_type_associations']: yield self.model(**resource_type), resp class PropertyController(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('metadefs/property') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def create(self, namespace, **kwargs): """Create a property. :param namespace: Name of a namespace the property will belong. :param kwargs: Unpacked property object. """ try: prop = self.model(kwargs) except (warlock.InvalidOperation, ValueError) as e: raise TypeError(encodeutils.exception_to_unicode(e)) url = '/v2/metadefs/namespaces/%(namespace)s/properties' % { 'namespace': namespace} resp, body = self.http_client.post(url, data=prop) body.pop('self', None) return self.model(**body), resp def update(self, namespace, prop_name, **kwargs): """Update a property. :param namespace: Name of a namespace the property belongs. :param prop_name: Name of a property (old one). :param kwargs: Unpacked property object. """ prop = self.get(namespace, prop_name) for (key, value) in kwargs.items(): try: setattr(prop, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) url = ('/v2/metadefs/namespaces/%(namespace)s/' 'properties/%(prop_name)s') % { 'namespace': namespace, 'prop_name': prop_name} # Pass the original wrapped value to http client. resp, _ = self.http_client.put(url, data=prop.wrapped) # Get request id from `put` request so it can be passed to the # following `get` call req_id_hdr = { 'x-openstack-request-id': utils._extract_request_id(resp)} return self._get(namespace, prop.name, req_id_hdr) def get(self, namespace, prop_name): return self._get(namespace, prop_name) @utils.add_req_id_to_object() def _get(self, namespace, prop_name, header=None): url = ('/v2/metadefs/namespaces/%(namespace)s/' 'properties/%(prop_name)s') % { 'namespace': namespace, 'prop_name': prop_name} header = header or {} resp, body = self.http_client.get(url, headers=header) body.pop('self', None) body['name'] = prop_name return self.model(**body), resp @utils.add_req_id_to_generator() def list(self, namespace, **kwargs): """Retrieve a listing of metadata properties. :returns: generator over list of objects """ url = '/v2/metadefs/namespaces/%(namespace)s/properties' % { 'namespace': namespace} resp, body = self.http_client.get(url) for key, value in body['properties'].items(): value['name'] = key yield self.model(value), resp @utils.add_req_id_to_object() def delete(self, namespace, prop_name): """Delete a property.""" url = ('/v2/metadefs/namespaces/%(namespace)s/' 'properties/%(prop_name)s') % { 'namespace': namespace, 'prop_name': prop_name} resp, body = self.http_client.delete(url) return (resp, body), resp @utils.add_req_id_to_object() def delete_all(self, namespace): """Delete all properties in a namespace.""" url = '/v2/metadefs/namespaces/%(namespace)s/properties' % { 'namespace': namespace} resp, body = self.http_client.delete(url) return (resp, body), resp class ObjectController(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('metadefs/object') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def create(self, namespace, **kwargs): """Create an object. :param namespace: Name of a namespace the object belongs. :param kwargs: Unpacked object. """ try: obj = self.model(kwargs) except (warlock.InvalidOperation, ValueError) as e: raise TypeError(encodeutils.exception_to_unicode(e)) url = '/v2/metadefs/namespaces/%(namespace)s/objects' % { 'namespace': namespace} resp, body = self.http_client.post(url, data=obj) body.pop('self', None) return self.model(**body), resp def update(self, namespace, object_name, **kwargs): """Update an object. :param namespace: Name of a namespace the object belongs. :param object_name: Name of an object (old one). :param kwargs: Unpacked object. """ obj = self.get(namespace, object_name) for (key, value) in kwargs.items(): try: setattr(obj, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) # Remove read-only parameters. read_only = ['schema', 'updated_at', 'created_at'] for elem in read_only: if elem in obj: del obj[elem] url = ('/v2/metadefs/namespaces/%(namespace)s/' 'objects/%(object_name)s') % { 'namespace': namespace, 'object_name': object_name} # Pass the original wrapped value to http client. resp, _ = self.http_client.put(url, data=obj.wrapped) # Get request id from `put` request so it can be passed to the # following `get` call req_id_hdr = { 'x-openstack-request-id': utils._extract_request_id(resp)} return self._get(namespace, obj.name, req_id_hdr) def get(self, namespace, object_name): return self._get(namespace, object_name) @utils.add_req_id_to_object() def _get(self, namespace, object_name, header=None): url = ('/v2/metadefs/namespaces/%(namespace)s/' 'objects/%(object_name)s') % { 'namespace': namespace, 'object_name': object_name} header = header or {} resp, body = self.http_client.get(url, headers=header) body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_generator() def list(self, namespace, **kwargs): """Retrieve a listing of metadata objects. :returns: generator over list of objects """ url = '/v2/metadefs/namespaces/%(namespace)s/objects' % { 'namespace': namespace} resp, body = self.http_client.get(url) for obj in body['objects']: yield self.model(obj), resp @utils.add_req_id_to_object() def delete(self, namespace, object_name): """Delete an object.""" url = ('/v2/metadefs/namespaces/%(namespace)s/' 'objects/%(object_name)s') % { 'namespace': namespace, 'object_name': object_name} resp, body = self.http_client.delete(url) return (resp, body), resp @utils.add_req_id_to_object() def delete_all(self, namespace): """Delete all objects in a namespace.""" url = '/v2/metadefs/namespaces/%(namespace)s/objects' % { 'namespace': namespace} resp, body = self.http_client.delete(url) return (resp, body), resp class TagController(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('metadefs/tag') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_object() def create(self, namespace, tag_name): """Create a tag. :param namespace: Name of a namespace the Tag belongs. :param tag_name: The name of the new tag to create. """ url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % { 'namespace': namespace, 'tag_name': tag_name} resp, body = self.http_client.post(url) body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_generator() def create_multiple(self, namespace, **kwargs): """Create the list of tags. :param namespace: Name of a namespace to which the Tags belong. :param kwargs: list of tags, optional parameter append. """ tag_names = kwargs.pop('tags', []) md_tag_list = [] for tag_name in tag_names: try: md_tag_list.append(self.model(name=tag_name)) except (warlock.InvalidOperation) as e: raise TypeError(encodeutils.exception_to_unicode(e)) tags = {'tags': md_tag_list} headers = {} url = '/v2/metadefs/namespaces/%(namespace)s/tags' % { 'namespace': namespace} append = kwargs.pop('append', False) if append: headers['X-Openstack-Append'] = True resp, body = self.http_client.post(url, headers=headers, data=tags) body.pop('self', None) for tag in body['tags']: yield self.model(tag), resp def update(self, namespace, tag_name, **kwargs): """Update a tag. :param namespace: Name of a namespace the Tag belongs. :param tag_name: Name of the Tag (old one). :param kwargs: Unpacked tag. """ tag = self.get(namespace, tag_name) for (key, value) in kwargs.items(): try: setattr(tag, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) # Remove read-only parameters. read_only = ['updated_at', 'created_at'] for elem in read_only: if elem in tag: del tag[elem] url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % { 'namespace': namespace, 'tag_name': tag_name} # Pass the original wrapped value to http client. resp, _ = self.http_client.put(url, data=tag.wrapped) # Get request id from `put` request so it can be passed to the # following `get` call req_id_hdr = { 'x-openstack-request-id': utils._extract_request_id(resp)} return self._get(namespace, tag.name, req_id_hdr) def get(self, namespace, tag_name): return self._get(namespace, tag_name) @utils.add_req_id_to_object() def _get(self, namespace, tag_name, header=None): url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % { 'namespace': namespace, 'tag_name': tag_name} header = header or {} resp, body = self.http_client.get(url, headers=header) body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_generator() def list(self, namespace, **kwargs): """Retrieve a listing of metadata tags. :returns: generator over list of tags. """ url = '/v2/metadefs/namespaces/%(namespace)s/tags' % { 'namespace': namespace} resp, body = self.http_client.get(url) for tag in body['tags']: yield self.model(tag), resp @utils.add_req_id_to_object() def delete(self, namespace, tag_name): """Delete a tag.""" url = '/v2/metadefs/namespaces/%(namespace)s/tags/%(tag_name)s' % { 'namespace': namespace, 'tag_name': tag_name} resp, body = self.http_client.delete(url) return (resp, body), resp @utils.add_req_id_to_object() def delete_all(self, namespace): """Delete all tags in a namespace.""" url = '/v2/metadefs/namespaces/%(namespace)s/tags' % { 'namespace': namespace} resp, body = self.http_client.delete(url) return (resp, body), resp ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/namespace_schema.py0000664000175000017500000001727400000000000024331 0ustar00zuulzuul00000000000000# Copyright 2015 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(flaper87): Keep a copy of the current default schema so that # we can react on cases where there's no connection to an OpenStack # deployment. See #1481729 BASE_SCHEMA = { "additionalProperties": False, "definitions": { "positiveInteger": { "minimum": 0, "type": "integer" }, "positiveIntegerDefault0": { "allOf": [ {"$ref": "#/definitions/positiveInteger"}, {"default": 0} ] }, "stringArray": { "type": "array", "items": {"type": "string"}, "uniqueItems": True }, "property": { "type": "object", "additionalProperties": { "type": "object", "required": ["title", "type"], "properties": { "name": { "type": "string" }, "title": { "type": "string" }, "description": { "type": "string" }, "operators": { "type": "array", "items": { "type": "string" } }, "type": { "type": "string", "enum": [ "array", "boolean", "integer", "number", "object", "string", None ] }, "required": { "$ref": "#/definitions/stringArray" }, "minimum": { "type": "number" }, "maximum": { "type": "number" }, "maxLength": { "$ref": "#/definitions/positiveInteger" }, "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" }, "pattern": { "type": "string", "format": "regex" }, "enum": { "type": "array" }, "readonly": { "type": "boolean" }, "default": {}, "items": { "type": "object", "properties": { "type": { "type": "string", "enum": [ "array", "boolean", "integer", "number", "object", "string", None ] }, "enum": { "type": "array" } } }, "maxItems": { "$ref": "#/definitions/positiveInteger" }, "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" }, "uniqueItems": { "type": "boolean", "default": False }, "additionalItems": { "type": "boolean" }, } } } }, "required": ["namespace"], "name": "namespace", "properties": { "namespace": { "type": "string", "description": "The unique namespace text.", "maxLength": 80 }, "display_name": { "type": "string", "description": "The user friendly name for the namespace. Used by " "UI if available.", "maxLength": 80 }, "description": { "type": "string", "description": "Provides a user friendly description of the " "namespace.", "maxLength": 500 }, "visibility": { "enum": [ "public", "private" ], "type": "string", "description": "Scope of namespace accessibility." }, "protected": { "type": "boolean", "description": "If true, namespace will not be deletable." }, "owner": { "type": "string", "description": "Owner of the namespace.", "maxLength": 255 }, "created_at": { "type": "string", "readOnly": True, "description": "Date and time of namespace creation.", "format": "date-time" }, "updated_at": { "type": "string", "readOnly": True, "description": "Date and time of the last namespace modification.", "format": "date-time" }, "schema": { "readOnly": True, "type": "string" }, "self": { "readOnly": True, "type": "string" }, "resource_type_associations": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "prefix": { "type": "string" }, "properties_target": { "type": "string" } } } }, "properties": { "$ref": "#/definitions/property" }, "objects": { "items": { "type": "object", "properties": { "required": { "$ref": "#/definitions/stringArray" }, "description": { "type": "string" }, "name": { "type": "string" }, "properties": { "$ref": "#/definitions/property" } } }, "type": "array" }, "tags": { "items": { "type": "object", "properties": { "name": { "type": "string" } } }, "type": "array" }, } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/resource_type_schema.py0000664000175000017500000000534200000000000025256 0ustar00zuulzuul00000000000000# Copyright 2015 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(flaper87): Keep a copy of the current default schema so that # we can react on cases where there's no connection to an OpenStack # deployment. See #1481729 BASE_SCHEMA = { "additionalProperties": False, "required": ["name"], "name": "resource_type_association", "properties": { "name": { "type": "string", "description": "Resource type names should be aligned with Heat " "resource types whenever possible: https://docs." "openstack.org/heat/latest/template_guide/" "openstack.html", "maxLength": 80 }, "prefix": { "type": "string", "description": "Specifies the prefix to use for the given resource" " type. Any properties in the namespace should be" " prefixed with this prefix when being applied to" " the specified resource type. Must include prefix" " separator (e.g. a colon :).", "maxLength": 80 }, "properties_target": { "type": "string", "description": "Some resource types allow more than one key / " "value pair per instance. For example, Cinder " "allows user and image metadata on volumes. Only " "the image properties metadata is evaluated by Nova" " (scheduling or drivers). This property allows a " "namespace target to remove the ambiguity.", "maxLength": 80 }, "created_at": { "type": "string", "readOnly": True, "description": "Date and time of resource type association.", "format": "date-time" }, "updated_at": { "type": "string", "readOnly": True, "description": "Date and time of the last resource type " "association modification.", "format": "date-time" } } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/schemas.py0000664000175000017500000001020000000000000022456 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. import copy import json import jsonpatch import warlock.model as warlock class SchemaBasedModel(warlock.Model): """Glance specific subclass of the warlock Model. This implementation alters the function of the patch property to take into account the schema's core properties. With this version undefined properties which are core will generated 'replace' operations rather than 'add' since this is what the Glance API expects. """ def _make_custom_patch(self, new, original): if not self.get('tags'): tags_patch = [] else: tags_patch = [{"path": "/tags", "value": self.get('tags'), "op": "replace"}] patch_string = jsonpatch.make_patch(original, new).to_string() patch = json.loads(patch_string) if not patch: return json.dumps(tags_patch) else: return json.dumps(patch + tags_patch) @warlock.Model.patch.getter def patch(self): """Return a jsonpatch object representing the delta.""" original = copy.deepcopy(self.__dict__['__original__']) new = dict(self) if self.schema: for (name, prop) in self.schema['properties'].items(): if (name not in original and name in new and prop.get('is_base', True)): original[name] = None original['tags'] = None new['tags'] = None return self._make_custom_patch(new, original) class SchemaProperty(object): def __init__(self, name, **kwargs): self.name = name self.description = kwargs.get('description') self.is_base = kwargs.get('is_base', True) def translate_schema_properties(schema_properties): """Parse the properties dictionary of a schema document. :returns: list of SchemaProperty objects """ properties = [] for (name, prop) in schema_properties.items(): properties.append(SchemaProperty(name, **prop)) return properties class Schema(object): def __init__(self, raw_schema): self._raw_schema = raw_schema self.name = raw_schema['name'] raw_properties = raw_schema['properties'] self.properties = translate_schema_properties(raw_properties) def is_core_property(self, property_name): """Check if a property with a given name is known to the schema. Determines if it is either a base property or a custom one registered in schema-image.json file :param property_name: name of the property :returns: True if the property is known, False otherwise """ return self._check_property(property_name, True) def is_base_property(self, property_name): """Checks if a property with a given name is a base property. :param property_name: name of the property :returns: True if the property is base, False otherwise """ return self._check_property(property_name, False) def _check_property(self, property_name, allow_non_base): for prop in self.properties: if property_name == prop.name: return prop.is_base or allow_non_base return False def raw(self): return copy.deepcopy(self._raw_schema) class Controller(object): def __init__(self, http_client): self.http_client = http_client def get(self, schema_name): uri = '/v2/schemas/%s' % schema_name _, raw_schema = self.http_client.get(uri) return Schema(raw_schema) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1688740181.0 python-glanceclient-4.4.0/glanceclient/v2/shell.py0000664000175000017500000021065700000000000022164 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. import json import os import sys from oslo_utils import strutils from glanceclient._i18n import _ from glanceclient.common import progressbar from glanceclient.common import utils from glanceclient import exc from glanceclient.v2 import cache from glanceclient.v2 import image_members from glanceclient.v2 import image_schema from glanceclient.v2 import images from glanceclient.v2 import namespace_schema from glanceclient.v2 import resource_type_schema from glanceclient.v2 import tasks MEMBER_STATUS_VALUES = image_members.MEMBER_STATUS_VALUES IMAGE_SCHEMA = None DATA_FIELDS = ('location', 'copy_from', 'file', 'uri') def get_image_schema(): global IMAGE_SCHEMA if IMAGE_SCHEMA is None: schema_path = os.path.expanduser("~/.glanceclient/image_schema.json") if os.path.isfile(schema_path): with open(schema_path, "r") as f: schema_raw = f.read() IMAGE_SCHEMA = json.loads(schema_raw) else: return image_schema._BASE_SCHEMA return IMAGE_SCHEMA @utils.schema_args(get_image_schema, omit=['locations', 'os_hidden']) # NOTE(rosmaita): to make this option more intuitive for end users, we # do not use the Glance image property name 'os_hidden' here. This means # we must include 'os_hidden' in the 'omit' list above and handle the # --hidden argument by hand @utils.arg('--hidden', type=strutils.bool_from_string, metavar='[True|False]', default=None, dest='os_hidden', help=("If true, image will not appear in default image list " "response.")) @utils.arg('--property', metavar="", action='append', default=[], help=_('Arbitrary property to associate with image.' ' May be used multiple times.')) @utils.arg('--file', metavar='', help=_('Local file that contains disk image to be uploaded ' 'during creation. Alternatively, the image data can be ' 'passed to the client via stdin.')) @utils.arg('--progress', action='store_true', default=False, help=_('Show upload progress bar.')) @utils.arg('--store', metavar='', default=utils.env('OS_IMAGE_STORE', default=None), help='Backend store to upload image to.') @utils.on_data_require_fields(DATA_FIELDS) def do_image_create(gc, args): """Create a new image.""" schema = gc.schemas.get("image") _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (x[0] == 'property' or schema.is_core_property(x[0])), _args)) raw_properties = fields.pop('property', []) for datum in raw_properties: key, value = datum.split('=', 1) fields[key] = value backend = args.store file_name = fields.pop('file', None) using_stdin = hasattr(sys.stdin, 'isatty') and not sys.stdin.isatty() if args.store and not (file_name or using_stdin): utils.exit("--store option should only be provided with --file " "option or stdin.") if backend: # determine if backend is valid _validate_backend(backend, gc) if file_name is not None and os.access(file_name, os.R_OK) is False: utils.exit("File %s does not exist or user does not have read " "privileges to it" % file_name) image = gc.images.create(**fields) try: if utils.get_data_file(args) is not None: backend = fields.get('store', None) args.id = image['id'] args.size = None do_image_upload(gc, args) image = gc.images.get(args.id) finally: utils.print_image(image) @utils.schema_args(get_image_schema, omit=['locations', 'os_hidden']) # NOTE: --hidden requires special handling; see note at do_image_create @utils.arg('--hidden', type=strutils.bool_from_string, metavar='[True|False]', default=None, dest='os_hidden', help=("If true, image will not appear in default image list " "response.")) @utils.arg('--property', metavar="", action='append', default=[], help=_('Arbitrary property to associate with image.' ' May be used multiple times.')) @utils.arg('--file', metavar='', help=_('Local file that contains disk image to be uploaded ' 'during creation. Alternatively, the image data can be ' 'passed to the client via stdin.')) @utils.arg('--progress', action='store_true', default=False, help=_('Show upload progress bar.')) @utils.arg('--import-method', metavar='', default=utils.env('OS_IMAGE_IMPORT_METHOD', default=None), help=_('Import method used for Image Import workflow. ' 'Valid values can be retrieved with import-info command. ' 'Defaults to env[OS_IMAGE_IMPORT_METHOD] or if that is ' 'undefined uses \'glance-direct\' if data is provided using ' '--file or stdin. Otherwise, simply creates an image ' 'record if no import-method and no data is supplied')) @utils.arg('--uri', metavar='', default=None, help=_('URI to download the external image.')) @utils.arg('--remote-region', metavar='', default=None, help=_('REMOTE_GLANCE_REGION to download the image.')) @utils.arg('--remote-image-id', metavar='', default=None, help=_('The IMAGE ID of the image of remote glance, which needs' 'to be imported with glance-download')) @utils.arg('--remote-service-interface', metavar='', default='public', help=_('The Remote Glance Service Interface for glance-download')) @utils.arg('--store', metavar='', default=utils.env('OS_IMAGE_STORE', default=None), help='Backend store to upload image to.') @utils.arg('--stores', metavar='', default=utils.env('OS_IMAGE_STORES', default=None), help=_('Stores to upload image to if multi-stores import ' 'available. Comma separated list. Available stores can be ' 'listed with "stores-info" call.')) @utils.arg('--all-stores', type=strutils.bool_from_string, metavar='[True|False]', default=None, dest='os_all_stores', help=_('"all-stores" can be ued instead of "stores"-list to ' 'indicate that image should be imported into all available ' 'stores.')) @utils.arg('--allow-failure', type=strutils.bool_from_string, metavar='[True|False]', dest='os_allow_failure', default=utils.env('OS_IMAGE_ALLOW_FAILURE', default=True), help=_('Indicator if all stores listed (or available) must ' 'succeed. "True" by default meaning that we allow some ' 'stores to fail and the status can be monitored from the ' 'image metadata. If this is set to "False" the import will ' 'be reverted should any of the uploads fail. Only usable ' 'with "stores" or "all-stores".')) @utils.on_data_require_fields(DATA_FIELDS) def do_image_create_via_import(gc, args): """EXPERIMENTAL: Create a new image via image import. Use the interoperable image import workflow to create an image. This command is designed to be backward compatible with the current image-create command, so its behavior is as follows: * If an import-method is specified (either on the command line or through the OS_IMAGE_IMPORT_METHOD environment variable, then you must provide a data source appropriate to that method (for example, --file for glance-direct, or --uri for web-download). * If no import-method is specified AND you provide either a --file or data to stdin, the command will assume you are using the 'glance-direct' import-method and will act accordingly. * If no import-method is specified and no data is supplied via --file or stdin, the command will simply create an image record in 'queued' status. """ schema = gc.schemas.get("image") _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (x[0] == 'property' or schema.is_core_property(x[0])), _args)) raw_properties = fields.pop('property', []) for datum in raw_properties: key, value = datum.split('=', 1) fields[key] = value file_name = fields.pop('file', None) using_stdin = hasattr(sys.stdin, 'isatty') and not sys.stdin.isatty() # special processing for backward compatibility with image-create if args.import_method is None and (file_name or using_stdin): args.import_method = 'glance-direct' if args.import_method == 'copy-image': utils.exit("Import method 'copy-image' cannot be used " "while creating the image.") # determine whether the requested import method is valid import_methods = gc.images.get_import_info().get('import-methods') if args.import_method and args.import_method not in import_methods.get( 'value'): utils.exit("Import method '%s' is not valid for this cloud. " "Valid values can be retrieved with import-info command." % args.import_method) # determine if backend is valid backend = None stores = getattr(args, "stores", None) all_stores = getattr(args, "os_all_stores", None) if (args.store and (stores or all_stores)) or (stores and all_stores): utils.exit("Only one of --store, --stores and --all-stores can be " "provided") elif args.store: backend = args.store # determine if backend is valid _validate_backend(backend, gc) elif stores: # NOTE(jokke): Making sure here that we do not include the stores in # the create call fields.pop("stores") stores = str(stores).split(',') for store in stores: # determine if backend is valid _validate_backend(store, gc) # make sure we have all and only correct inputs for the requested method if args.import_method is None: if args.uri: utils.exit("You cannot use --uri without specifying an import " "method.") if args.import_method == 'glance-direct': if backend and not (file_name or using_stdin): utils.exit("--store option should only be provided with --file " "option or stdin for the glance-direct import method.") if stores and not (file_name or using_stdin): utils.exit("--stores option should only be provided with --file " "option or stdin for the glance-direct import method.") if all_stores and not (file_name or using_stdin): utils.exit("--all-stores option should only be provided with " "--file option or stdin for the glance-direct import " "method.") if args.uri: utils.exit("You cannot specify a --uri with the glance-direct " "import method.") if file_name is not None and os.access(file_name, os.R_OK) is False: utils.exit("File %s does not exist or user does not have read " "privileges to it." % file_name) if file_name is not None and using_stdin: utils.exit("You cannot use both --file and stdin with the " "glance-direct import method.") if not file_name and not using_stdin: utils.exit("You must specify a --file or provide data via stdin " "for the glance-direct import method.") if args.import_method == 'web-download': if backend and not args.uri: utils.exit("--store option should only be provided with --uri " "option for the web-download import method.") if stores and not args.uri: utils.exit("--stores option should only be provided with --uri " "option for the web-download import method.") if all_stores and not args.uri: utils.exit("--all-stores option should only be provided with " "--uri option for the web-download import method.") if not args.uri: utils.exit("URI is required for web-download import method. " "Please use '--uri '.") if file_name: utils.exit("You cannot specify a --file with the web-download " "import method.") if using_stdin: utils.exit("You cannot pass data via stdin with the web-download " "import method.") if args.import_method == 'glance-download': if not (args.remote_region and args.remote_image_id): utils.exit("REMOTE GlANCE REGION and REMOTE IMAGE ID are " "required for glance-download import method. " "Please use --remote-region and " "--remote-image-id .") if args.uri: utils.exit("You cannot specify a --uri with the glance-download " "import method.") if file_name: utils.exit("You cannot specify a --file with the glance-download " "import method.") if using_stdin: utils.exit("You cannot pass data via stdin with the " "glance-download import method.") # process image = gc.images.create(**fields) try: args.id = image['id'] if args.import_method: if utils.get_data_file(args) is not None: args.size = None do_image_stage(gc, args) args.from_create = True args.stores = stores do_image_import(gc, args) image = gc.images.get(args.id) finally: utils.print_image(image) def _validate_backend(backend, gc): try: enabled_backends = gc.images.get_stores_info().get('stores') except exc.HTTPNotFound: # NOTE(abhishekk): To maintain backward compatibility return if backend: valid_backend = False for available_backend in enabled_backends: if available_backend['id'] == backend: valid_backend = True break if not valid_backend: utils.exit("Store '%s' is not valid for this cloud. Valid " "values can be retrieved with stores-info command." % backend) @utils.arg('id', metavar='', help=_('ID of image to update.')) @utils.schema_args(get_image_schema, omit=['id', 'locations', 'tags', 'os_hidden']) # NOTE: --hidden requires special handling; see note at do_image_create @utils.arg('--hidden', type=strutils.bool_from_string, metavar='[True|False]', default=None, dest='os_hidden', help=("If true, image will not appear in default image list " "response.")) @utils.arg('--property', metavar="", action='append', default=[], help=_('Arbitrary property to associate with image.' ' May be used multiple times.')) @utils.arg('--remove-property', metavar="key", action='append', default=[], help=_("Name of arbitrary property to remove from the image.")) def do_image_update(gc, args): """Update an existing image.""" schema = gc.schemas.get("image") _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (x[0] in ['property', 'remove_property'] or schema.is_core_property(x[0])), _args)) raw_properties = fields.pop('property', []) for datum in raw_properties: key, value = datum.split('=', 1) fields[key] = value remove_properties = fields.pop('remove_property', None) image_id = fields.pop('id') image = gc.images.update(image_id, remove_properties, **fields) utils.print_image(image) @utils.arg('--limit', metavar='', default=None, type=int, help=_('Maximum number of images to get.')) @utils.arg('--page-size', metavar='', default=None, type=int, help=_('Number of images to request in each paginated request.')) @utils.arg('--visibility', metavar='', help=_('The visibility of the images to display.')) @utils.arg('--member-status', metavar='', help=_('The status of images to display.')) @utils.arg('--owner', metavar='', help=_('Display images owned by .')) @utils.arg('--property-filter', metavar='', help=_("Filter images by a user-defined image property."), action='append', dest='properties', default=[]) @utils.arg('--checksum', metavar='', help=_('Displays images that match the MD5 checksum.')) @utils.arg('--hash', dest='os_hash_value', default=None, metavar='', help=_('Displays images that match the specified hash value.')) @utils.arg('--tag', metavar='', action='append', help=_("Filter images by a user-defined tag.")) @utils.arg('--sort-key', default=[], action='append', choices=images.SORT_KEY_VALUES, help=_('Sort image list by specified fields.' ' May be used multiple times.')) @utils.arg('--sort-dir', default=[], action='append', choices=images.SORT_DIR_VALUES, help=_('Sort image list in specified directions.')) @utils.arg('--sort', metavar='[:]', default=None, help=(_("Comma-separated list of sort keys and directions in the " "form of [:]. Valid keys: %s. OPTIONAL." ) % ', '.join(images.SORT_KEY_VALUES))) @utils.arg('--hidden', dest='os_hidden', metavar='[True|False]', default=None, type=strutils.bool_from_string, const=True, nargs='?', help="Filters results by hidden status. Default=None.") @utils.arg('--include-stores', metavar='[True|False]', default=None, type=strutils.bool_from_string, const=True, nargs='?', help="Print backend store id.") def do_image_list(gc, args): """List images you can access.""" filter_keys = ['visibility', 'member_status', 'owner', 'checksum', 'tag', 'os_hidden', 'os_hash_value'] filter_items = [(key, getattr(args, key)) for key in filter_keys] if args.properties: filter_properties = [prop.split('=', 1) for prop in args.properties] if any(len(pair) != 2 for pair in filter_properties): utils.exit('Argument --property-filter expected properties in the' ' format KEY=VALUE') filter_items += filter_properties filters = dict([item for item in filter_items if item[1] is not None]) kwargs = {'filters': filters} if args.limit is not None: kwargs['limit'] = args.limit if args.page_size is not None: kwargs['page_size'] = args.page_size if args.sort_key: kwargs['sort_key'] = args.sort_key if args.sort_dir: kwargs['sort_dir'] = args.sort_dir if args.sort is not None: kwargs['sort'] = args.sort elif not args.sort_dir and not args.sort_key: kwargs['sort_key'] = 'name' kwargs['sort_dir'] = 'asc' columns = ['ID', 'Name'] if args.verbose: columns += ['Disk_format', 'Container_format', 'Size', 'Status', 'Owner'] if args.include_stores: columns += ['Stores'] images = gc.images.list(**kwargs) utils.print_list(images, columns) @utils.arg('id', metavar='', help=_('ID of image to describe.')) @utils.arg('--human-readable', action='store_true', default=False, help=_('Print image size in a human-friendly format.')) @utils.arg('--max-column-width', metavar='', default=80, help=_('The max column width of the printed table.')) def do_image_show(gc, args): """Describe a specific image.""" image = gc.images.get(args.id) utils.print_image(image, args.human_readable, int(args.max_column_width)) @utils.arg('id', metavar='', help=_('ID of image to get tasks.')) def do_image_tasks(gc, args): """Get tasks associated with image""" columns = ['Message', 'Status', 'Updated at'] if args.verbose: columns_to_prepend = ['Image Id', 'Task Id'] columns_to_extend = ['User Id', 'Request Id', 'Result', 'Owner', 'Input', 'Expires at'] columns = columns_to_prepend + columns + columns_to_extend try: tasks = gc.images.get_associated_image_tasks(args.id) utils.print_dict_list(tasks['tasks'], columns) except exc.HTTPNotFound: utils.exit('Image %s not found.' % args.id) except exc.HTTPNotImplemented: utils.exit('Server does not support image tasks API (v2.12)') def do_usage(gc, args): """Get quota usage information.""" columns = ['Quota', 'Limit', 'Usage'] usage = gc.info.get_usage() utils.print_dict_list( [dict(v, quota=k) for k, v in usage.items()], columns) @utils.arg('--image-id', metavar='', required=True, help=_('Image to display members of.')) def do_member_list(gc, args): """Describe sharing permissions by image.""" members = gc.image_members.list(args.image_id) columns = ['Image ID', 'Member ID', 'Status'] utils.print_list(members, columns) @utils.arg('image_id', metavar='', help=_('Image from which to display member.')) @utils.arg('member_id', metavar='', help=_('Project to display.')) def do_member_get(gc, args): """Show details of an image member""" member = gc.image_members.get(args.image_id, args.member_id) utils.print_dict(member) @utils.arg('image_id', metavar='', help=_('Image from which to remove member.')) @utils.arg('member_id', metavar='', help=_('Tenant to remove as member.')) def do_member_delete(gc, args): """Delete image member.""" if not (args.image_id and args.member_id): utils.exit('Unable to delete member. Specify image_id and member_id') else: gc.image_members.delete(args.image_id, args.member_id) @utils.arg('image_id', metavar='', help=_('Image from which to update member.')) @utils.arg('member_id', metavar='', help=_('Tenant to update.')) @utils.arg('member_status', metavar='', choices=MEMBER_STATUS_VALUES, help=(_('Updated status of member. Valid Values: %s') % ', '.join(str(val) for val in MEMBER_STATUS_VALUES))) def do_member_update(gc, args): """Update the status of a member for a given image.""" if not (args.image_id and args.member_id and args.member_status): utils.exit('Unable to update member. Specify image_id, member_id and' ' member_status') else: member = gc.image_members.update(args.image_id, args.member_id, args.member_status) member = [member] columns = ['Image ID', 'Member ID', 'Status'] utils.print_list(member, columns) @utils.arg('image_id', metavar='', help=_('Image with which to create member.')) @utils.arg('member_id', metavar='', help=_('Tenant to add as member.')) def do_member_create(gc, args): """Create member for a given image.""" if not (args.image_id and args.member_id): utils.exit('Unable to create member. Specify image_id and member_id') else: member = gc.image_members.create(args.image_id, args.member_id) member = [member] columns = ['Image ID', 'Member ID', 'Status'] utils.print_list(member, columns) @utils.arg('model', metavar='', help=_('Name of model to describe.')) def do_explain(gc, args): """Describe a specific model.""" try: schema = gc.schemas.get(args.model) except exc.HTTPNotFound: utils.exit('Unable to find requested model \'%s\'' % args.model) else: formatters = {'Attribute': lambda m: m.name} columns = ['Attribute', 'Description'] utils.print_list(schema.properties, columns, formatters) def do_import_info(gc, args): """Print import methods available from Glance.""" try: import_info = gc.images.get_import_info() except exc.HTTPNotFound: utils.exit('Target Glance does not support Image Import workflow') else: utils.print_dict(import_info) @utils.arg('--detail', default=False, action='store_true', help='Shows details of stores. Admin only.') def do_stores_info(gc, args): """Print available backends from Glance.""" try: if args.detail: stores_info = gc.images.get_stores_info_detail() else: stores_info = gc.images.get_stores_info() except exc.HTTPNotFound: utils.exit('Multi Backend support is not enabled') else: utils.print_dict(stores_info) @utils.arg('id', metavar='', help=_('ID of image to update.')) @utils.arg('--store', metavar='', required=True, help=_('Store to delete image from.')) def do_stores_delete(gc, args): """Delete image from specific store.""" try: gc.images.delete_from_store(args.store, args.id) except exc.HTTPNotFound: utils.exit('Multi Backend support is not enabled or Image/store not ' 'found.') except (exc.HTTPForbidden, exc.HTTPException) as e: msg = ("Unable to delete image '%s' from store '%s'. (%s)" % ( args.id, args.store, e)) utils.exit(msg) @utils.arg('--allow-md5-fallback', action='store_true', default=utils.env('OS_IMAGE_ALLOW_MD5_FALLBACK', default=False), help=_('If os_hash_algo and os_hash_value properties are available ' 'on the image, they will be used to validate the downloaded ' 'image data. If the indicated secure hash algorithm is not ' 'available on the client, the download will fail. Use this ' 'flag to indicate that in such a case the legacy MD5 image ' 'checksum should be used to validate the downloaded data. ' 'You can also set the environment variable ' 'OS_IMAGE_ALLOW_MD5_FALLBACK to any value to activate this ' 'option.')) @utils.arg('--file', metavar='', help=_('Local file to save downloaded image data to. ' 'If this is not specified and there is no redirection ' 'the image data will not be saved.')) @utils.arg('id', metavar='', help=_('ID of image to download.')) @utils.arg('--progress', action='store_true', default=False, help=_('Show download progress bar.')) def do_image_download(gc, args): """Download a specific image.""" if sys.stdout.isatty() and (args.file is None): msg = ('No redirection or local file specified for downloaded image ' 'data. Please specify a local file with --file to save ' 'downloaded image or redirect output to another source.') utils.exit(msg) try: body = gc.images.data(args.id, allow_md5_fallback=args.allow_md5_fallback) except (exc.HTTPForbidden, exc.HTTPException) as e: msg = "Unable to download image '%s'. (%s)" % (args.id, e) utils.exit(msg) if body.wrapped is None: msg = ('Image %s has no data.' % args.id) utils.exit(msg) if args.progress: body = progressbar.VerboseIteratorWrapper(body, len(body)) utils.save_image(body, args.file) @utils.arg('--file', metavar='', help=_('Local file that contains disk image to be uploaded.' ' Alternatively, images can be passed' ' to the client via stdin.')) @utils.arg('--size', metavar='', type=int, help=_('Size in bytes of image to be uploaded. Default is to get ' 'size from provided data object but this is supported in ' 'case where size cannot be inferred.'), default=None) @utils.arg('--progress', action='store_true', default=False, help=_('Show upload progress bar.')) @utils.arg('id', metavar='', help=_('ID of image to upload data to.')) @utils.arg('--store', metavar='', default=utils.env('OS_IMAGE_STORE', default=None), help='Backend store to upload image to.') def do_image_upload(gc, args): """Upload data for a specific image.""" backend = None if args.store: backend = args.store # determine if backend is valid _validate_backend(backend, gc) image_data = utils.get_data_file(args) if args.progress: filesize = utils.get_file_size(image_data) if filesize is not None: # NOTE(kragniz): do not show a progress bar if the size of the # input is unknown (most likely a piped input) image_data = progressbar.VerboseFileWrapper(image_data, filesize) gc.images.upload(args.id, image_data, args.size, backend=backend) @utils.arg('--file', metavar='', help=_('Local file that contains disk image to be uploaded.' ' Alternatively, images can be passed' ' to the client via stdin.')) @utils.arg('--size', metavar='', type=int, help=_('Size in bytes of image to be uploaded. Default is to get ' 'size from provided data object but this is supported in ' 'case where size cannot be inferred.'), default=None) @utils.arg('--progress', action='store_true', default=False, help=_('Show upload progress bar.')) @utils.arg('id', metavar='', help=_('ID of image to upload data to.')) def do_image_stage(gc, args): """Upload data for a specific image to staging.""" image_data = utils.get_data_file(args) if args.progress: filesize = utils.get_file_size(image_data) if filesize is not None: # NOTE(kragniz): do not show a progress bar if the size of the # input is unknown (most likely a piped input) image_data = progressbar.VerboseFileWrapper(image_data, filesize) gc.images.stage(args.id, image_data, args.size) @utils.arg('--import-method', metavar='', default='glance-direct', help=_('Import method used for Image Import workflow. ' 'Valid values can be retrieved with import-info command ' 'and the default "glance-direct" is used with ' '"image-stage".')) @utils.arg('--uri', metavar='', default=None, help=_('URI to download the external image.')) @utils.arg('--remote-region', metavar='', default=None, help=_('REMOTE GLANCE REGION to download the image.')) @utils.arg('--remote-image-id', metavar='', default=None, help=_('The IMAGE ID of the image of remote glance, which needs' 'to be imported with glance-download')) @utils.arg('--remote-service-interface', metavar='', default='public', help=_('The Remote Glance Service Interface for glance-download')) @utils.arg('id', metavar='', help=_('ID of image to import.')) @utils.arg('--store', metavar='', default=utils.env('OS_IMAGE_STORE', default=None), help='Backend store to upload image to.') @utils.arg('--stores', metavar='', default=utils.env('OS_IMAGE_STORES', default=None), help='Stores to upload image to if multi-stores import available.') @utils.arg('--all-stores', type=strutils.bool_from_string, metavar='[True|False]', default=None, dest='os_all_stores', help=_('"all-stores" can be ued instead of "stores"-list to ' 'indicate that image should be imported all available ' 'stores.')) @utils.arg('--allow-failure', type=strutils.bool_from_string, metavar='[True|False]', dest='os_allow_failure', default=utils.env('OS_IMAGE_ALLOW_FAILURE', default=True), help=_('Indicator if all stores listed (or available) must ' 'succeed. "True" by default meaning that we allow some ' 'stores to fail and the status can be monitored from the ' 'image metadata. If this is set to "False" the import will ' 'be reverted should any of the uploads fail. Only usable ' 'with "stores" or "all-stores".')) def do_image_import(gc, args): """Initiate the image import taskflow.""" backend = getattr(args, "store", None) stores = getattr(args, "stores", None) all_stores = getattr(args, "os_all_stores", None) allow_failure = getattr(args, "os_allow_failure", True) uri = getattr(args, "uri", None) remote_region = getattr(args, "remote_region", None) remote_image_id = getattr(args, "remote_image_id", None) remote_service_interface = getattr(args, "remote_service_interface", None) if not getattr(args, 'from_create', False): if (args.store and (stores or all_stores)) or (stores and all_stores): utils.exit("Only one of --store, --stores and --all-stores can be " "provided") elif args.store: backend = args.store # determine if backend is valid _validate_backend(backend, gc) elif stores: stores = str(stores).split(',') # determine if backend is valid if stores: for store in stores: _validate_backend(store, gc) if getattr(args, 'from_create', False): # this command is being called "internally" so we can skip # validation -- just do the import and get out of here gc.images.image_import( args.id, args.import_method, args.uri, remote_region=remote_region, remote_image_id=remote_image_id, remote_service_interface=remote_service_interface, backend=backend, stores=stores, all_stores=all_stores, allow_failure=allow_failure) return # do input validation try: import_methods = gc.images.get_import_info().get('import-methods') except exc.HTTPNotFound: utils.exit('Target Glance does not support Image Import workflow') if args.import_method not in import_methods.get('value'): utils.exit("Import method '%s' is not valid for this cloud. " "Valid values can be retrieved with import-info command." % args.import_method) if args.import_method == 'web-download' and not args.uri: utils.exit("Provide URI for web-download import method.") if args.uri and args.import_method != 'web-download': utils.exit("Import method should be 'web-download' if URI is " "provided.") if args.import_method == 'glance-download' and \ not (remote_region and remote_image_id): utils.exit("Provide REMOTE_IMAGE_ID and remote-region for " "'glance-download' import method.") if remote_region and args.import_method != 'glance-download': utils.exit("Import method should be 'glance-download' if " "REMOTE REGION is provided.") if remote_image_id and args.import_method != 'glance-download': utils.exit("Import method should be 'glance-download' if " "REMOTE IMAGE ID is provided.") if remote_service_interface and args.import_method != 'glance-download': utils.exit("Import method should be 'glance-download' if " "REMOTE SERVICE INTERFACE is provided.") if args.import_method == 'copy-image' and not (stores or all_stores): utils.exit("Provide either --stores or --all-stores for " "'copy-image' import method.") # check image properties image = gc.images.get(args.id) container_format = image.get('container_format') disk_format = image.get('disk_format') if not (container_format and disk_format): utils.exit("The 'container_format' and 'disk_format' properties " "must be set on an image before it can be imported.") image_status = image.get('status') if args.import_method == 'glance-direct': if image_status != 'uploading': utils.exit("The 'glance-direct' import method can only be applied " "to an image in status 'uploading'") if args.import_method == 'web-download': if image_status != 'queued': utils.exit("The 'web-download' import method can only be applied " "to an image in status 'queued'") if args.import_method == 'copy-image': if image_status != 'active': utils.exit("The 'copy-image' import method can only be used on " "an image with status 'active'.") # finally, do the import gc.images.image_import(args.id, args.import_method, uri=uri, remote_region=remote_region, remote_image_id=remote_image_id, remote_service_interface=remote_service_interface, backend=backend, stores=stores, all_stores=all_stores, allow_failure=allow_failure) image = gc.images.get(args.id) utils.print_image(image) @utils.arg('id', metavar='', nargs='+', help=_('ID of image(s) to delete.')) def do_image_delete(gc, args): """Delete specified image.""" failure_flag = False for args_id in args.id: try: gc.images.delete(args_id) except exc.HTTPForbidden: msg = "You are not permitted to delete the image '%s'." % args_id utils.print_err(msg) failure_flag = True except exc.HTTPNotFound: msg = "No image with an ID of '%s' exists." % args_id utils.print_err(msg) failure_flag = True except exc.HTTPConflict: msg = "Unable to delete image '%s' because it is in use." % args_id utils.print_err(msg) failure_flag = True except exc.HTTPException as e: msg = "'%s': Unable to delete image '%s'" % (e, args_id) utils.print_err(msg) failure_flag = True if failure_flag: utils.exit() @utils.arg('id', metavar='', help=_('ID of image to deactivate.')) def do_image_deactivate(gc, args): """Deactivate specified image.""" gc.images.deactivate(args.id) @utils.arg('id', metavar='', help=_('ID of image to reactivate.')) def do_image_reactivate(gc, args): """Reactivate specified image.""" gc.images.reactivate(args.id) @utils.arg('image_id', metavar='', help=_('Image to be updated with the given tag.')) @utils.arg('tag_value', metavar='', help=_('Value of the tag.')) def do_image_tag_update(gc, args): """Update an image with the given tag.""" if not (args.image_id and args.tag_value): utils.exit('Unable to update tag. Specify image_id and tag_value') else: gc.image_tags.update(args.image_id, args.tag_value) image = gc.images.get(args.image_id) image = [image] columns = ['ID', 'Tags'] utils.print_list(image, columns) @utils.arg('image_id', metavar='', help=_('ID of the image from which to delete tag.')) @utils.arg('tag_value', metavar='', help=_('Value of the tag.')) def do_image_tag_delete(gc, args): """Delete the tag associated with the given image.""" if not (args.image_id and args.tag_value): utils.exit('Unable to delete tag. Specify image_id and tag_value') else: gc.image_tags.delete(args.image_id, args.tag_value) @utils.arg('--url', metavar='', required=True, help=_('URL of location to add.')) @utils.arg('--metadata', metavar='', default='{}', help=_('Metadata associated with the location. ' 'Must be a valid JSON object (default: %(default)s)')) @utils.arg('--checksum', metavar='', help=_('md5 checksum of image contents')) @utils.arg('--hash-algo', metavar='', help=_('Multihash algorithm')) @utils.arg('--hash-value', metavar='', help=_('Multihash value')) @utils.arg('id', metavar='', help=_('ID of image to which the location is to be added.')) def do_location_add(gc, args): """Add a location (and related metadata) to an image.""" validation_data = {} if args.checksum: validation_data['checksum'] = args.checksum if args.hash_algo: validation_data['os_hash_algo'] = args.hash_algo if args.hash_value: validation_data['os_hash_value'] = args.hash_value try: metadata = json.loads(args.metadata) except ValueError: utils.exit('Metadata is not a valid JSON object.') else: image = gc.images.add_location(args.id, args.url, metadata, validation_data=validation_data) utils.print_dict(image) @utils.arg('--url', metavar='', action='append', required=True, help=_('URL of location to remove. May be used multiple times.')) @utils.arg('id', metavar='', help=_('ID of image whose locations are to be removed.')) def do_location_delete(gc, args): """Remove locations (and related metadata) from an image.""" gc.images.delete_locations(args.id, set(args.url)) @utils.arg('--url', metavar='', required=True, help=_('URL of location to update.')) @utils.arg('--metadata', metavar='', default='{}', help=_('Metadata associated with the location. ' 'Must be a valid JSON object (default: %(default)s)')) @utils.arg('id', metavar='', help=_('ID of image whose location is to be updated.')) def do_location_update(gc, args): """Update metadata of an image's location.""" try: metadata = json.loads(args.metadata) if metadata == {}: print("WARNING -- The location's metadata will be updated to " "an empty JSON object.") except ValueError: utils.exit('Metadata is not a valid JSON object.') else: image = gc.images.update_location(args.id, args.url, metadata) utils.print_dict(image) # Metadata - catalog NAMESPACE_SCHEMA = None def get_namespace_schema(): global NAMESPACE_SCHEMA if NAMESPACE_SCHEMA is None: schema_path = os.path.expanduser("~/.glanceclient/" "namespace_schema.json") if os.path.isfile(schema_path): with open(schema_path, "r") as f: schema_raw = f.read() NAMESPACE_SCHEMA = json.loads(schema_raw) else: return namespace_schema.BASE_SCHEMA return NAMESPACE_SCHEMA def _namespace_show(namespace, max_column_width=None): namespace = dict(namespace) # Warlock objects are compatible with dicts # Flatten dicts for display if 'properties' in namespace: props = [k for k in namespace['properties']] namespace['properties'] = props if 'resource_type_associations' in namespace: assocs = [assoc['name'] for assoc in namespace['resource_type_associations']] namespace['resource_type_associations'] = assocs if 'objects' in namespace: objects = [obj['name'] for obj in namespace['objects']] namespace['objects'] = objects if 'tags' in namespace: tags = [tag['name'] for tag in namespace['tags']] namespace['tags'] = tags if max_column_width: utils.print_dict(namespace, max_column_width) else: utils.print_dict(namespace) @utils.arg('namespace', metavar='', help=_('Name of the namespace.')) @utils.schema_args(get_namespace_schema, omit=['namespace', 'property_count', 'properties', 'tag_count', 'tags', 'object_count', 'objects', 'resource_types']) def do_md_namespace_create(gc, args): """Create a new metadata definitions namespace.""" schema = gc.schemas.get('metadefs/namespace') _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (schema.is_core_property(x[0])), _args)) namespace = gc.metadefs_namespace.create(**fields) _namespace_show(namespace) @utils.arg('--file', metavar='', help=_('Path to file with namespace schema to import. ' 'Alternatively, namespaces schema can be passed to the ' 'client via stdin.')) def do_md_namespace_import(gc, args): """Import a metadata definitions namespace from file or standard input.""" namespace_data = utils.get_data_file(args) if not namespace_data: utils.exit('No metadata definition namespace passed via stdin or ' '--file argument.') try: namespace_json = json.load(namespace_data) except ValueError: utils.exit('Schema is not a valid JSON object.') else: namespace = gc.metadefs_namespace.create(**namespace_json) _namespace_show(namespace) @utils.arg('id', metavar='', help=_('Name of namespace to update.')) @utils.schema_args(get_namespace_schema, omit=['property_count', 'properties', 'tag_count', 'tags', 'object_count', 'objects', 'resource_type_associations']) def do_md_namespace_update(gc, args): """Update an existing metadata definitions namespace.""" schema = gc.schemas.get('metadefs/namespace') _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (schema.is_core_property(x[0])), _args)) namespace = gc.metadefs_namespace.update(args.id, **fields) _namespace_show(namespace) @utils.arg('namespace', metavar='', help=_('Name of namespace to describe.')) @utils.arg('--resource-type', metavar='', help=_('Applies prefix of given resource type associated to a ' 'namespace to all properties of a namespace.'), default=None) @utils.arg('--max-column-width', metavar='', default=80, help=_('The max column width of the printed table.')) def do_md_namespace_show(gc, args): """Describe a specific metadata definitions namespace. Lists also the namespace properties, objects and resource type associations. """ kwargs = {} if args.resource_type: kwargs['resource_type'] = args.resource_type namespace = gc.metadefs_namespace.get(args.namespace, **kwargs) _namespace_show(namespace, int(args.max_column_width)) @utils.arg('--resource-types', metavar='', action='append', help=_('Resource type to filter namespaces.')) @utils.arg('--visibility', metavar='', help=_('Visibility parameter to filter namespaces.')) @utils.arg('--page-size', metavar='', default=None, type=int, help=_('Number of namespaces to request ' 'in each paginated request.')) def do_md_namespace_list(gc, args): """List metadata definitions namespaces.""" filter_keys = ['resource_types', 'visibility'] filter_items = [(key, getattr(args, key, None)) for key in filter_keys] filters = dict([item for item in filter_items if item[1] is not None]) kwargs = {'filters': filters} if args.page_size is not None: kwargs['page_size'] = args.page_size namespaces = gc.metadefs_namespace.list(**kwargs) columns = ['namespace'] utils.print_list(namespaces, columns) @utils.arg('namespace', metavar='', help=_('Name of namespace to delete.')) def do_md_namespace_delete(gc, args): """Delete specified metadata definitions namespace with its contents.""" gc.metadefs_namespace.delete(args.namespace) # Metadata - catalog RESOURCE_TYPE_SCHEMA = None def get_resource_type_schema(): global RESOURCE_TYPE_SCHEMA if RESOURCE_TYPE_SCHEMA is None: schema_path = os.path.expanduser("~/.glanceclient/" "resource_type_schema.json") if os.path.isfile(schema_path): with open(schema_path, "r") as f: schema_raw = f.read() RESOURCE_TYPE_SCHEMA = json.loads(schema_raw) else: return resource_type_schema.BASE_SCHEMA return RESOURCE_TYPE_SCHEMA @utils.arg('namespace', metavar='', help=_('Name of namespace.')) @utils.schema_args(get_resource_type_schema) def do_md_resource_type_associate(gc, args): """Associate resource type with a metadata definitions namespace.""" schema = gc.schemas.get('metadefs/resource_type') _args = [(x[0].replace('-', '_'), x[1]) for x in vars(args).items()] fields = dict(filter(lambda x: x[1] is not None and (schema.is_core_property(x[0])), _args)) resource_type = gc.metadefs_resource_type.associate(args.namespace, **fields) utils.print_dict(resource_type) @utils.arg('namespace', metavar='', help=_('Name of namespace.')) @utils.arg('resource_type', metavar='', help=_('Name of resource type.')) def do_md_resource_type_deassociate(gc, args): """Deassociate resource type with a metadata definitions namespace.""" gc.metadefs_resource_type.deassociate(args.namespace, args.resource_type) def do_md_resource_type_list(gc, args): """List available resource type names.""" resource_types = gc.metadefs_resource_type.list() utils.print_list(resource_types, ['name']) @utils.arg('namespace', metavar='', help=_('Name of namespace.')) def do_md_namespace_resource_type_list(gc, args): """List resource types associated to specific namespace.""" resource_types = gc.metadefs_resource_type.get(args.namespace) utils.print_list(resource_types, ['name', 'prefix', 'properties_target']) @utils.arg('namespace', metavar='', help=_('Name of namespace the property will belong.')) @utils.arg('--name', metavar='', required=True, help=_('Internal name of a property.')) @utils.arg('--title', metavar='', required=True, help=_('Property name displayed to the user.')) @utils.arg('--schema', metavar='<SCHEMA>', required=True, help=_('Valid JSON schema of a property.')) @utils.arg('--type', metavar='<TYPE>', required=True, help=_('Type of the property')) def do_md_property_create(gc, args): """Create a new metadata definitions property inside a namespace.""" try: schema = json.loads(args.schema) except ValueError: utils.exit('Schema is not a valid JSON object.') else: fields = {'name': args.name, 'title': args.title, 'type': args.type} fields.update(schema) new_property = gc.metadefs_property.create(args.namespace, **fields) utils.print_dict(new_property) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the property belongs.')) @utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.')) @utils.arg('--name', metavar='<NAME>', default=None, help=_('New name of a property.')) @utils.arg('--title', metavar='<TITLE>', default=None, help=_('Property name displayed to the user.')) @utils.arg('--schema', metavar='<SCHEMA>', default=None, help=_('Valid JSON schema of a property.')) def do_md_property_update(gc, args): """Update metadata definitions property inside a namespace.""" fields = {} if args.name: fields['name'] = args.name if args.title: fields['title'] = args.title if args.schema: try: schema = json.loads(args.schema) except ValueError: utils.exit('Schema is not a valid JSON object.') else: fields.update(schema) new_property = gc.metadefs_property.update(args.namespace, args.property, **fields) utils.print_dict(new_property) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the property belongs.')) @utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.')) @utils.arg('--max-column-width', metavar='<integer>', default=80, help=_('The max column width of the printed table.')) def do_md_property_show(gc, args): """Describe a specific metadata definitions property inside a namespace.""" prop = gc.metadefs_property.get(args.namespace, args.property) utils.print_dict(prop, int(args.max_column_width)) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the property belongs.')) @utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.')) def do_md_property_delete(gc, args): """Delete a specific metadata definitions property inside a namespace.""" gc.metadefs_property.delete(args.namespace, args.property) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_namespace_properties_delete(gc, args): """Delete all metadata definitions property inside a specific namespace.""" gc.metadefs_property.delete_all(args.namespace) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_property_list(gc, args): """List metadata definitions properties inside a specific namespace.""" properties = gc.metadefs_property.list(args.namespace) columns = ['name', 'title', 'type'] utils.print_list(properties, columns) def _object_show(obj, max_column_width=None): obj = dict(obj) # Warlock objects are compatible with dicts # Flatten dicts for display if 'properties' in obj: objects = [k for k in obj['properties']] obj['properties'] = objects if max_column_width: utils.print_dict(obj, max_column_width) else: utils.print_dict(obj) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the object will belong.')) @utils.arg('--name', metavar='<NAME>', required=True, help=_('Internal name of an object.')) @utils.arg('--schema', metavar='<SCHEMA>', required=True, help=_('Valid JSON schema of an object.')) def do_md_object_create(gc, args): """Create a new metadata definitions object inside a namespace.""" try: schema = json.loads(args.schema) except ValueError: utils.exit('Schema is not a valid JSON object.') else: fields = {'name': args.name} fields.update(schema) new_object = gc.metadefs_object.create(args.namespace, **fields) _object_show(new_object) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the object belongs.')) @utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.')) @utils.arg('--name', metavar='<NAME>', default=None, help=_('New name of an object.')) @utils.arg('--schema', metavar='<SCHEMA>', default=None, help=_('Valid JSON schema of an object.')) def do_md_object_update(gc, args): """Update metadata definitions object inside a namespace.""" fields = {} if args.name: fields['name'] = args.name if args.schema: try: schema = json.loads(args.schema) except ValueError: utils.exit('Schema is not a valid JSON object.') else: fields.update(schema) new_object = gc.metadefs_object.update(args.namespace, args.object, **fields) _object_show(new_object) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the object belongs.')) @utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.')) @utils.arg('--max-column-width', metavar='<integer>', default=80, help=_('The max column width of the printed table.')) def do_md_object_show(gc, args): """Describe a specific metadata definitions object inside a namespace.""" obj = gc.metadefs_object.get(args.namespace, args.object) _object_show(obj, int(args.max_column_width)) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the object belongs.')) @utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.')) @utils.arg('property', metavar='<PROPERTY>', help=_('Name of a property.')) @utils.arg('--max-column-width', metavar='<integer>', default=80, help=_('The max column width of the printed table.')) def do_md_object_property_show(gc, args): """Describe a specific metadata definitions property inside an object.""" obj = gc.metadefs_object.get(args.namespace, args.object) try: prop = obj['properties'][args.property] prop['name'] = args.property except KeyError: utils.exit('Property %s not found in object %s.' % (args.property, args.object)) utils.print_dict(prop, int(args.max_column_width)) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace the object belongs.')) @utils.arg('object', metavar='<OBJECT>', help=_('Name of an object.')) def do_md_object_delete(gc, args): """Delete a specific metadata definitions object inside a namespace.""" gc.metadefs_object.delete(args.namespace, args.object) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_namespace_objects_delete(gc, args): """Delete all metadata definitions objects inside a specific namespace.""" gc.metadefs_object.delete_all(args.namespace) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_object_list(gc, args): """List metadata definitions objects inside a specific namespace.""" objects = gc.metadefs_object.list(args.namespace) columns = ['name', 'description'] column_settings = { "description": { "max_width": 50, "align": "l" } } utils.print_list(objects, columns, field_settings=column_settings) def _tag_show(tag, max_column_width=None): tag = dict(tag) # Warlock objects are compatible with dicts if max_column_width: utils.print_dict(tag, max_column_width) else: utils.print_dict(tag) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of the namespace the tag will belong to.')) @utils.arg('--name', metavar='<NAME>', required=True, help=_('The name of the new tag to add.')) def do_md_tag_create(gc, args): """Add a new metadata definitions tag inside a namespace.""" name = args.name.strip() if name: new_tag = gc.metadefs_tag.create(args.namespace, name) _tag_show(new_tag) else: utils.exit('Please supply at least one non-blank tag name.') @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of the namespace the tags will belong to.')) @utils.arg('--names', metavar='<NAMES>', required=True, help=_('A comma separated list of tag names.')) @utils.arg('--delim', metavar='<DELIM>', required=False, help=_('The delimiter used to separate the names' ' (if none is provided then the default is a comma).')) @utils.arg('--append', default=False, action='store_true', required=False, help=_('Append the new tags to the existing ones instead of' 'overwriting them')) def do_md_tag_create_multiple(gc, args): """Create new metadata definitions tags inside a namespace.""" delim = args.delim or ',' tags = [] names_list = args.names.split(delim) for name in names_list: name = name.strip() if name: tags.append(name) if not tags: utils.exit('Please supply at least one tag name. For example: ' '--names Tag1') fields = {'tags': tags, 'append': args.append} new_tags = gc.metadefs_tag.create_multiple(args.namespace, **fields) columns = ['name'] column_settings = { "description": { "max_width": 50, "align": "l" } } utils.print_list(new_tags, columns, field_settings=column_settings) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of the namespace to which the tag belongs.')) @utils.arg('tag', metavar='<TAG>', help=_('Name of the old tag.')) @utils.arg('--name', metavar='<NAME>', default=None, required=True, help=_('New name of the new tag.')) def do_md_tag_update(gc, args): """Rename a metadata definitions tag inside a namespace.""" name = args.name.strip() if name: fields = {'name': name} new_tag = gc.metadefs_tag.update(args.namespace, args.tag, **fields) _tag_show(new_tag) else: utils.exit('Please supply at least one non-blank tag name.') @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of the namespace to which the tag belongs.')) @utils.arg('tag', metavar='<TAG>', help=_('Name of the tag.')) def do_md_tag_show(gc, args): """Describe a specific metadata definitions tag inside a namespace.""" tag = gc.metadefs_tag.get(args.namespace, args.tag) _tag_show(tag) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of the namespace to which the tag belongs.')) @utils.arg('tag', metavar='<TAG>', help=_('Name of the tag.')) def do_md_tag_delete(gc, args): """Delete a specific metadata definitions tag inside a namespace.""" gc.metadefs_tag.delete(args.namespace, args.tag) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_namespace_tags_delete(gc, args): """Delete all metadata definitions tags inside a specific namespace.""" gc.metadefs_tag.delete_all(args.namespace) @utils.arg('namespace', metavar='<NAMESPACE>', help=_('Name of namespace.')) def do_md_tag_list(gc, args): """List metadata definitions tags inside a specific namespace.""" tags = gc.metadefs_tag.list(args.namespace) columns = ['name'] column_settings = { "description": { "max_width": 50, "align": "l" } } utils.print_list(tags, columns, field_settings=column_settings) @utils.arg('--target', default='both', choices=cache.TARGET_VALUES, help=_('Specify which target you want to clear')) def do_cache_clear(gc, args): """Clear all images from cache, queue or both""" if not gc.endpoint_provided: utils.exit("Direct server endpoint needs to be provided. Do not use " "loadbalanced or catalog endpoints.") try: gc.cache.clear(args.target) except exc.HTTPForbidden: msg = _("You are not permitted to delete image(s) " "from cache.") utils.print_err(msg) except exc.HTTPException as e: msg = _("'%s': Unable to delete image(s) from cache." % e) utils.print_err(msg) @utils.arg('id', metavar='<IMAGE_ID>', nargs='+', help=_('ID of image(s) to delete from cache/queue.')) def do_cache_delete(gc, args): """Delete image from cache/caching queue.""" if not gc.endpoint_provided: utils.exit("Direct server endpoint needs to be provided. Do not use " "loadbalanced or catalog endpoints.") for args_id in args.id: try: gc.cache.delete(args_id) except exc.HTTPForbidden: msg = _("You are not permitted to delete the image '%s' " "from cache." % args_id) utils.print_err(msg) except exc.HTTPException as e: msg = _("'%s': Unable to delete image '%s' from cache." % (e, args_id)) utils.print_err(msg) def do_cache_list(gc, args): """Get cache state.""" if not gc.endpoint_provided: utils.exit("Direct server endpoint needs to be provided. Do not use " "loadbalanced or catalog endpoints.") cached_images = gc.cache.list() utils.print_cached_images(cached_images) @utils.arg('id', metavar='<IMAGE_ID>', nargs='+', help=_('ID of image(s) to queue for caching.')) def do_cache_queue(gc, args): """Queue image(s) for caching.""" if not gc.endpoint_provided: utils.exit("Direct server endpoint needs to be provided. Do not use " "loadbalanced or catalog endpoints.") for args_id in args.id: try: gc.cache.queue(args_id) except exc.HTTPForbidden: msg = _("You are not permitted to queue the image '%s' " "for caching." % args_id) utils.print_err(msg) except exc.HTTPException as e: msg = _("'%s': Unable to queue image '%s' for caching." % (e, args_id)) utils.print_err(msg) @utils.arg('--sort-key', default='status', choices=tasks.SORT_KEY_VALUES, help=_('Sort task list by specified field.')) @utils.arg('--sort-dir', default='desc', choices=tasks.SORT_DIR_VALUES, help=_('Sort task list in specified direction.')) @utils.arg('--page-size', metavar='<SIZE>', default=None, type=int, help=_('Number of tasks to request in each paginated request.')) @utils.arg('--type', metavar='<TYPE>', help=_('Filter tasks to those that have this type.')) @utils.arg('--status', metavar='<STATUS>', help=_('Filter tasks to those that have this status.')) def do_task_list(gc, args): """List tasks you can access.""" filter_keys = ['type', 'status'] filter_items = [(key, getattr(args, key)) for key in filter_keys] filters = dict([item for item in filter_items if item[1] is not None]) kwargs = {'filters': filters} if args.page_size is not None: kwargs['page_size'] = args.page_size kwargs['sort_key'] = args.sort_key kwargs['sort_dir'] = args.sort_dir tasks = gc.tasks.list(**kwargs) columns = ['ID', 'Type', 'Status', 'Owner'] utils.print_list(tasks, columns) @utils.arg('id', metavar='<TASK_ID>', help=_('ID of task to describe.')) def do_task_show(gc, args): """Describe a specific task.""" task = gc.tasks.get(args.id) ignore = ['self', 'schema'] task = dict([item for item in task.items() if item[0] not in ignore]) utils.print_dict(task) @utils.arg('--type', metavar='<TYPE>', help=_('Type of Task. Please refer to Glance schema or ' 'documentation to see which tasks are supported.')) @utils.arg('--input', metavar='<STRING>', default='{}', help=_('Parameters of the task to be launched')) def do_task_create(gc, args): """Create a new task.""" if not (args.type and args.input): utils.exit('Unable to create task. Specify task type and input.') else: try: input = json.loads(args.input) except ValueError: utils.exit('Failed to parse the "input" parameter. Must be a ' 'valid JSON object.') task_values = {'type': args.type, 'input': input} task = gc.tasks.create(**task_values) ignore = ['self', 'schema'] task = dict([item for item in task.items() if item[0] not in ignore]) utils.print_dict(task) ���������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/v2/tasks.py��������������������������������������������������0000664�0001750�0001750�00000010446�00000000000�022174� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 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. import urllib.parse from oslo_utils import encodeutils import warlock from glanceclient.common import utils from glanceclient.v2 import schemas DEFAULT_PAGE_SIZE = 20 SORT_DIR_VALUES = ('asc', 'desc') SORT_KEY_VALUES = ('id', 'type', 'status') class Controller(object): def __init__(self, http_client, schema_client): self.http_client = http_client self.schema_client = schema_client @utils.memoized_property def model(self): schema = self.schema_client.get('task') return warlock.model_factory(schema.raw(), base_class=schemas.SchemaBasedModel) @utils.add_req_id_to_generator() def list(self, **kwargs): """Retrieve a listing of Task objects. :param page_size: Number of tasks to request in each paginated request :returns: generator over list of Tasks """ def paginate(url): resp, body = self.http_client.get(url) for task in body['tasks']: yield task, resp try: next_url = body['next'] except KeyError: return else: for task, resp in paginate(next_url): yield task, resp filters = kwargs.get('filters', {}) if not kwargs.get('page_size'): filters['limit'] = DEFAULT_PAGE_SIZE else: filters['limit'] = kwargs['page_size'] if 'marker' in kwargs: filters['marker'] = kwargs['marker'] sort_key = kwargs.get('sort_key') if sort_key is not None: if sort_key in SORT_KEY_VALUES: filters['sort_key'] = sort_key else: raise ValueError('sort_key must be one of the following: %s.' % ', '.join(SORT_KEY_VALUES)) sort_dir = kwargs.get('sort_dir') if sort_dir is not None: if sort_dir in SORT_DIR_VALUES: filters['sort_dir'] = sort_dir else: raise ValueError('sort_dir must be one of the following: %s.' % ', '.join(SORT_DIR_VALUES)) for param, value in filters.items(): if isinstance(value, str): filters[param] = encodeutils.safe_encode(value) url = '/v2/tasks?%s' % urllib.parse.urlencode(filters) for task, resp in paginate(url): # NOTE(flwang): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict task.pop('self', None) yield self.model(**task), resp @utils.add_req_id_to_object() def get(self, task_id): """Get a task based on given task id.""" url = '/v2/tasks/%s' % task_id resp, body = self.http_client.get(url) # NOTE(flwang): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict body.pop('self', None) return self.model(**body), resp @utils.add_req_id_to_object() def create(self, **kwargs): """Create a new task.""" url = '/v2/tasks' task = self.model() for (key, value) in kwargs.items(): try: setattr(task, key, value) except warlock.InvalidOperation as e: raise TypeError(encodeutils.exception_to_unicode(e)) resp, body = self.http_client.post(url, data=task) # NOTE(flwang): remove 'self' for now until we have an elegant # way to pass it into the model constructor without conflict body.pop('self', None) return self.model(**body), resp ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/glanceclient/v2/versions.py�����������������������������������������������0000664�0001750�0001750�00000001661�00000000000�022716� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# Copyright 2015 OpenStack Foundation # Copyright 2015 Huawei 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. class VersionController(object): def __init__(self, http_client): self.http_client = http_client def list(self): """List all versions.""" url = '/versions' resp, body = self.http_client.get(url) return body.get('versions', None) �������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0861638 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/python_glanceclient.egg-info/���������������������������������������������0000775�0001750�0001750�00000000000�00000000000�023234� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740209.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/python_glanceclient.egg-info/PKG-INFO�������������������������������������0000664�0001750�0001750�00000007455�00000000000�024344� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������Metadata-Version: 1.2 Name: python-glanceclient Version: 4.4.0 Summary: OpenStack Image API Client Library Home-page: https://docs.openstack.org/python-glanceclient/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: Apache License, Version 2.0 Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-glanceclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html :alt: The following tags have been asserted for Python bindings to the OpenStack Images API: "project:official", "stable:follows-policy", "vulnerability:managed". Follow the link for an explanation of these tags. .. NOTE(rosmaita): the alt text above will have to be updated when additional tags are asserted for python-glanceclient. (The SVG in the governance repo is updated automatically.) .. Change things from this point on =========================================== Python bindings to the OpenStack Images API =========================================== .. image:: https://img.shields.io/pypi/v/python-glanceclient.svg :target: https://pypi.org/project/python-glanceclient/ :alt: Latest Version This is a client library for Glance built on the OpenStack Images API. It provides a Python API (the ``glanceclient`` module) and a command-line tool (``glance``). This library fully supports the v1 Images API, while support for the v2 API is in progress. Development takes place via the usual OpenStack processes as outlined in the `developer guide <https://docs.openstack.org/infra/manual/developers.html>`_. The master repository is in `Git <https://opendev.org/openstack/python-glanceclient>`_. See release notes and more at `<https://docs.openstack.org/python-glanceclient/latest/>`_. * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.org/project/python-glanceclient .. _Online Documentation: https://docs.openstack.org/python-glanceclient/latest/ .. _Launchpad project: https://launchpad.net/python-glanceclient .. _Blueprints: https://blueprints.launchpad.net/python-glanceclient .. _Bugs: https://bugs.launchpad.net/python-glanceclient .. _Source: https://opendev.org/openstack/python-glanceclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/glance-specs/ .. _Release notes: https://docs.openstack.org/releasenotes/python-glanceclient 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 :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Requires-Python: >=3.8 �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740210.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/python_glanceclient.egg-info/SOURCES.txt����������������������������������0000664�0001750�0001750�00000015101�00000000000�025116� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������.coveragerc .mailmap .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog HACKING.rst LICENSE README.rst requirements.txt run_tests.sh setup.cfg setup.py test-requirements.txt tox.ini doc/requirements.txt doc/source/conf.py doc/source/index.rst doc/source/cli/details.rst doc/source/cli/glance.rst doc/source/cli/index.rst doc/source/cli/property-keys.rst doc/source/reference/apiv2.rst doc/source/reference/index.rst glanceclient/__init__.py glanceclient/_i18n.py glanceclient/client.py glanceclient/exc.py glanceclient/shell.py glanceclient/common/__init__.py glanceclient/common/exceptions.py glanceclient/common/http.py glanceclient/common/https.py glanceclient/common/progressbar.py glanceclient/common/utils.py glanceclient/tests/__init__.py glanceclient/tests/utils.py glanceclient/tests/functional/README.rst glanceclient/tests/functional/__init__.py glanceclient/tests/functional/base.py glanceclient/tests/functional/v1/__init__.py glanceclient/tests/functional/v1/test_readonly_glance.py glanceclient/tests/functional/v2/__init__.py glanceclient/tests/functional/v2/test_http_headers.py glanceclient/tests/functional/v2/test_readonly_glance.py glanceclient/tests/unit/__init__.py glanceclient/tests/unit/test_base.py glanceclient/tests/unit/test_client.py glanceclient/tests/unit/test_exc.py glanceclient/tests/unit/test_http.py glanceclient/tests/unit/test_progressbar.py glanceclient/tests/unit/test_shell.py glanceclient/tests/unit/test_ssl.py glanceclient/tests/unit/test_utils.py glanceclient/tests/unit/v1/__init__.py glanceclient/tests/unit/v1/test_image_members.py glanceclient/tests/unit/v1/test_images.py glanceclient/tests/unit/v1/test_shell.py glanceclient/tests/unit/v1/test_versions.py glanceclient/tests/unit/v2/__init__.py glanceclient/tests/unit/v2/base.py glanceclient/tests/unit/v2/fixtures.py glanceclient/tests/unit/v2/test_cache.py glanceclient/tests/unit/v2/test_client_requests.py glanceclient/tests/unit/v2/test_images.py glanceclient/tests/unit/v2/test_info.py glanceclient/tests/unit/v2/test_members.py glanceclient/tests/unit/v2/test_metadefs_namespaces.py glanceclient/tests/unit/v2/test_metadefs_objects.py glanceclient/tests/unit/v2/test_metadefs_properties.py glanceclient/tests/unit/v2/test_metadefs_resource_types.py glanceclient/tests/unit/v2/test_metadefs_tags.py glanceclient/tests/unit/v2/test_schemas.py glanceclient/tests/unit/v2/test_shell_v2.py glanceclient/tests/unit/v2/test_tags.py glanceclient/tests/unit/v2/test_tasks.py glanceclient/tests/unit/v2/test_versions.py glanceclient/tests/unit/var/badcert.crt glanceclient/tests/unit/var/ca.crt glanceclient/tests/unit/var/certificate.crt glanceclient/tests/unit/var/expired-cert.crt glanceclient/tests/unit/var/privatekey.key glanceclient/tests/unit/var/wildcard-certificate.crt glanceclient/tests/unit/var/wildcard-san-certificate.crt glanceclient/v1/__init__.py glanceclient/v1/client.py glanceclient/v1/image_members.py glanceclient/v1/images.py glanceclient/v1/shell.py glanceclient/v1/versions.py glanceclient/v1/apiclient/__init__.py glanceclient/v1/apiclient/base.py glanceclient/v1/apiclient/exceptions.py glanceclient/v1/apiclient/utils.py glanceclient/v2/__init__.py glanceclient/v2/cache.py glanceclient/v2/client.py glanceclient/v2/image_members.py glanceclient/v2/image_schema.py glanceclient/v2/image_tags.py glanceclient/v2/images.py glanceclient/v2/info.py glanceclient/v2/metadefs.py glanceclient/v2/namespace_schema.py glanceclient/v2/resource_type_schema.py glanceclient/v2/schemas.py glanceclient/v2/shell.py glanceclient/v2/tasks.py glanceclient/v2/versions.py python_glanceclient.egg-info/PKG-INFO python_glanceclient.egg-info/SOURCES.txt python_glanceclient.egg-info/dependency_links.txt python_glanceclient.egg-info/entry_points.txt python_glanceclient.egg-info/not-zip-safe python_glanceclient.egg-info/pbr.json python_glanceclient.egg-info/requires.txt python_glanceclient.egg-info/top_level.txt releasenotes/notes/.placeholder releasenotes/notes/2.16.0_Release-43ebe06b74a272ba.yaml releasenotes/notes/2.17.0_Release-c67392be3b428d10.yaml releasenotes/notes/3.1.0_Release-1337ddc753b88905.yaml releasenotes/notes/3.6.0_Release-04d3b5017747290b.yaml releasenotes/notes/4.3.0_Release-1a7acbd472e16c72.yaml releasenotes/notes/4.4.0_Release-a3c89184f345e5a2.yaml releasenotes/notes/add-member-get-command-11c15e0a94ecd39a.yaml releasenotes/notes/add-support-for-glance-download-import-method-10525254db3e8e7a.yaml releasenotes/notes/boolean-properties-strict-checking-bdd624b5da81e723.yaml releasenotes/notes/bp-use-keystoneauth-e12f300e58577b13.yaml releasenotes/notes/check-for-md5-59db8fd67870b214.yaml releasenotes/notes/copy-existing-image-619b7e6bc3394446.yaml releasenotes/notes/del_from_store-2d807c3038283907.yaml releasenotes/notes/drop-py-2-7-f10417b8d1dd38fb.yaml releasenotes/notes/drop-python-3-6-and-3-7-0b299b4dc9673c6e.yaml releasenotes/notes/fix-undesirable-raw-python-error-66e3ddaca7b72ae2.yaml releasenotes/notes/fix_1889666-22dc97ce577eccc6.yaml releasenotes/notes/headers-encoding-bug-rocky-889ccd885a9cc4e8.yaml releasenotes/notes/hidden-images-support-9e2277ad62bf0d31.yaml releasenotes/notes/http-headers-per-rfc-8187-aafa3199f863be81.yaml releasenotes/notes/image-tasks-api-ee3ea043557a1dfa.yaml releasenotes/notes/log-request-id-e7f67a23a0ed5c7b.yaml releasenotes/notes/multi-store-import-45d05a6193ef2c04.yaml releasenotes/notes/multi-store-support-acc7ad0e7e8b6f99.yaml releasenotes/notes/multihash-download-verification-596e91bf7b68e7db.yaml releasenotes/notes/multihash-filter-ef2a48dc48fae9dc.yaml releasenotes/notes/multihash-support-f1474590cf3ef5cf.yaml releasenotes/notes/pike-relnote-2c77b01aa8799f35.yaml releasenotes/notes/return-request-id-to-caller-47f4c0a684b1d88e.yaml releasenotes/notes/rm-deprecate-ssl-opts-c88225a4ba2285ad.yaml releasenotes/notes/rocky-2.11.0-ba936fd5e969198d.yaml releasenotes/notes/sess_client_grid-3c2101609110f413.yaml releasenotes/notes/validation-data-support-dfb2463914818cd2.yaml releasenotes/source/2023.1.rst releasenotes/source/conf.py releasenotes/source/earlier.rst 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/yoga.rst releasenotes/source/zed.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder tools/fix_ca_bundle.sh tools/glance.bash_completion tools/with_venv.sh���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740209.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/python_glanceclient.egg-info/dependency_links.txt�������������������������0000664�0001750�0001750�00000000001�00000000000�027302� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740209.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/python_glanceclient.egg-info/entry_points.txt�����������������������������0000664�0001750�0001750�00000000064�00000000000�026532� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[console_scripts] glance = glanceclient.shell:main ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740209.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/python_glanceclient.egg-info/not-zip-safe���������������������������������0000664�0001750�0001750�00000000001�00000000000�025462� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������ �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740209.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/python_glanceclient.egg-info/pbr.json�������������������������������������0000664�0001750�0001750�00000000056�00000000000�024713� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������{"git_version": "62e6fc8", "is_release": true}����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740209.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/python_glanceclient.egg-info/requires.txt���������������������������������0000664�0001750�0001750�00000000237�00000000000�025636� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������PrettyTable>=0.7.1 keystoneauth1>=3.6.2 oslo.i18n>=3.15.3 oslo.utils>=3.33.0 pbr!=2.1.0,>=2.0.0 pyOpenSSL>=17.1.0 requests>=2.14.2 warlock>=1.2.0 wrapt>=1.7.0 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740209.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/python_glanceclient.egg-info/top_level.txt��������������������������������0000664�0001750�0001750�00000000015�00000000000�025762� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������glanceclient �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0741637 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/�������������������������������������������������������������0000775�0001750�0001750�00000000000�00000000000�020202� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0901637 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/�������������������������������������������������������0000775�0001750�0001750�00000000000�00000000000�021332� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/.placeholder�������������������������������������������0000664�0001750�0001750�00000000000�00000000000�023603� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/2.16.0_Release-43ebe06b74a272ba.yaml�������������������0000664�0001750�0001750�00000000565�00000000000�026672� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- prelude: > This version of python-glanceclient adds Python 3.6 classifier and gating on Python 3.7 environment. fixes: - | * Bug 1788271_: Add image-list filter for multihash * Bug 1598714_: Remove redundant information from error message .. _1788271: https://code.launchpad.net/bugs/1788271 .. _1598714: https://code.launchpad.net/bugs/1598714 �������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/2.17.0_Release-c67392be3b428d10.yaml�������������������0000664�0001750�0001750�00000002457�00000000000�026545� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- prelude: | This version of python-glanceclient finalizes client-side support for the Glance multiple stores feature. See the `Multi Store Support <https://docs.openstack.org/glance/latest/admin/multistores.html>`_ section of the Glance documentation for more information. Support for Glance multiple stores has been available on an EXPERIMENTAL basis since release 2.12.0. For the Train release, the Image service has finalized how API users interact with multiple stores. See the "Upgrade Notes" section of this document for information about changes this has necessitated in multistore support in the glanceclient. fixes: - | Bug 1822052_: HTTPClient: actually set a timeout for requests .. _1822052: https://code.launchpad.net/bugs/1822052 upgrade: - | The following Command Line Interface calls now take a ``--store`` option: * ``glance image-create`` * ``glance image-create-via-import`` * ``glance image-upload`` * ``glance image-import`` The value for this option is a store identifier. The list of available stores may be obtained from the ``glance stores-info`` command. - | The ``--backend`` option, available on some commands on an experimental basis since release 2.12.0, is no longer available. Use ``--store`` instead. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/3.1.0_Release-1337ddc753b88905.yaml��������������������0000664�0001750�0001750�00000001614�00000000000�026404� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- prelude: | This version of python-glanceclient finalizes client-side support for the Glance import image in multiple stores, copy existing image in multiple stores and delete image from single store. fixes: - | Bug 1838694: glanceclient doesn't cleanup session it creates if one is not provided .. _1838694: https://bugs.launchpad.net/python-glanceclient/+bug/1838694 upgrade: - | The following Command Line Interface calls now take ``--stores``, ``--all-stores`` and ``--allow-failure`` option: * ``glance image-create-via-import`` * ``glance image-import`` The value for ``--stores`` option is a list of store identifiers. The list of available stores may be obtained from the ``glance stores-info`` command. The value for ``--all-stores`` option could be True or False. The value for ``--allow-failure`` option could be True or False. ��������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/3.6.0_Release-04d3b5017747290b.yaml��������������������0000664�0001750�0001750�00000003577�00000000000�026324� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- prelude: | This version of python-glanceclient introduces new commands ``usage`` to fetch quota related usage information and ``cache-clear``, ``cache-delete``, ``cache-list`` and ``cache-queue`` for cache related operations. features: - | Adds support for ``usage`` command which will report the usage of unified limits configured. The following commands have been added to the command line interface: * ``usage`` - Get quota usage information. - | Client support has been added for the new caching functionality introduced into Glance in this cycle. This feature is only available in the Images API version 2 when the caching middleware is enabled in the Glance service that the client is contacting. The following commands have been added to the command line interface: * ``cache-clear`` - Delete all the images from cache and queued for caching * ``cache-delete`` - Delete the specified image from cache or from the queued list * ``cache-list`` - List all the images which are cached or being queued for caching * ``cache-queue`` - Queue specified image(s) for caching. - | Client side support has been added to fetch store specific configuration information. With sufficient permissions, this will display additional information about the stores. - | Client side support has been added to provide the facility of appending the tags while creating new multiple tags rather than overwriting the existing tags. upgrade: - | The following Command Line Interface call now takes ``--detail`` option: * | ``glance stores-info`` | The value for ``--detail`` option could be True or False. - | The following Command Line Interface call now takes ``--append`` option: * | ``glance md-tag-create-multiple`` | The value for ``--append`` option could be True or False. ���������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/4.3.0_Release-1a7acbd472e16c72.yaml��������������������0000664�0001750�0001750�00000000421�00000000000�026602� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- fixes: - | Bug 911805_: Scriptify the generation of man pages - | Bug 1561828_: unallowed and read-only parameters in namspace and resouce_type .. _911805: https://code.launchpad.net/bugs/911805 .. _1561828: https://code.launchpad.net/bugs/1561828 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/4.4.0_Release-a3c89184f345e5a2.yaml��������������������0000664�0001750�0001750�00000000454�00000000000�026465� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- fixes: - | Bug 2012442_: import image with glance-download return 400 - | Bug 1934626_: glanceclient has no support to add type while creating md-property for namespace .. _2012442: https://code.launchpad.net/bugs/2012442 .. _1934626: https://code.launchpad.net/bugs/1934626 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/add-member-get-command-11c15e0a94ecd39a.yaml�����������0000664�0001750�0001750�00000000175�00000000000�030673� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- fixes: - | Bug 1938154_: Added member-get command .. _1938154: https://bugs.launchpad.net/glance/+bug/1938154 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000220�00000000000�011447� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������122 path=python-glanceclient-4.4.0/releasenotes/notes/add-support-for-glance-download-import-method-10525254db3e8e7a.yaml 22 mtime=1688740181.0 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/add-support-for-glance-download-import-method-10525254d0000664�0001750�0001750�00000000234�00000000000�033175� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - | Add support for new ``glance-download`` image-import method to import image from another glance/region in federated deployment. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000205�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������111 path=python-glanceclient-4.4.0/releasenotes/notes/boolean-properties-strict-checking-bdd624b5da81e723.yaml 22 mtime=1688740181.0 �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/boolean-properties-strict-checking-bdd624b5da81e723.yam0000664�0001750�0001750�00000000636�00000000000�033230� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- prelude: > fixes: - | * Bug 1607317_: metadata def namespace update CLI is not working as expected for parameter "protected" .. _1607317: https://code.launchpad.net/bugs/1607317 other: - | Boolean arguments now expect one of the following values: '0', '1', 'f', 'false', 'n', 'no', 'off', 'on', 't', 'true', 'y', 'yes' (case-insensitive). This will not change anything for most users. ��������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/bp-use-keystoneauth-e12f300e58577b13.yaml��������������0000664�0001750�0001750�00000000776�00000000000�030215� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- prelude: > Switch to using keystoneauth for session and auth plugins. other: - > [`bp use-keystoneauth <https://blueprints.launchpad.net/python-glanceclient/+spec/use-keystoneauth>`_] As of keystoneclient 2.2.0, the session and auth plugins code has been deprecated. These modules have been moved to the keystoneauth library. Consumers of the session and plugin modules are encouraged to move to keystoneauth. Note that there should be no change to end users of glanceclient. ��././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/check-for-md5-59db8fd67870b214.yaml��������������������0000664�0001750�0001750�00000001316�00000000000�026713� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- other: -| For legacy (pre-Rocky) images that do not contain "multihash" metadata, or when the ``--allow-md5-fallback`` option is used in cases where the multihash metadata is present but the specified algorithm is not available to the glanceclient, the glanceclient uses an MD5 checksum to validate the download. When operating in a FIPS-compliant environment, however, the MD5 algorithm may be unavailable to the glanceclient. In such a case, (that is, when the MD5 checksum information is available to the glanceclient but the MD5 algorithm is not), the glanceclient will fail the download as corrupt because it cannot prove otherwise. This is consistent with current behavior. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/copy-existing-image-619b7e6bc3394446.yaml��������������0000664�0001750�0001750�00000000174�00000000000�030171� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - | Adds support for copy-image import method which will copy existing images into multiple stores. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/del_from_store-2d807c3038283907.yaml�������������������0000664�0001750�0001750�00000000117�00000000000�027140� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - | Support for deleting the image data from single store. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/drop-py-2-7-f10417b8d1dd38fb.yaml����������������������0000664�0001750�0001750�00000000337�00000000000�026410� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- upgrade: - | Python 2.7 support has been dropped. Last release of python-glanceclient to support py2.7 is OpenStack Train. The minimum version of Python now supported by python-glanceclient is Python 3.6. �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/drop-python-3-6-and-3-7-0b299b4dc9673c6e.yaml����������0000664�0001750�0001750�00000000201�00000000000�030270� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- upgrade: - | Python 3.6 & 3.7 support has been dropped. The minimum version of Python now supported is Python 3.8. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/fix-undesirable-raw-python-error-66e3ddaca7b72ae2.yaml�0000664�0001750�0001750�00000000402�00000000000�033160� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- fixes: - | `Bug #1903727 <https://bugs.launchpad.net/python-glanceclient/+bug/1903727>`_: Fixed raw Python error message when using ``glance`` without a subcommand while passing an optional argument, such as ``--os-image-api-version``. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/fix_1889666-22dc97ce577eccc6.yaml����������������������0000664�0001750�0001750�00000000257�00000000000�026332� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- fixes: - | Bug 1889666_: 'stores' property added when using image-create-via-import --stores <list> .. _1889666: https://bugs.launchpad.net/glance/+bug/1889666 �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/headers-encoding-bug-rocky-889ccd885a9cc4e8.yaml�������0000664�0001750�0001750�00000000243�00000000000�031642� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- fixes: - | * Bug 1783290_: glance will return 401 error if the request token contains url code .. _1783290: https://code.launchpad.net/bugs/1783290 �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/hidden-images-support-9e2277ad62bf0d31.yaml������������0000664�0001750�0001750�00000001742�00000000000�030644� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - | This release adds client support for the Glance "hidden images" feature described in the spec `Operator maintained images lifecycle <https://specs.openstack.org/openstack/glance-specs/specs/rocky/approved/glance/operator-image-workflow.html>`_. Support in the glanceclient includes the following: - The following calls now allow the specification of a ``--hidden`` option that takes a boolean value (``true`` or ``false``). When this option is omitted, the default value is ``false``. * ``image-create`` * ``image-create-via-import`` * ``image-update`` - The ``image-list`` call now allows the specification of a ``--hidden`` filter that takes a boolean value (``true`` or ``false``). By default, "hidden" images are not displayed in the ``image-list`` response (that's why they're called "hidden"). To see those images, use ``--hidden true`` as a filter on the ``image-list`` call. ������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/http-headers-per-rfc-8187-aafa3199f863be81.yaml��������0000664�0001750�0001750�00000001060�00000000000�031047� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- fixes: - | Bug 1766235_: Handle HTTP headers per RFC 8187 Previously the glanceclient encoded HTTP headers as UTF-8 bytes. According to `RFC 8187`_, however, headers should be encoded as 7-bit ASCII. The glanceclient now sends all headers as 7-bit ASCII. It handles unicode strings by percent-encoding_ them before sending them in headers. .. _1766235: https://code.launchpad.net/bugs/1766235 .. _RFC 8187: https://tools.ietf.org/html/rfc8187 .. _percent-encoding: https://tools.ietf.org/html/rfc3986#section-2.1 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/image-tasks-api-ee3ea043557a1dfa.yaml������������������0000664�0001750�0001750�00000000120�00000000000�027530� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - | Support for showing tasks associated with given image. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/log-request-id-e7f67a23a0ed5c7b.yaml�������������������0000664�0001750�0001750�00000000311�00000000000�027427� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - Added support to log 'x-openstack-request-id' for each api call. Please refer, https://blueprints.launchpad.net/python-glanceclient/+spec/log-request-id for more details. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/multi-store-import-45d05a6193ef2c04.yaml���������������0000664�0001750�0001750�00000000216�00000000000�030140� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - | Adds support for multi-store import where user can import image into multiple backend stores with single command. ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/multi-store-support-acc7ad0e7e8b6f99.yaml��������������0000664�0001750�0001750�00000003237�00000000000�030672� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - | This release adds client support for the Glance feature `multi-store backend support <https://specs.openstack.org/openstack/glance-specs/specs/rocky/approved/glance/multi-store.html>`_, introduced in the Rocky release. This feature allows end users to direct uploaded or imported image data to a particular backend when a cloud operator has configured the Image Service to use multiple backends. The available backends are discoverable by making the ``stores-info`` call, which will return a list of available backends. The list contains an identifier (``id``) and a ``description`` of each available backend. The default backend is indicated in this response. When uploading or importing an image, the glanceclient now accepts the ``--backend`` option. Its value must be the ``id`` of a backend configured in the cloud against which the call is being made. This option may also be configured by exporting the ``OS_IMAGE_BACKEND`` environment variable with the ``id`` of a configured backend as its value. Some other points to keep in mind: - If no backend is specified, the image data is stored in the default backend. - If the version of the Image Service API contacted does not support multi-store backends, the option is silently ignored and the image data is stored in the default backend. - If an invalid backend identifier is used, the glanceclient will exit with an error message. - Backend identifiers and their meanings are unique to each cloud. Consult the ``stores-info`` call and your cloud provider's documentation for details. �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/multihash-download-verification-596e91bf7b68e7db.yaml��0000664�0001750�0001750�00000004506�00000000000�033027� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - | This release adds verification of image data downloads using the Glance "multihash" feature introduced in the OpenStack Rocky release. When the ``os_hash_value`` is populated on an image, the glanceclient will verify this value by computing the hexdigest of the downloaded data using the algorithm specified by the image's ``os_hash_algo`` property. Because the secure hash algorithm specified is determined by the cloud provider, it is possible that the ``os_hash_algo`` may identify an algorithm not available in the version of the Python ``hashlib`` library used by the client. In such a case the download will fail due to an unsupported hash type. In the event this occurs, a new option, ``--allow-md5-fallback``, is introduced to the ``image-download`` command. When present, this option will allow the glanceclient to use the legacy MD5 checksum to verify the downloaded data if the secure hash algorithm specified by the ``os_hash_algo`` image property is not supported. Note that the fallback is *not* used in the case where the algorithm is supported but the hexdigest of the downloaded data does not match the ``os_hash_value``. In that case the download fails regardless of whether the option is present or not. Whether using the ``--allow-md5-fallback`` option is a good idea depends upon the user's expectations for the verification. MD5 is an insecure hashing algorithm, so if you are interested in making sure that the downloaded image data has not been replaced by a datastream carefully crafted to have the same MD5 checksum, then you should not use the fallback. If, however, you are using Glance in a trusted environment and your interest is simply to verify that no bits have flipped during the data transfer, the MD5 fallback is sufficient for that purpose. That being said, it is our recommendation that the multihash should be used whenever possible. security: - | This release of the glanceclient uses the Glance "multihash" feature, introduced in Rocky, to use a secure hashing algorithm to verify the integrity of downloaded data. Legacy images without the "multihash" image properties (``os_hash_algo`` and ``os_hash_value``) are verified using the MD5 ``checksum`` image property. ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/multihash-filter-ef2a48dc48fae9dc.yaml�����������������0000664�0001750�0001750�00000000520�00000000000�030222� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - | For parity with the old ``checksum`` field, this release adds the ability for CLI users to filter the image list based upon a particular multihash value using the ``--hash <HASH_VALUE>`` option. Issue the command: .. code-block:: none glance help image-list for more information. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/multihash-support-f1474590cf3ef5cf.yaml����������������0000664�0001750�0001750�00000001410�00000000000�030230� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - | This release adds client support for the Glance "multihash" feature introduced in Rocky. This feature introduces two new image properties, ``os_hash_algo`` and ``os_hash_value``. The content of ``os_hash_algo`` is an algorithm identifier recognized by the Python ``hashlib`` library. The ``os_hash_value`` is a hexdigest of the image data computed using this algorithm. The ``os_hash_algo`` is not end-user settable; it is configured in Glance by the cloud operator. In the glanceclient, the feature is limited solely to the display of these values. If the "multihash" properties are not available on an image, their values are displayed as ``None`` in the glanceclient image-show and image-list responses. ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/pike-relnote-2c77b01aa8799f35.yaml���������������������0000664�0001750�0001750�00000007761�00000000000�026773� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- prelude: > This was a quiet development cycle for the python-glanceclient. There were several improvements made in the testing code, and the documentation was reorganized in accord with the `new standard layout <http://specs.openstack.org/openstack/docs-specs/specs/pike/os-manuals-migration.html>`_ for OpenStack projects. The main feature in this release is the addition of support for the new image import functionality introduced into Glance during this cycle. features: - | Client support has been added for the new image import functionality introduced into Glance in this cycle. This is a feature of the Images API version 2 only, and it is disabled by default in Glance. The following commands have been added to the command line interface: * ``import-info`` - gets information about the import configuration of the target Glance * ``image-stage`` - uploads image data to the staging area * ``image-import`` - initiates the import process for a previously created image record whose image data is currently in the staging area * ``image-create-via-import`` - this is an EXPERIMENTAL command that compresses the three-step import process of the Images API version 2 into a single call from the command line, just as the current client ``image-create`` command compresses a two-step process into one step. *It is EXPERIMENTAL because the name of the command may change or it may be removed entirely in future releases.* The intent is that as Glance image import is adopted by deployers, this command may be renamed to ``image-create`` as it behaves exactly the same from the user's point of view. It is included in this release so that the Glance team can get feedback from deployers and end users. fixes: - | The following are some highlights of the bug fixes included in this release. * Bug 1659010_: Help text inaccurate for container_format, disk_format * Bug 1570766_: Fix 'UnicodeEncodeError' for unicode values in url * Bug 1583919_: --no-ssl-compression is deprecated .. _1659010: https://code.launchpad.net/bugs/1659010 .. _1570766: https://code.launchpad.net/bugs/1570766 .. _1583919: https://code.launchpad.net/bugs/1583919 other: - | The deprecated ``--no-ssl-compression`` option to the python-glanceclient command line interface has been removed_. The option has been inoperative_ since the Liberty release. - | An optimization_ was added in the case where an image download is requested from the command line interface without specifying either a filename destination for the data or output redirection. The optimization properly delays opening a connection to the server until *after* the CLI has verified that the user has specified a location for the downloaded data. In the pre-optimized code, if a user did not have permission to download the requested image or if the image had no data associated with it, the CLI would fail with an appropriate message when the client attempted to create the connection but before it had determined that there was no place to put the data. With this optimization, a user will not be able to "probe" the server to see whether image data is available without specifying either the ``--file`` option or command line redirection. - | The argument to the ``--profile`` option of the command line interface may now be specified_ by setting the value of the ``OS_PROFILE`` environment variable. .. _removed: https://opendev.org/openstack/python-glanceclient/commit/28c003dc1179ddb3124fd30c6f525dd341ae9213 .. _inoperative: https://specs.openstack.org/openstack/glance-specs/specs/liberty/approved/remove-special-client-ssl-handling.html .. _optimization: https://opendev.org/openstack/python-glanceclient/commit/1df55dd952fe52c1c1fc2583326d017275b01ddc .. _specified: https://opendev.org/openstack/python-glanceclient/commit/c0f88d5fc0fd947319e022cfeba21bcb15635316 ���������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/return-request-id-to-caller-47f4c0a684b1d88e.yaml������0000664�0001750�0001750�00000000522�00000000000�031720� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - Added support to return "x-openstack-request-id" header in request_ids attribute for better tracing. | For ex. | >>> from glanceclient import Client | >>> glance = Client('2', endpoint='OS_IMAGE_ENDPOINT', token='OS_AUTH_TOKEN') | >>> res = glance.images.get('<image_id>') | >>> res.request_ids ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/rm-deprecate-ssl-opts-c88225a4ba2285ad.yaml������������0000664�0001750�0001750�00000000436�00000000000�030562� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- other: - | The following options to the command line client, which have been deprecated since Icehouse, have been removed: * ``--key-file`` (use ``--os-key`` instead) * ``--ca-file`` (use ``--os-cacert`` instead) * ``--cert-file`` (use ``--os-cert`` instead) ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/rocky-2.11.0-ba936fd5e969198d.yaml���������������������0000664�0001750�0001750�00000000610�00000000000�026323� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- issues: - | Help texts for some properties has possibly outdated links. Please refer to the documentation of the deployment while we try to find a way how to document these references in a way that they do not point user to false information. fixes: - | * Bug 1762044_: Sync schema with glance-api service .. _1762044: https://code.launchpad.net/bugs/1762044 ������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/sess_client_grid-3c2101609110f413.yaml�����������������0000664�0001750�0001750�00000000233�00000000000�027422� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- fixes: - | * Bug 1886650_: Glance client does not correctly forward global request IDs .. _1886650: https://code.launchpad.net/bugs/1886650 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/notes/validation-data-support-dfb2463914818cd2.yaml����������0000664�0001750�0001750�00000000756�00000000000�031141� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������--- features: - | Support for embedding validation data (checksum and multihash) when adding a location to an image. Requires the Stein release server-side. The ``glance.images.add_location()`` method now accepts an optional argument ``validation_data``, in the form of a dictionary containing ``checksum``, ``os_hash_algo`` and ``os_hash_value``. The ``location-add`` command now accepts optional arguments ``--checksum``, ``--hash-algo`` and ``--hash-value``. ������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000034�00000000000�011452� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������28 mtime=1688740210.0901637 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/������������������������������������������������������0000775�0001750�0001750�00000000000�00000000000�021502� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/2023.1.rst��������������������������������������������0000664�0001750�0001750�00000000202�00000000000�022753� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.1 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000033�00000000000�011451� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������27 mtime=1688740210.094164 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/_static/����������������������������������������������0000775�0001750�0001750�00000000000�00000000000�023130� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/_static/.placeholder����������������������������������0000664�0001750�0001750�00000000000�00000000000�025401� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000033�00000000000�011451� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������27 mtime=1688740210.094164 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/_templates/�������������������������������������������0000775�0001750�0001750�00000000000�00000000000�023637� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/_templates/.placeholder�������������������������������0000664�0001750�0001750�00000000000�00000000000�026110� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/conf.py�����������������������������������������������0000664�0001750�0001750�00000021447�00000000000�023011� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # Glance Release Notes documentation build configuration file, created by # sphinx-quickstart on Tue Nov 3 17:40:50 2015. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # 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 = [ 'reno.sphinxext', 'openstackdocstheme' ] # 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 = 'glanceclient Release Notes' copyright = '2016, Glance Developers' openstackdocs_repo_name = 'openstack/python-glanceclient' openstackdocs_bug_project = 'python-glanceclient' openstackdocs_bug_tag = '' openstackdocs_auto_name = False # Release notes are not versioned, so we don't need to set version or release version = '' release = '' # 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 = {} # The name for this set of Sphinx documents. If None, it defaults to # "<project> v<release> documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. # html_last_updated_fmt = '%b %d, %Y' # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a <link> 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 = 'GlanceClientReleaseNotesdoc' # -- 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', 'glanceclientReleaseNotes.tex', 'glanceclient Release Notes Documentation', 'Glance 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', 'glanceclientreleasenotes', 'glanceclient Release Notes Documentation', ['Glance 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', 'glanceclientReleaseNotes', 'glanceclient Release Notes Documentation', 'Glance Developers', 'glanceclientReleaseNotes', 'Python bindings for the OpenStack Image service.', '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/'] �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/earlier.rst�������������������������������������������0000664�0001750�0001750�00000056017�00000000000�023670� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������================== Earlier Releases ================== 1.2.0 ----- * This release consists mainly bugfixes since Liberty release. * Some functionality has been added, documentation improved. * Trivial & typo fixed and requirement changes not included below. * 1511180_: Add versions list function * 1508356_: Added reactivate/deactivate image using CLI * 1510340_: Fix the missing help descripiton of "image-create" * 8a4cd79 Add documentation for running the functional tests * 5a24705 Update docs to recommend KSA instead of KSC * 1507386_: Use clouds.yaml from devstack for functional tests * 4fb3092 Add translation to v2 shell * b51634a improve readme contents * 1480529_: Add support for setting Accept-Language header * 1504058_: Use the subcomand parsed args instead of the base * afd1810 Stop trying to send image_size to the server * 1485407_: Support image deletion in batches in v2 * 1295356_: print usage when no argument is specified for python3 * df0f664 Do not use openstack.common.i18n in glance client * 1f2fefb Use common identity parameters fro keystone client * 1499540_: No auth when token and endpoint are passed * 557acb1 Use dictionary literal for dictionary creation * c6addc7 Replace exception_to_str with oslo.utils function * 1496305_: Don't get the image before deleting it * 1495632_: Fix human readable when size is None * 1489727_: Add parsing the endpoint URL * 1467719_: Add check Identity validate when get schemas .. _1511180: https://bugs.launchpad.net/python-glanceclient/+bug/1511180 .. _1508356: https://bugs.launchpad.net/python-glanceclient/+bug/1508356 .. _1510340: https://bugs.launchpad.net/python-glanceclient/+bug/1510340 .. _1507386: https://bugs.launchpad.net/python-neutronclient/+bug/1507386 .. _1480529: https://bugs.launchpad.net/python-glanceclient/+bug/1480529 .. _1504058: https://bugs.launchpad.net/python-glanceclient/+bug/1504058 .. _1485407: https://bugs.launchpad.net/python-glanceclient/+bug/1485407 .. _1295356: https://bugs.launchpad.net/python-novaclient/+bug/1295356 .. _1499540: https://bugs.launchpad.net/python-glanceclient/+bug/1499540 .. _1496305: https://bugs.launchpad.net/python-glanceclient/+bug/1496305 .. _1495632: https://bugs.launchpad.net/python-glanceclient/+bug/1495632 .. _1489727: https://bugs.launchpad.net/python-glanceclient/+bug/1489727 .. _1467719: https://bugs.launchpad.net/glance/+bug/1467719 1.1.0 ----- * This release provides mainly bugfixes for the bugs discovered after defaulting to v2 API on CLI. If you're using 1.0.0 client, it is highly recommended to upgrade. * 1494259_: Fixes CLI client called without subcommands * 1488914_: Print the reverting back to v1 to stderr * 1487645_: Invalid output running the command 'glance image-show <image_id>' * 1490457_: Don't make `help` require auth parameters * 1491311_: check for None value in utils.safe_header * f0b30f4 Updated from global requirements * 1490462_: Consider `--os-token` when using v2 * 1489381_: Check if v2 is available and fallback * 1491646_: Update path to subunit2html in post_test_hook * 1488892_: Password should be prompted once .. _1494259: https://bugs.launchpad.net/python-glanceclient/+bug/1494259 .. _1488914: https://bugs.launchpad.net/python-glanceclient/+bug/1488914 .. _1487645: https://bugs.launchpad.net/python-glanceclient/+bug/1487645 .. _1490457: https://bugs.launchpad.net/python-glanceclient/+bug/1490457 .. _1491311: https://bugs.launchpad.net/python-glanceclient/+bug/1491311 .. _1490462: https://bugs.launchpad.net/python-glanceclient/+bug/1490462 .. _1489381: https://bugs.launchpad.net/python-glanceclient/+bug/1489381 .. _1491646: https://bugs.launchpad.net/python-glanceclient/+bug/1491646 .. _1488892: https://bugs.launchpad.net/python-glanceclient/+bug/1488892 1.0.0 ----- * This major release of python-glanceclient defaults to using the Images v2 API for the Command Line Interface. This is consistent with the current situation in the Glance project, where the Images v1 API is 'SUPPORTED' and the Images v2 API is 'CURRENT'. Further, it makes the CLI consistent with the client API, which has used the Images v2 API as the default since the Kilo release. A lot of effort has been invested to make the transition as smooth as possible, but we acknowledge that CLI users will encounter backwards incompatibility. * remcustssl_: Remove custom SSL compression handling * 14be607 Add more information show in v2 * 1309272_: Require disk and container format on image-create * 1481729_: Ship the default image schema in the client * 181131e Use API v2 as default * 1477910_: V2: Do not validate image schema when listing * 9284eb4 Updated from global requirements * 1475769_: Add unicode support for properties values in v2 shell * 1479020_: Fix failure to create glance https connection pool * ec0f2df Enable flake8 checks * 1433637_: Extend unittests coverage for v2 tasks module * metatags_: Support for Metadata Definition Catalog for Tags * b48ff98 Fix exception message in Http.py * 1472234_: Fix an issue with broken test on ci * 1473454_: Remove usage of assert_called_once on Mock objects * 9fdd4f1 Add .eggs/* to .gitignore * 0f9aa99 Updated from global requirements * 1468485_: Account for dictionary order in test_shell.py * bp-oslo-ns_: Do not fall back to namespaced oslo.i18n * b10e893 Updated from global requirements * 1465373_: Add v2 support for the marker attribute * 997c12d Import only modules and update tox.ini * 0810805 Updated from global requirements * 1461678_: Close iterables at the end of iteration * bp-session_: Make glanceclient accept a session object * 5e85d61 cleanup openstack-common.conf and sync updated files * 1432701_: Add parameter 'changes-since' for image-list of v1 .. _remcustssl: https://review.opendev.org/#/c/187674 .. _1309272: https://bugs.launchpad.net/python-glanceclient/+bug/1309272 .. _1481729: https://bugs.launchpad.net/python-glanceclient/+bug/1481729 .. _1477910: https://bugs.launchpad.net/python-glanceclient/+bug/1477910 .. _1475769: https://bugs.launchpad.net/python-glanceclient/+bug/1475769 .. _1479020: https://bugs.launchpad.net/python-glanceclient/+bug/1479020 .. _1433637: https://bugs.launchpad.net/python-glanceclient/+bug/1433637 .. _metatags: https://review.opendev.org/#/c/179674/ .. _1472234: https://bugs.launchpad.net/python-glanceclient/+bug/1472234 .. _1473454: https://bugs.launchpad.net/python-cinderclient/+bug/1473454 .. _1468485: https://bugs.launchpad.net/python-glanceclient/+bug/1468485 .. _bp-oslo-ns: https://blueprints.launchpad.net/oslo-incubator/+spec/remove-namespace-packages .. _1465373: https://bugs.launchpad.net/python-glanceclient/+bug/1465373 .. _1461678: https://bugs.launchpad.net/nova/+bug/1461678 .. _bp-session: https://blueprints.launchpad.net/python-glanceclient/+spec/session-objects .. _1432701: https://bugs.launchpad.net/glance/+bug/1432701 0.19.0 ------ * 1381514_: Include ``owner`` in v2 image list * 1433884_: Fix ``md-object-update`` issue * 1446096_: Stop crashing if ``$HOME`` is not writable * 1402632_: Improve import related error handling .. _1381514: https://bugs.launchpad.net/python-glanceclient/+bug/1381514 .. _1433884: https://bugs.launchpad.net/python-glanceclient/+bug/1433884 .. _1455102: https://bugs.launchpad.net/python-glanceclient/+bug/1455102 .. _1446096: https://bugs.launchpad.net/python-glanceclient/+bug/1446096 .. _1402632: https://bugs.launchpad.net/python-glanceclient/+bug/1402632 0.18.0 ------ * 1442664_, 1442883_, 1357430_: Fix errors when SSL compression is disabled * 1399778_: Remove ``locations`` from image-create arguments * 1439513_: Fix error on python 3 when creating a task with and invalid property * Stop accepting ``*args`` in the main client interface * Expose ``is_base`` schema property attribute, allowing the client to differentiate between base and custom image properties * 1433962_: Validate whether a tag is valid when filtering for images. Invalid tags now raise an error rather than being ignored * 1434381_: Add ``--human-readable`` option to ``image-show`` .. _1442664: https://bugs.launchpad.net/python-glanceclient/+bug/1442664 .. _1442883: https://bugs.launchpad.net/python-glanceclient/+bug/1442883 .. _1357430: https://bugs.launchpad.net/python-glanceclient/+bug/1357430 .. _1399778: https://bugs.launchpad.net/python-glanceclient/+bug/1399778 .. _1439513: https://bugs.launchpad.net/python-glanceclient/+bug/1439513 .. _1433962: https://bugs.launchpad.net/python-glanceclient/+bug/1433962 .. _1434381: https://bugs.launchpad.net/python-glanceclient/+bug/1434381 0.17.0 ------ * 1420707_: Updated help for v2 member-update api * glance-sorting-enhancements_: Extend images CLI v2 with new sorting syntax * glance-sorting-enhancements_: Add the ability to specify the sort dir for each key * glance-sorting-enhancements_: Adds the ability to sort images with multiple keys * 1306774_: Apply expected patch format when updating tags in v2.images * 1429088_: v2: read limit for list from --limit in shell * 1428797_: Fix leaking sockets after v2 list operation * 1423939_: Fix leaking sockets after v1 list operation * 1408033_: v2: Allow upload from stdin on image-create .. _1420707: https://bugs.launchpad.net/python-glanceclient/+bug/1420707 .. _glance-sorting-enhancements: https://blueprints.launchpad.net/glance/+spec/glance-sorting-enhancements .. _1306774: https://bugs.launchpad.net/python-glanceclient/+bug/1306774 .. _1429088: https://bugs.launchpad.net/python-glanceclient/+bug/1429088 .. _1428797: https://bugs.launchpad.net/python-glanceclient/+bug/1428797 .. _1423939: https://bugs.launchpad.net/python-glanceclient/+bug/1423939 .. _1408033: https://bugs.launchpad.net/python-glanceclient/+bug/1408033 0.16.1 ------ * 1423165_: Fix sockets leaking for a subset of operations (show, delete and update) * 1395084_: Show error when trying to upload image data to non-queued image * 1398838_: Stop showing JSON and HTML in error messages returned from the glance service * 1396550_: More reliably register connection pools in cases where urllib3 is both vendored and installed system-wide .. _1423165: https://bugs.launchpad.net/python-glanceclient/+bug/1423165 .. _1395084: https://bugs.launchpad.net/python-glanceclient/+bug/1395084 .. _1398838: https://bugs.launchpad.net/python-glanceclient/+bug/1398838 .. _1396550: https://bugs.launchpad.net/python-glanceclient/+bug/1396550 0.16.0 ------ * Add --limit option to the v2 list operation. This allows a user to limit the number of images requested from the glance server. Previously the client would always go through the entire list of images * The CLI exit code on keyboard interrupt is now ``130``, changed from ``1``. * 1370283_: The set of supported SSL ciphers is now reduced to a smaller and more secure subset * 1384664_, 1402746_: Fix enabling the progress bar on download and upload when image data is piped into the client causing the client to crash * 1415935_: NoneType header values are now ignored when encoding headers * 1341777_: Requests which are streamed are now explicitly closed when the end of the stream has been reached * 1394236_: The CLI is now strict about what it counts as a boolean, and exits with an error if a non-boolean value is used as input to a boolean option * 1401197_: The CLI is now strict about valid inputs to ``--os-image-api-version`` * 1333119_: The CLI now raises a more useful error message when a user requests the deletion of an image which is already deleted * 1384759_: Fix client erroring if ``--os-tenant-id`` and ``--os-tenant-name`` are not defined * 1228744_: Add globoff option to debug curl statements. This allows it to work with IPv6 addresses .. _1370283: https://bugs.launchpad.net/python-glanceclient/+bug/1370283 .. _1384664: https://bugs.launchpad.net/python-glanceclient/+bug/1384664 .. _1402746: https://bugs.launchpad.net/python-glanceclient/+bug/1402746 .. _1415935: https://bugs.launchpad.net/python-glanceclient/+bug/1415935 .. _1394236: https://bugs.launchpad.net/python-glanceclient/+bug/1394236 .. _1401197: https://bugs.launchpad.net/python-glanceclient/+bug/1401197 .. _1384759: https://bugs.launchpad.net/python-glanceclient/+bug/1384759 .. _1228744: https://bugs.launchpad.net/python-glanceclient/+bug/1228744 .. _1333119: https://bugs.launchpad.net/python-glanceclient/+bug/1333119 0.15.0 ------ * Stop requiring a version to create a Client instance. The ``version`` argument is now a keyword. If no ``version`` is specified and a versioned endpoint is supplied, glanceclient will use the endpoint's version. If the endpoint is unversioned and a value for ``version`` is not supplied, glanceclient falls back to v1. This change is backwards-compatible. Examples:: >>> glanceclient.Client(version=1, endpoint='http://localhost:9292') # returns a v1 client >>> glanceclient.Client(endpoint='http://localhost:9292/v2') # returns a v2 client >>> glanceclient.Client(endpoint='http://localhost:9292') # returns a v1 client >>> glanceclient.Client(2, 'http://localhost:9292/v2') # old behavior is preserved * Add bash completion to glance client. The new bash completion files are stored in ``tools/glance.bash_completion`` * Add tty password entry. This prompts for a password if neither ``--os-password`` nor ``OS_PASSWORD`` have been set * Add the ``--property-filter`` option from the v1 client to v2 image-list. This allows you to do something similar to:: $ glance --os-image-api-version 2 image-list --property-filter os_distro=NixOS * 1324067_: Allow --file flag in v2 image-create. This selects a local disk image to upload during the creation of the image * 1395841_: Output a useful error on an invalid ``--os-image-api-version`` argument * 1394965_: Add ``identity_headers`` back into the request headers * 1350802_: Remove read only options from v2 shell commands. The options omitted are - ``created_at`` - ``updated_at`` - ``file`` - ``checksum`` - ``virtual_size`` - ``size`` - ``status`` - ``schema`` - ``direct_url`` * 1381295_: Stop setting X-Auth-Token key in http session header if there is no token provided * 1378844_: Fix ``--public`` being ignored on image-create * 1367782_: Fix to ensure ``endpoint_type`` is used by ``_get_endpoint()`` * 1381816_: Support Pagination for namespace list * 1401032_: Add support for enum types in the schema that accept ``None`` .. _1324067: https://bugs.launchpad.net/python-glanceclient/+bug/1324067 .. _1395841: https://bugs.launchpad.net/python-glanceclient/+bug/1395841 .. _1394965: https://bugs.launchpad.net/python-glanceclient/+bug/1394965 .. _1350802: https://bugs.launchpad.net/python-glanceclient/+bug/1350802 .. _1381295: https://bugs.launchpad.net/python-glanceclient/+bug/1381295 .. _1378844: https://bugs.launchpad.net/python-glanceclient/+bug/1378844 .. _1367782: https://bugs.launchpad.net/python-glanceclient/+bug/1367782 .. _1381816: https://bugs.launchpad.net/python-glanceclient/+bug/1381816 .. _1401032: https://bugs.launchpad.net/python-glanceclient/+bug/1401032 0.14.2 ------ * Add support for Glance Tasks calls (task create, list all and show) * 1362179_: Default to system CA bundle if no CA certificate is provided * 1350251_, 1347150_, 1362766_: Don't replace the https handler in the poolmanager * 1371559_: Skip non-base properties in patch method .. _1362179: https://bugs.launchpad.net/python-glanceclient/+bug/1362179 .. _1350251: https://bugs.launchpad.net/python-glanceclient/+bug/1350251 .. _1347150: https://bugs.launchpad.net/python-glanceclient/+bug/1347150 .. _1362766: https://bugs.launchpad.net/python-glanceclient/+bug/1362766 .. _1371559: https://bugs.launchpad.net/python-glanceclient/+bug/1371559 0.14.1 ------ * Print traceback to stderr if ``--debug`` is set * Downgrade log message for http request failures * Fix CLI image-update giving the wrong help on '--tags' parameter * 1367326_: Fix requests to non-bleeding edge servers using the v2 API * 1329301_: Update how tokens are redacted * 1369756_: Fix decoding errors when logging response headers .. _1367326: https://bugs.launchpad.net/python-glanceclient/+bug/1367326 .. _1329301: https://bugs.launchpad.net/python-glanceclient/+bug/1329301 .. _1369756: https://bugs.launchpad.net/python-glanceclient/+bug/1369756 0.14.0 ------ * Add support for metadata definitions catalog API * Enable osprofiler profiling support to glanceclient and its shell. This adds the ``--profile <HMAC_KEY>`` argument. * Add support for Keystone v3 * Replace old httpclient with requests * Fix performance issue for image listing of v2 API * 1364893_: Catch a new urllib3 exception: ProtocolError * 1359880_: Fix error when logging http response with python 3 * 1357430_: Ensure server's SSL cert is validated to help guard against man-in-the-middle attack * 1314218_: Remove deprecated commands from shell * 1348030_: Fix glance-client on IPv6 controllers * 1341777_: Don't stream non-binary requests .. _1364893: https://bugs.launchpad.net/python-glanceclient/+bug/1364893 .. _1359880: https://bugs.launchpad.net/python-glanceclient/+bug/1359880 .. _1357430: https://bugs.launchpad.net/python-glanceclient/+bug/1357430 .. _1314218: https://bugs.launchpad.net/python-glanceclient/+bug/1314218 .. _1348030: https://bugs.launchpad.net/python-glanceclient/+bug/1348030 .. _1341777: https://bugs.launchpad.net/python-glanceclient/+bug/1341777 0.13.0 ------ * Add command line support for image multi-locations * Py3K support completed * Fixed several issues related to UX * Progress bar support for V2 0.12.0 ------ * Add command line support for V2 image create, update, and upload * Enable querying for images by tag * 1230032_, 1231524_: Fix several issues with handling redirects * 1206095_: Use openstack-images-v2.1-json-patch for update method .. _1230032: http://bugs.launchpad.net/python-glanceclient/+bug/1230032 .. _1231524: http://bugs.launchpad.net/python-glanceclient/+bug/1231524 .. _1206095: http://bugs.launchpad.net/python-glanceclient/+bug/1206095 0.11.0 ------ * 1212463_: Allow single-wildcard SSL common name matching * 1208618_: Support absolute redirects for endpoint urls * 1190606_: Properly handle integer-like image ids * Support removing properties from images in the v2 library .. _1212463: http://bugs.launchpad.net/python-glanceclient/+bug/1212463 .. _1208618: http://bugs.launchpad.net/python-glanceclient/+bug/1208618 .. _1190606: http://bugs.launchpad.net/python-glanceclient/+bug/1190606 0.10.0 ------ * 1192229_: Security Update! Fix SSL certificate CNAME checking to handle ip addresses correctly * Add an optional progress bar for image downloads * Additional v2 api functionality, including image creation and uploads * Allow v1 admin clients to list all users' images, and to list the images of specific tenants. * Add a --checksum option to the v2 CLI for selecting images by checksum * Added support for image creation and uploads to the v2 library * Added support for updating and deleting v2 image tags to the v2 library and CLI * Added support for managing image memberships to the v2 library and CLI * Added a cli man page. * 1184566_: Fix support for unix pipes when uploading images in the v1 CLI * 1157864_: Fix an issue where glanceclient would fail with eventlet. .. _1192229: http://bugs.launchpad.net/python-glanceclient/+bug/1192229 .. _1184566: http://bugs.launchpad.net/python-glanceclient/+bug/1184566 .. _1157864: http://bugs.launchpad.net/python-glanceclient/+bug/1157864 0.9.0 ----- * Implement 'visibility', 'owner' and 'member_status' filters for v2 CLI and library * Relax prettytable dependency to v0.7.X * 1118799_: Implement filter on 'is_public' attribute in v1 API * 1157905_, 1130390_: Improve handling of SIGINT (CTRL-C) .. _1118799: http://bugs.launchpad.net/python-glanceclient/+bug/1118799 .. _1157905: http://bugs.launchpad.net/python-glanceclient/+bug/1157905 .. _1130390: http://bugs.launchpad.net/python-glanceclient/+bug/1130390 0.8.0 ----- * Implement image-delete for Image API v2 * Update warlock dependency to >= 0.7.0 and < 1 * 1061150_: Support non-ASCII characters * 1102944_: The port option is configurable when using HTTPS * 1093380_: Support image names in place of IDs for CLI commands * 1094917_: Better representation of errors through CLI .. _1061150: http://bugs.launchpad.net/python-glanceclient/+bug/1061150 .. _1102944: http://bugs.launchpad.net/python-glanceclient/+bug/1102944 .. _1093380: http://bugs.launchpad.net/python-glanceclient/+bug/1093380 .. _1094917: http://bugs.launchpad.net/python-glanceclient/+bug/1094917 0.7.0 ----- * Add ``--store`` option to ``image-create`` command * Deprecate ``--ca-file`` in favor of ``--os-cacert`` * 1082957_: Add ``--sort-key`` and ``--sort-dir`` CLI options to ``image-list`` command * 1081542_: Change default ``image-list`` CLI sort to order by image name ascending * 1079692_: Verify SSL certification hostnames when using HTTPS * 1080739_: Use ``--os-region-name`` in service catalog lookup .. _1082957: http://bugs.launchpad.net/python-glanceclient/+bug/1082957 .. _1081542: http://bugs.launchpad.net/python-glanceclient/+bug/1081542 .. _1079692: http://bugs.launchpad.net/python-glanceclient/+bug/1079692 .. _1080739: http://bugs.launchpad.net/python-glanceclient/+bug/1080739 0.6.0 ----- * Multiple image ID can be passed to ``glance image-delete`` * ``glance --version`` and glanceclient.__version__ expose the current library version * Use ``--human-readable`` with ``image-list`` and ``image-show`` to display image sizes in human-friendly formats * Use OpenSSL for HTTPS connections * 1056220_: Always use 'Transfer-Encoding: chunked' when transferring image data * 1052846_: Padded endpoints enabled (e.g. glance.example.com/padding/v1) * 1050345_: ``glance image-create`` and ``glance image-update`` now work on Windows .. _1056220: http://bugs.launchpad.net/python-glanceclient/+bug/1056220 .. _1052846: http://bugs.launchpad.net/python-glanceclient/+bug/1052846 .. _1050345: http://bugs.launchpad.net/python-glanceclient/+bug/1050345 0.5.1 ----- * 1045824_: Always send Content-Length when updating image with image data * 1046607_: Handle 300 Multiple Choices nicely in the CLI * 1035931_: Properly display URI in legacy 'show' command * 1048698_: Catch proper httplib InvalidURL exception .. _1045824: http://bugs.launchpad.net/python-glanceclient/+bug/1045824 .. _1046607: http://bugs.launchpad.net/python-glanceclient/+bug/1046607 .. _1035931: http://bugs.launchpad.net/python-glanceclient/+bug/1035931 .. _1048698: http://bugs.launchpad.net/python-glanceclient/+bug/1048698 0.5.0 ----- * Add 'image-download' command to CLI * Relax dependency on warlock to anything less than v2 0.4.2 ----- * 1037233_: Fix v1 image list where limit kwarg is less than page_size .. _1037233: https://bugs.launchpad.net/python-glanceclient/+bug/1037233 0.4.1 ----- * Default to system CA cert if one is not provided while using SSL * 1036315_: Allow 'deleted' to be provided in v1 API image update * 1036299_: Fix case where boolean values were treated as strings in v1 API * 1036297_: Fix case where int values were treated as strings in v1 API .. _1036315: https://bugs.launchpad.net/python-glanceclient/+bug/1036315 .. _1036299: https://bugs.launchpad.net/python-glanceclient/+bug/1036299 .. _1036297: https://bugs.launchpad.net/python-glanceclient/+bug/1036297 0.4.0 ----- * Send client SSL certificate to server for self-identification * Properly validate server SSL certificates * Images API v2 image data download �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/index.rst���������������������������������������������0000664�0001750�0001750�00000000425�00000000000�023344� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������========================== glanceclient Release Notes ========================== .. toctree:: :maxdepth: 1 unreleased 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky queens pike ocata newton mitaka earlier �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/mitaka.rst��������������������������������������������0000664�0001750�0001750�00000000232�00000000000�023477� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=================================== Mitaka Series Release Notes =================================== .. release-notes:: :branch: origin/stable/mitaka ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/newton.rst��������������������������������������������0000664�0001750�0001750�00000000232�00000000000�023543� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=================================== Newton Series Release Notes =================================== .. release-notes:: :branch: origin/stable/newton ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/ocata.rst���������������������������������������������0000664�0001750�0001750�00000000230�00000000000�023316� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/pike.rst����������������������������������������������0000664�0001750�0001750�00000000217�00000000000�023164� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/queens.rst��������������������������������������������0000664�0001750�0001750�00000000223�00000000000�023531� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/rocky.rst���������������������������������������������0000664�0001750�0001750�00000000221�00000000000�023356� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/stein.rst���������������������������������������������0000664�0001750�0001750�00000000221�00000000000�023351� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/train.rst���������������������������������������������0000664�0001750�0001750�00000000221�00000000000�023344� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=================================== Train Series Release Notes =================================== .. release-notes:: :branch: stable/train �������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/unreleased.rst����������������������������������������0000664�0001750�0001750�00000000153�00000000000�024362� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������============================ Current Series Release Notes ============================ .. release-notes:: ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/ussuri.rst��������������������������������������������0000664�0001750�0001750�00000000202�00000000000�023560� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/victoria.rst������������������������������������������0000664�0001750�0001750�00000000212�00000000000�024047� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: stable/victoria ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/wallaby.rst�������������������������������������������0000664�0001750�0001750�00000000206�00000000000�023665� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: stable/wallaby ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/xena.rst����������������������������������������������0000664�0001750�0001750�00000000172�00000000000�023167� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������========================= Xena Series Release Notes ========================= .. release-notes:: :branch: stable/xena ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/yoga.rst����������������������������������������������0000664�0001750�0001750�00000000172�00000000000�023173� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: stable/yoga ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/releasenotes/source/zed.rst�����������������������������������������������0000664�0001750�0001750�00000000166�00000000000�023021� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������======================== Zed Series Release Notes ======================== .. release-notes:: :branch: stable/zed ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/requirements.txt����������������������������������������������������������0000664�0001750�0001750�00000000416�00000000000�020776� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������pbr!=2.1.0,>=2.0.0 # Apache-2.0 PrettyTable>=0.7.1 # BSD keystoneauth1>=3.6.2 # Apache-2.0 requests>=2.14.2 # Apache-2.0 warlock>=1.2.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 wrapt>=1.7.0 # BSD License pyOpenSSL>=17.1.0 # Apache-2.0 ��������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/run_tests.sh��������������������������������������������������������������0000775�0001750�0001750�00000002114�00000000000�020074� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/bash function usage { echo "Usage: $0 [OPTION]..." echo "Run python-glanceclient's test suite(s)" echo "" echo " -p, --pep8 Just run flake8" echo " -h, --help Print this usage message" echo "" echo "This script is deprecated and currently retained for compatibility." echo 'You can run the full test suite for multiple environments by running "tox".' echo 'You can run tests for only python 2.7 by running "tox -e py27", or run only' echo 'the flake8 tests with "tox -e pep8".' exit } command -v tox > /dev/null 2>&1 if [ $? -ne 0 ]; then echo 'This script requires "tox" to run.' echo 'You can install it with "pip install tox".' exit 1; fi just_pep8=0 function process_option { case "$1" in -h|--help) usage;; -p|--pep8) let just_pep8=1;; esac } for arg in "$@"; do process_option $arg done if [ $just_pep8 -eq 1 ]; then tox -e pep8 exit fi tox -e py27 $toxargs 2>&1 | tee run_tests.err.log || exit if [ ${PIPESTATUS[0]} -ne 0 ]; then exit ${PIPESTATUS[0]} fi if [ -z "$toxargs" ]; then tox -e pep8 fi ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000033�00000000000�011451� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������27 mtime=1688740210.094164 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/setup.cfg�����������������������������������������������������������������0000664�0001750�0001750�00000001746�00000000000�017342� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[metadata] name = python-glanceclient summary = OpenStack Image API Client Library description_file = README.rst license = Apache License, Version 2.0 author = OpenStack author_email = openstack-discuss@lists.openstack.org home_page = https://docs.openstack.org/python-glanceclient/latest/ python_requires = >=3.8 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 :: Implementation :: CPython Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 [files] packages = glanceclient [entry_points] console_scripts = glance = glanceclient.shell:main [egg_info] tag_build = tag_date = 0 ��������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/setup.py������������������������������������������������������������������0000664�0001750�0001750�00000001271�00000000000�017224� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# 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) ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/test-requirements.txt�����������������������������������������������������0000664�0001750�0001750�00000000461�00000000000�021753� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������hacking>=3.0.1,<3.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 os-client-config>=1.28.0 # Apache-2.0 stestr>=2.0.0 # Apache-2.0 testtools>=2.2.0 # MIT testscenarios>=0.4 # Apache-2.0/BSD ddt>=1.2.1 # MIT fixtures>=3.0.0 # Apache-2.0/BSD requests-mock>=1.2.0 # Apache-2.0 tempest>=17.1.0 # Apache-2.0 ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000033�00000000000�011451� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������27 mtime=1688740210.094164 �����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/tools/��������������������������������������������������������������������0000775�0001750�0001750�00000000000�00000000000�016651� 5����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/tools/fix_ca_bundle.sh����������������������������������������������������0000775�0001750�0001750�00000002722�00000000000�021775� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������# When the functional tests are run in a devstack environment, we # need to make sure that the python-requests module installed by # tox in the test environment can find the distro-specific CA store # where the devstack certs have been installed. # # assumptions: # - devstack is running # - the devstack tls-proxy service is running # - the environment var OS_TESTENV_NAME is set in tox.ini (defaults # to 'functional' # # This code based on a function in devstack lib/tls function set_ca_bundle { local python_cmd=".tox/${OS_TESTENV_NAME:-functional}/bin/python" local capath=$($python_cmd -c $'try:\n from requests import certs\n print (certs.where())\nexcept ImportError: pass') # of course, each distro keeps the CA store in a different location local fedora_CA='/etc/pki/tls/certs/ca-bundle.crt' local ubuntu_CA='/etc/ssl/certs/ca-certificates.crt' local suse_CA='/etc/ssl/ca-bundle.pem' # the distro CA is rooted in /etc, so if ours isn't, we need to # change it if [[ ! $capath == "" && ! $capath =~ ^/etc/.* && ! -L $capath ]]; then if [[ -e $fedora_CA ]]; then rm -f $capath ln -s $fedora_CA $capath elif [[ -e $ubuntu_CA ]]; then rm -f $capath ln -s $ubuntu_CA $capath elif [[ -e $suse_CA ]]; then rm -f $capath ln -s $suse_CA $capath else echo "can't set CA bundle, expect tests to fail" fi fi } set_ca_bundle ����������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/tools/glance.bash_completion����������������������������������������������0000664�0001750�0001750�00000001562�00000000000�023176� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������_glance_opts="" # lazy init _glance_flags="" # lazy init _glance_opts_exp="" # lazy init _glance() { local cur prev nbc cflags COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" if [ "x$_glance_opts" == "x" ] ; then nbc="`glance bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`" _glance_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`" _glance_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`" _glance_opts_exp="`echo "$_glance_opts" | sed 's/^ *//' | tr ' ' '|'`" fi if [[ " ${COMP_WORDS[@]} " =~ " "($_glance_opts_exp)" " && "$prev" != "help" ]] ; then COMPREPLY=($(compgen -W "${_glance_flags}" -- ${cur})) else COMPREPLY=($(compgen -W "${_glance_opts}" -- ${cur})) fi return 0 } complete -F _glance glance����������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/tools/with_venv.sh��������������������������������������������������������0000775�0001750�0001750�00000000302�00000000000�021214� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������#!/bin/bash command -v tox > /dev/null 2>&1 if [ $? -ne 0 ]; then echo 'This script requires "tox" to run.' echo 'You can install it with "pip install tox".' exit 1; fi tox -evenv -- $@ ������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������././@PaxHeader��������������������������������������������������������������������������������������0000000�0000000�0000000�00000000026�00000000000�011453� x����������������������������������������������������������������������������������������������������ustar�00����������������������������������������������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������22 mtime=1688740181.0 ����������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������python-glanceclient-4.4.0/tox.ini�������������������������������������������������������������������0000664�0001750�0001750�00000004575�00000000000�017037� 0����������������������������������������������������������������������������������������������������ustar�00zuul����������������������������zuul����������������������������0000000�0000000������������������������������������������������������������������������������������������������������������������������������������������������������������������������[tox] envlist = py39,pep8 minversion = 3.18.0 [testenv] usedevelop = True setenv = OS_STDOUT_NOCAPTURE=False OS_STDERR_NOCAPTURE=False PYTHONDONTWRITEBYTECODE=1 # Nowadays, TOX_CONSTRAINTS_FILE should be used, but some older scripts might # still be using UPPER_CONSTRAINTS_FILE, so we check both variables and use the # first one that is defined. If none of them is defined, we fallback to the # default value. deps = -c{env:TOX_CONSTRAINTS_FILE:{env:UPPER_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master}} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = stestr run --slowest {posargs} [testenv:pep8] basepython = python3 commands = flake8 [testenv:venv] basepython = python3 commands = {posargs} [pbr] warnerror = True [testenv:functional] # See glanceclient/tests/functional/README.rst # for information on running the functional tests. setenv = OS_TEST_PATH = ./glanceclient/tests/functional/v2 OS_TESTENV_NAME = {envname} allowlist_externals = bash commands = bash tools/fix_ca_bundle.sh stestr run --slowest {posargs} [testenv:cover] basepython = python3 setenv = PYTHON=coverage run --source glanceclient --parallel-mode commands = stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml [testenv:docs] basepython = python3 deps = -r{toxinidir}/requirements.txt -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html sphinx-build -W -b man doc/source doc/build/man [testenv:releasenotes] basepython = python3 # Nowadays, TOX_CONSTRAINTS_FILE should be used, but some older scripts might # still be using UPPER_CONSTRAINTS_FILE, so we check both variables and use the # first one that is defined. If none of them is defined, we fallback to the # default value. deps = -c{env:TOX_CONSTRAINTS_FILE:{env:UPPER_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 [flake8] # E731 skipped as assign a lambda expression # W504 line break after binary operator ignore = E731,W504 show-source = True exclude = .venv*,.tox,dist,*egg,build,.git,doc,*lib/python*,.update-venv [hacking] import_exceptions = glanceclient._i18n ���������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������������