././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0175784 python_magnumclient-5.0.0/0000775000175000017500000000000015244246764014440 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.0604188 python_magnumclient-5.0.0/.coveragerc0000664000175000017500000000014515244246676016563 0ustar00zuulzuul[run] branch = True source = magnumclient omit = magnumclient/tests/* [report] ignore_errors = True ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.0604188 python_magnumclient-5.0.0/.mailmap0000664000175000017500000000013115244246676016056 0ustar00zuulzuul# Format is: # # ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.0604188 python_magnumclient-5.0.0/.stestr.conf0000664000175000017500000000006315244246676016712 0ustar00zuulzuul[DEFAULT] test_path=${OS_TEST_PATH:-./} top_dir=./ ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.0604188 python_magnumclient-5.0.0/.zuul.yaml0000664000175000017500000000043715244246676016407 0ustar00zuulzuul- project: queue: magnum templates: - check-requirements - openstack-cover-jobs - openstack-python3-jobs - publish-openstack-docs-pti - build-release-notes-jobs-python3 check: jobs: - openstack-tox-cover: voting: false ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9235773 python_magnumclient-5.0.0/AUTHORS0000664000175000017500000001454315244246764015517 0ustar00zuulzuulAaron-DH Abhishek Chanda Abhishek Chanda Adrian Otto Akhila Kishore Amey Bhide Andreas Jaeger Andreas Jaeger Andrew Melton Anh Tran Bharat Kunwar Bharat Kunwar Bharath Thiruveedula Boris Pilka Cedric Brandily Christoph Jansen Clenimar Filemon Corey Bryant Corey O'Brien Costin Gamenț Dai Dang Van Dale Smith Dane LeBlanc Daneyon Hansen Daniel Abad Davanum Srinivas Davanum Srinivas David Liu David Rabel Diogo Guerra Diogo Guerra Dmitriy Rabotyagov Dmitriy Rabotyagov Doug Hellmann Eli Qiao Eric Brown Erik Olof Gunnar Andersson EsmerlinJM Fang Fenghua <449171342@qq.com> Fang fenghua <449171342@qq.com> Feilong Wang Felipe Reyes Feng Shengqin Flavio Percoco Ghanshyam Mann Haiwei Xu Hangdong Zhang Hervé Beraud Hieu LE Hongbin Lu Hongbin Lu Hongbn Lu Hua Wang Ian Cordasco Ivan Anfimov Jake Yip Jake Yip James Page James Page Janek Lehr Janonymous Jason Dunsmore Jay Lau (Guangya Liu) Jay Lau Jennifer Carlucci Jeremy Stanley Johannes Grassler Kennan Kennan Kiran_totad Lan Qi song Lin Yang Lucky samadhiya M V P Nitesh Madhuri Madhuri Kumari Madhuri Kumari Madhuri Kumari Manjeet Singh Bhatia Mark Goddard Matthew Northcott Michael Lekkas Michal Arbet Michal Nasiadka Mike Fedosin Monty Taylor Motohiro OTSUKA Murali Allada Namrata Nguyen Hai Niall Bunting OTSUKA, Yuanying OTSUKA, Yuanying OpenStack Release Bot PanFengyun Pavlo Shchelokovskyy Pierre Riteau Rajiv Kumar Ricardo Rocha Sam Morrison Sean McGinnis Sergey Vilgelm Spyros Trigazis Spyros Trigazis Stephen Finucane Stephen Watson Steven Dake Steven Dake Surojit Pathak Swapnil Kulkarni (coolsvap) Takashi Kajinami Theodoros Tsioutsias Tobias Urdin Tom Cammann Tom Cammann Tovin Seven Travis Holton Vieri <15050873171@163.com> Vijendar Komalla Vikas Choudhary Vilobh Meshram Vipul Nayyar Vivek Jain Vu Cong Tuan Wanlong Gao Wenzhi Yu XiaojueGuan Yang Hongyang Yolanda Robla Zhenguo Niu chenaidong1 chenlx coldmoment coldmoment digambar digambar gecong1973 guo yunxian houming-wang jacky06 kangyufei kartik494 kavithahr maliki melissaml npraveen35 pawnesh.kumar phelanm qingszhao rajat29 ricolin ricolin ricolin shu-mutou space sunjia ting.wang venkatamahesh wangbo wanghui wangjiaqi07 wangqun wangzihao wu.chunyang wu.shiming xiexs yanghuichan yangyong yatin yatin yatin karel yatinkarel ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1787907518.061419 python_magnumclient-5.0.0/CONTRIBUTING.rst0000664000175000017500000000110415244246676017077 0ustar00zuulzuulIf you would like to contribute to the development of OpenStack, you must follow the steps in this page: https://docs.openstack.org/infra/manual/developers.html 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 Storyboard, not GitHub: https://storyboard.openstack.org/#!/project/openstack/python-magnumclient ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.8695765 python_magnumclient-5.0.0/ChangeLog0000664000175000017500000006330415244246764016220 0ustar00zuulzuulCHANGES ======= 5.0.0 ----- * Replace deprecated datetime.datetime.utcnow * Add node label and taint support to nodegroups * Support microversion 1.13: replace stack\_id with cluster\_id in clusters * CI: Bump hacking to 8.1.0 * Fix --os-container-infra-api-version with microversion strings * Migrate setup configuration to pyproject.toml * Rework coe cluster/nodegroup/template set * Move display attribute lists to v1 manager modules * Remove deprecated apiclient indirection layer * Remove legacy magnum CLI and shell infrastructure * Remove backward-compatibility shims from Client and legacy shell * Add test certs from unit testing to ignore * Drop support for Python 3.8, 3.9 and 3.10 * Add pyproject.toml to support pip 23.1 * Remove url tags from README * Remove legacy 'container' service type fallback * Remove deprecated docker storage driver warning in OSC cluster template * Remove deprecated CLI argument aliases from legacy shell commands * Add option to set driver in cluster\_template * CI: Use magnum Zuul queue 4.11.0 ------ * Add --hidden/--visible flags to cluster template update command * Fix tox releasenotes job * Add requests-mock to test-requirements.txt * Update master for stable/2026.1 4.10.0 ------ * reno: Update master for unmaintained/2024.1 * Update master for stable/2025.2 4.9.0 ----- * Support credential API * Replace os-client-config * Replace UPPER\_CONSTRAINTS\_FILE * Use releases.openstack.org instead of opendev.org * Update master for stable/2025.1 4.8.1 ----- * [FIX]: check\_hostname requires server\_hostname in Magnum client 4.8.0 ----- * reno: Update master for unmaintained/2023.1 * Bump hacking * Update master for stable/2024.2 4.7.0 ----- 4.6.0 ----- * Update VerifiedHTTPSConnection for Python >= 3.12 * Refactor TLS cert fetching for config * Remove extraneous quote in non-TLS kubeconfig 4.5.0 ----- * Display project\_id for cluster template * Replace deprecated inspector.getargspec * Update Python runtime for 2024.2 * Always use io module * Add insecure\_registry to osc part of the client * reno: Update master for unmaintained/zed * Update master for stable/2024.1 4.4.0 ----- * reno: Update master for unmaintained/yoga * Update python classifier in setup.cfg 4.3.0 ----- * Bugfix: master\_lb\_enabled flag now works as intended * setup.cfg: rename deprecated dash-separated options * Update master for stable/2023.2 * Use TOX\_CONSTRAINTS\_FILE 4.2.0 ----- * Allow directly output cluster config * Fix multi-line strings spaces * Add python 3.10 to setup.cfg metadata * Update master for stable/2023.1 * Clean up tox.ini 4.1.0 ----- * Update client.authentication.k8s.io api version for kubeconfig * Support tox4 * Remove translation sections from setup.cfg * Remove pypy and bump py37 to py38 * Switch to 2023.1 Python3 unit tests and generic template name * Update master for stable/zed 4.0.0 ----- * Drop bay and baymodel * Update python testing as per zed cycle testing runtime * remove unicode from code * Imported Translations from Zanata 3.7.0 ----- * Remove translation sections from setup.cfg * Drop lower-constraints.txt and its testing * Fix test\_help\_on\_subcommand on Python 3.10 * Update master for stable/yoga 3.6.0 ----- * Add Python3 wallaby unit tests * Update master for stable/victoria * Update master for stable/xena 3.5.0 ----- * Fix master\_lb\_enabled for cluster creation * Fix failing to parse json error msg * Update master for stable/wallaby 3.4.0 ----- * Uncap PrettyTable * Add CT tags argument and field to client * Nodegroup min\_node\_count should default to 0 * Drop lower constraints testing 3.3.0 ----- * Show docker\_volume\_size in \`cluster show\` 3.2.1 ----- * Migrate testing to Ubuntu Focal 3.2.0 ----- * Use overlay2 driver by default * Use unittest.mock instead of mock * Support master\_lb\_enabled when creating cluster * Show cluster fixed\_network, fixed\_subnet, floating\_ip\_enabled * Print response after issuing CA rotate request * Stop to use the \_\_future\_\_ module 3.1.0 ----- * Switch to newer openstackdocstheme and reno versions * Rename variables to address pep8 error * Labels override * Fix hacking min version to 3.0.1 * Remove six * Fix raw\_request of SessionClient * Add py38 package metadata * Support updating cluster health status * Use unittest.mock instead of third party mock * Add Python3 victoria unit tests * Update master for stable/ussuri * Check response type in \_extract\_error\_json * Cleanup py27 support 3.0.0 ----- * Update master for stable/train * Update to hacking 3.0 * Update hacking for Python3 * Drop py27 tests * Bugfix: Use fields option for cluster template list 2.17.0 ------ * Allow cluster config for any cluster state * Add nodegroup CRUD commands * replace launchpad urls with storyboard urls * Replace git.openstack.org URLs with opendev.org URLs 2.15.0 ------ * Support network, subnet and FIP when creating cluster 2.14.0 ------ * Add Python 3 Train unit tests * Conditional hidden arg for backward compatibility * Fix coverage test * Blacklist bandit 1.6.0 and cap Sphinx on Python2 * Display project\_id for cluster show * OpenDev Migration Patch * Dropping the py35 testing 2.13.0 ------ * Add nodegroup list/show commands * Support upgrade API * Support resize api * Update master for stable/stein * Support health\_status on client side * python3 fixes 2.12.0 ------ * Keystone auth support * add python 3.7 unit test job * Add hidden property to cluster template * Fix py37 compatibility * Use oslo\_serialization instead of the json module directly * Use template for lower-constraints * Change openstack-dev to openstack-discuss * Add Python 3.6 classifier to setup.cfg * add python 3.6 unit test job * Refactor the getid method base.py * Trivial: Update pypi url to new url 2.11.0 ------ * Fix crash on Service catalog empty 403 response * add python 3.6 unit test job * switch documentation job to new PTI * import zuul job settings from project-config * Add release notes for magnum client * [k8s] Add embed certs to config 2.10.0 ------ * Switch to stestr * osc: Don't pass parameters with null value * Update links in README * fix tox python3 overrides * Fix entrypoints for quotas * Follow the new PTI for document build * add lower-constraints job * OSC command for magnum quota's * Add deprecation warnings to magnum client commands * Replace six.iteritems() with dict.items() in python-magnumclient * OSC command for ca-show, ca-sign, ca-roatate and stats-list * Now cluster-template-update works for "labels" * Make cluster-config rbac compatible for kubebernetes 2.8.0 ----- * Add disable floating ip parameter * Updated from global requirements * Cleanup test-requirements * Updated from global requirements * Updated from global requirements * Set --labels default to None on cluster create * OSC: Add --flavor to coe cluster create * Add missing master\_flavor\_id in cluster attributes * Avoid tox\_install.sh for constraints support * Updated from global requirements * Replace six.iteritems() with .items() * OSC: Add --master-flavor to coe cluster create * Make cluster config --force a boolean * Updated from global requirements * Add --labels for cluster-create * OSC: Add --labels to coe cluster create * Add oslo.log as requirement * OSC: Add magnum service-list command * Now \`name\` is a positional argument in cluster creation * OSC: Remove unused files * inline comment typo fix 2.7.0 ----- * Remove log translations * Updated from global requirements * OSC: Add cluster config command * OSC 4/4 Add remaining cluster commands * OSC 3/4 Add remaining CT commands * OSC 2/4 Add Cluster Create and List * Updated from global requirements * OSC 1/4 Add CT create and UT framework * Remove docker-volume-size from cluster-list * Updated from global requirements * Make cluster name positional in ca-show * remove slash from kubernetes cluster context name * Update the documentation link for doc migration * Updated from global requirements * Updated from global requirements * switch to openstackdocstheme * Don't set a default for docker\_volume\_size * Updated from global requirements * Make --profile load from environment variables * Add --docker-volume-size for cluster-create * Updated from global requirements * Change assertTrue(isinstance()) by optimal assert 2.6.0 ----- * Add the support for 'detail' flag * Updated from global requirements * Replace assertRaisesRegexp with assertRaisesRegex * Updated from global requirements * Updated from global requirements * Magnum client suport insecure\_registry * Use assertIsNone(...) instead of assertEqual(None, ...) * Updated from global requirements * Updated from global requirements * Make --cluster option required for ca-rotate * Move cover.sh to the tools directory * Remove support for keyapir UUID * Fix wrong path reference * Update .gitignore to ignore .eggs * Increase Test coverage * The python 3.4 support is removed * Correct mistake from OSprofiler help * Fix UT for duplicate name args test case * Updated from global requirements * Add magnum client support for resource quotas * Update test requirement * Allow name as positional argument * Print exception details on update failure * Simplify magnumclient.shell.OpenStackMagnumShell.main() * Compare test coverage with the master branch * Updated from global requirements 2.5.0 ----- * Add usage docs for magnum client * Updated from global requirements * Increase UT Coverage * Add the OSC 'cluster template list' command * Add osc-lib to requirements.txt * Add ca-rotate command to magnumclient 2.4.0 ----- * Adding stats-list command to magnum client * Added link for modindex * Increase UT Coverage * Fix passing TLS\_VERIFIED in clusters/bays\_shell * Increase UT Coverage * Integrate OSprofiler in Magnum Client * Remove H903 error in sources * Updated from global requirements * Fix: swarm cluster-config, bay-config with tls\_disabled * Updated from global requirements * Pass 'api\_version' to create HTTPClient * Mark help messages for Translation * Allow cluster-config on cluster status 'ROLLBACK\_COMPLETE' * Add OSC command for cluster\_templates list * Rollback cluster/bay on update failure * Updated from global requirements * Magnum cluster-config/bay-config compatible with py3 * Updated from global requirements * Show team and repo badges on README * Remove Not used Classes/Methods from apiclient.base * Use assert\_called\_\*/assert\_not\_called to verify mock calls * Fix: some typos in unit test * Use assert\_called\_\*/assert\_not\_called to verify mock calls * Renames \*-id parameters * Implement Parameter Deprecation * Use assert\_called\_\*/assert\_not\_called to verify mock calls * Add a deprecation message to the bay\* commands' help text * Updated from global requirements * Increase UT coverage * Remove invalid check for 'manifest' path * Updated from global requirements * Add .venv directory to .gitignore * Enable DeprecationWarning in test environments * Updated from global requirements * Add \_\_ne\_\_ built-in function * Add missing options for HTTPClient if auth\_token given * Updated from global requirements * Remove white space between print and () * Add --keypair-id for cluster-create * Replace assertTrue(a in b) with assertIn(a, b) * Updated from global requirements * Fix the generated k8s config file * cluster-config return absolute path * delete python bytecode including pyo before every test run * Fix a keyward arguement error on bay-config * Add magnum.bash\_completion This can support the function of autocomplete for magnum's commands * Cluster creation command returns complete cluster uuid * Sync tools/tox\_install.sh 2.3.0 ----- * Updated from global requirements * Magnum client to support sync and async bay opts * Add microversioning support for httpclient * Add floating\_ip\_enabled attributes to baymodel * Cleanup coverage configuration * Fixes bay\_uuid parameter issue in functional tests * Fix to use HttpClient if token is given * Updates certificate CLI to use cluster * Updated from global requirements * Adds 'cluster' and 'cluster template' * Append value using comma if key exists in label * bay-config return absolute path * Updated from global requirements * Use testr coverage feature * Use upper constraints for all jobs in tox.ini * Updated from global requirements * Enabled magnum client to display detailed information * Updated from global requirements * Add Python 3.5 classifier and venv * Base OSC plugin support * Use os-client-config in shell * Remove discover from test-requirements * Increase unit test coverage for module baymodels * Update the magnum client to send the latest version * Updated from global requirements * Add shell command bay-config * Display baymodel info with bay-show command 2.2.0 ----- * Updated from global requirements * Bay\_create\_timeout should be set to 60 default * Prints '-' instead of 'None' when data is None * Updated from global requirements * Pass a flag to disable LB in baymodels * Log appropriate error while exception * Add fixed\_subnet attributes to baymodel * Updated from global requirements * Support OS\_PROJECT\_\* env variables * Completely remove openstack common modules * Updated from global requirements * Add docker-storage-driver argument to baymodel * Updated from global requirements * Updated from global requirements * Updated from global requirements * Tox test should respect upper-constraints * Switch to a new service type "container-infra" * Add mesos\_slave\_executor\_environment\_variables validate in CLI * Add some fields back to bay\_list * Updated from global requirements * Add '--fields' to show more columns for bay-list * Update the home-page with developer documentation * Correcting help messages of baymodel * Add python3 to python classifiers * Add '--fields' to show more columns for baymodel-list * Corrected spacing mistake in baymodels\_shell.py * Updated from global requirements * Fix the bug when some value in labels has comma, it will fail * Remove k8s pods, rcs, svc and container API calls 2.1.0 ----- * Updated from global requirements * Add endpoint\_override parameter to python-magnumclient * Updated from global requirements * Revert "Parameter format change for ca-show and ca-sign" * Updated from global requirements * Parameter format change for ca-show and ca-sign * Updated from global requirements * Fix the container-list with --limit 'a negative number' * Remove the update function of container * Add missing user message * Add marker/limit/sort-key/sort-dir features for bay-list * Add marker/limit/sort-key/sort-dir features for container-list * Allow semicolons in list of labels * Add unit tests for MServiceManager.list() method * Add unit tests for ContainerManager.list() method * Add unit tests for BayModelManager.list() method * Add unit tests for BayManager.list() method * Adapt http response error message parsing 2.0.0 ----- * Fix incorrect initialization of OrderedDict * Remove bandit.yaml in favor of defaults * Revert "Completely remove openstack common modules" * Use six.u instead of u'' * Completely remove openstack common modules * Add param for magnum baymodel-list * Move bandit into pep8 * Client : Create BayModel with server type(VM/BM) * Updated from global requirements * Sync with oslo-incubator * Updated from global requirements * Remove node object from magnumclient * Exception not catched when bay create failed * Fix a spell typo * The rc-list should contain bay\_uuid * Remove unused attribute "ssh\_authorized\_key" * Update translation setup * Fix incorrect help message * Add Keystone v3 compatibility * Updated from global requirements * Adds Container Volume Model volume\_driver support * Python 3.4 compatibility * Add --insecure option * Fix endpoint\_types * Updated from global requirements * Replace six.iteritems(dict) with dict.items() * Change the endpoint env varible * Prevent list rcs when bay is not ready * raise exception when create rc with invalid bay status * raise exception when create container with invalid bay status * raise exception when create pod with invalid bay status * Revert "Pass environment variables of proxy to tox" * Add debug testenv in tox * Remove check for bay state on ca-sign and ca-show * Fix bypass\_url and errors with no headers * Remove unnecessary check when create bay * Fix test\_keys\_and\_vals\_to\_strs dict assert * Add unit test for pod-create with invalid bay status * Prevent list pods while bay is not ready * Updated from global requirements * Python 3 deprecated the logger.warn method in favor of warning * Remove references to \_i18n and apiclient.exceptions * Handle list object when print a dict * Put py34 first in the env order of tox * Updated from global requirements * Packages missing from requirements.txt * Add type validation and default for some parameter * Add py3 compatibility for unicode builtin * Add optional parameter --bay when doing container list * Updated from global requirements * Drop py33 support * use wild card for passing env variables * Add registry\_enabled option to baymodel-create * Improve client master\_count validation * Improve client node\_count validation * Fix default bay create timeout * Remove py26 support * Fix POD CLI to work with Object from Bay * Fix RC CLI to work with Object from Bay * Add .idea directory to .gitignore * Fix Service CLI to work with Object from Bay * Handle faultstring when using SessionClient * fix wrong function description * fix exceptions.from\_response() parameter * Fix the ouput of 'container-create' when it fails * Make bandit included in test-requirements.txt * Correct help message of '--name' for baymodel-create * Remove test\_shell\_args.py 1.1.0 ----- * Revise help message of '--docker-volume-size' for baymodel-create * Updated from global requirements * Improve readme contents * Split v1 shell sub-command into specific files * Updated from global requirements * Fix RC CLI to work with Object from Bay changes * Update baymodel-update help doc * Delete python bytecode before every test run * Add .DS\_Store to .gitignore * Updated from global requirements * Use keystoneauth to create a Session * Make image as required for do\_container\_create * Add test for container\_create * Add unversioned client constructor * Fix rc cli to work with bay identifier * Updated from global requirements * Updated from global requirements * Add the introduce of the command "magnum help ca-show" * Add the introduce of the command "magnum help ca-sign" 1.0.0.0b1 --------- * Updated from global requirements * Pass bay\_ident to k8s objects methods * Add an option to specify container memory size * Add support for 'baymodel-update' in python-client * Support for public baymodels * Rename 'insecure' baymodel attribute to 'tls\_disabled' * Client: Pass bay\_uuid to Service Read/Write API * Client: Pass bay\_uuid to Pod Read/Write API * Client: Pass bay\_uuid to RC Read/Write API * Add insecure flag to baymodel * Magnum show cmds display dictionaries with unicode u chars * Adding 'magnum service-list' * Add certificates operations * Change ignore-errors to ignore\_errors * Add support for python >= 3.4 * Updated from global requirements * Adds Labels Support * Adds Container Network Model network\_driver support * Update help message for coe service related command * Add missed space between two words * Updated from global requirements * Rename existing service-\* to coe-service-\* * Updated from global requirements * Remove name from test token * This adds proxy feature in magnum client * Updated from global requirements * Updated from global requirements * Updated from global requirements * Add support for multiple master nodes * Updated from global requirements * Remove uuidutils from openstack.common * Updated from global requirements * Updated from global requirements * Remove H803 rule * Updated from global requirements * Updated from global requirements * Updated from global requirements * Rename image\_id to image when create a container * Updated from global requirements * Add missing dependency oslo.serialization * Updated from global requirements * Add additional arguments to CLI for container-create * Pass environment variables of proxy to tox * Change container-execute to container-exec * Updated from global requirements * Sync from latest oslo-incubator * Updated from global requirements * Fix translation setup 0.2.1 ----- * Make metavar's consistent * Setup for translation * Updated from global requirements * Bump up to newer hacking * Add support of container resource management with "name" * Add support for container status * Drop use of 'oslo' namespace package 0.2.0 ----- * Remove links attribute from pod show * Log the correct url in debug mode * Add heat timeout to bay-create for magnum client * Add coe attribute to BayModel creation * Add bay\_uuid to container create * Update README to work with release tools * Add support for pod name in pod-update command 0.1.0 ----- * Add bay status check when rc create * Add bay status check when service create * fix a typo in log * Add bay status check when pod create * Rename swarm-token to discovery-url * Support pass command field when create a container * Delegate magnum url search to url\_for method * Remove side effect in get\_keystoneclient * Add unittests for magnumclient.v1.client module * Correct mock use in TestCommandLineArgument.setUp * Uncap library requirements for liberty * Support update a replication controller * Support update a service * Add swarm\_token to bay-create call * Support update a pod * Authenticate once * Add error info detail for magnum cli * Update bandit for latest usage * Add tox bandit support for python-magnumclient * Remove unused get\_projects\_list method * Add support for bay name in bay-update command * Allow bay name when replication controller is created * Update .gitreview for project rename * Allow baymodel name when bay is created * Allow bay name when pod is created * Allow bay name when service is created * Support keystone region * Rename k8s specific bay attributes * Add support of baymodel resource management with "name" * Add support of rc resource management with "name" * Add support of service resource management with "name" * Allow specification of ssh\_authorized\_key 0.0.1 ----- * Add support of pod resource management with "name" * Remove some default values for baymodel create * Add support of bay resource management with "name" * Fix container exec output * Fix the container logs output * Make baymodel-create fail when no argument passed * Display bay status column * tidy up language on one shell help command * Allow adding master flavor id to baymodel * Allow specification of fixed\_network * Allow specification of docker\_volume\_size * Support multiple args in some magnum commands * Remove '--id' option from magnum commands * Make bay-create fail when no argument passed * Adding test for magnumclient argument parsing * Add support for updating a bay * Adding tests for v1/shell.py * Adding test for shell.py * Container logs should use HTTP GET * Add help info for container operations * Update help message for baymodel operations * Add help for magnum object show * Remove 'desc' from container output * Rename \`resource\`\_data/url attributes to manifest/manifest\_url * Default version should be v1 in magnum * Fix an error on not finding log handler * Add id as required for bay-show/delete * Revert "Fix an error on not finding log handler" * Change rc\_data to replicationcontroller\_data * Fix an error on not finding log handler * Make replication controller client works * Remove 'desc' from docker creation attribute * Update test\_update to test\_pod\_update for pod client test * Add client test for k8s replication controller * Add client test for k8s service * Add client test for magnum containers * Add client test for magnum baymodel * Add client test for magnum bay * Add client test for magnum node * Support file path as a service manifest data * Adjusted CLI argument names to use dashes rather than underscores * Add client test for test\_pods * Support file path as a pod data * Add unit test for magnumclient/common/httpclient.py * Add unit test for magnumclient/common/utils.py * Update K8S Pod and Service input parameter * Make id as required when delete or show object info * Construct URI properly for container-execute * Add k8s replication support for magnum client * Ensure a id string is passed when --id is used * Change folder of api to v1 for magumclient * Get container-list CLI working * Do not advertise py33 compatibility * Set HTTP PUT Content-Length for container actions * Implement container actions * Add image\_id attribute for container * Service create only need service filename * pod create only need pod filename * Implement client for service operations * Update parmater name for \_show\_pod * Add baymodel\_id to bay\_create * Add flavor\_id property to baymodel object * Implement pod client CLI * Make bay-create operate again as a result of baymodel * Make bay create work with new bay model structure * Type and image\_id are removed from server * Don't show links when showing bay * Make baymodel-list only show name and UUID * Make baymodel not show links * Implement baymodel in client * Add node object to the python client * Add image\_id and node\_count to bay-create * Workflow documentation is now in infra-manual * Make separate Bay objects for each bay object * Complete implementation of bay operations * Docstring cleanups * Delete bay rather then container when requested * Misc cleanups * Make container in sync with bays and pods * Make pod objects work in ReST Client API * Make bay objects work in ReST Client API * Modify container list attributes * Implements basic container operations * Add client resources and managers * Update apiclient from oslo-incubator * Add apiclient library from oslo-incubator * Added default service type * Skeleton for the cli client * Include the auth module from oslo * Sync common code from oslo-incubator * Added project required files * Add service API methods to what should be implemented * Boilerplate client for communicating with ReST API * Initial commit ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1787907518.061419 python_magnumclient-5.0.0/LICENSE0000664000175000017500000002607515244246676015461 0ustar00zuulzuulApache 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. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0175784 python_magnumclient-5.0.0/PKG-INFO0000644000175000017500000000522415244246764015536 0ustar00zuulzuulMetadata-Version: 2.4 Name: python-magnumclient Version: 5.0.0 Summary: Client library for Magnum API Author-email: OpenStack License-Expression: Apache-2.0 Project-URL: Homepage, https://docs.openstack.org/python-magnumclient/latest/ Project-URL: Repository, https://opendev.org/openstack/python-magnumclient Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.11 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: pbr>=6.1.1 Requires-Dist: keystoneauth1>=3.4.0 Requires-Dist: stevedore>=1.20.0 Requires-Dist: requests>=2.14.2 Requires-Dist: oslo.i18n>=3.15.3 Requires-Dist: oslo.log>=3.36.0 Requires-Dist: oslo.serialization!=2.19.1,>=2.18.0 Requires-Dist: oslo.utils>=3.33.0 Requires-Dist: openstacksdk>=0.10.0 Requires-Dist: osc-lib>=1.8.0 Requires-Dist: PrettyTable>=0.7.2 Requires-Dist: cryptography>=3.0 Requires-Dist: decorator>=3.4.0 Dynamic: license-file Dynamic: requires-dist ================================= Python bindings to the Magnum API ================================= .. image:: https://governance.openstack.org/tc/badges/python-magnumclient.svg .. image:: https://img.shields.io/pypi/v/python-magnumclient.svg :target: https://pypi.org/project/python-magnumclient/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/python-magnumclient.svg :target: https://pypi.org/project/python-magnumclient/ :alt: Downloads This is a client library for Magnum built on the Magnum API. It provides a Python API (the ``magnumclient`` module) and a command-line tool (``magnum``). Development takes place via the usual OpenStack processes as outlined in the `developer guide `_. * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Bugs`_ - issue tracking * `Source`_ .. _PyPi: https://pypi.org/project/python-magnumclient .. _Online Documentation: https://docs.openstack.org/python-magnumclient/latest/ .. _Bugs: https://bugs.launchpad.net/magnum .. _Source: https://opendev.org/openstack/python-magnumclient ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1787907518.061419 python_magnumclient-5.0.0/README.rst0000664000175000017500000000222115244246676016126 0ustar00zuulzuul================================= Python bindings to the Magnum API ================================= .. image:: https://governance.openstack.org/tc/badges/python-magnumclient.svg .. image:: https://img.shields.io/pypi/v/python-magnumclient.svg :target: https://pypi.org/project/python-magnumclient/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/python-magnumclient.svg :target: https://pypi.org/project/python-magnumclient/ :alt: Downloads This is a client library for Magnum built on the Magnum API. It provides a Python API (the ``magnumclient`` module) and a command-line tool (``magnum``). Development takes place via the usual OpenStack processes as outlined in the `developer guide `_. * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Bugs`_ - issue tracking * `Source`_ .. _PyPi: https://pypi.org/project/python-magnumclient .. _Online Documentation: https://docs.openstack.org/python-magnumclient/latest/ .. _Bugs: https://bugs.launchpad.net/magnum .. _Source: https://opendev.org/openstack/python-magnumclient ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9905782 python_magnumclient-5.0.0/doc/0000775000175000017500000000000015244246764015205 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1787907518.061419 python_magnumclient-5.0.0/doc/requirements.txt0000664000175000017500000000013415244246676020471 0ustar00zuulzuulsphinx>=2.0.0,!=2.1.0 # BSD openstackdocstheme>=2.2.0 # Apache-2.0 reno>=3.1.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1787907571.991578 python_magnumclient-5.0.0/doc/source/0000775000175000017500000000000015244246764016505 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1787907518.061419 python_magnumclient-5.0.0/doc/source/conf.py0000664000175000017500000000526515244246676020016 0ustar00zuulzuul# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import os import sys sys.path.insert(0, os.path.abspath('../..')) # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinx.ext.autodoc', #'sphinx.ext.intersphinx', 'openstackdocstheme' ] # 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 # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'python-magnumclient' copyright = '2013, 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 = 'sphinx' # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['magnumclient.'] # -- 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_path = ["."] html_theme = 'openstackdocs' # html_static_path = ['static'] # openstackdocstheme options openstackdocs_repo_name = 'openstack/python-magnumclient' openstackdocs_bug_project = 'python-magnumclient' openstackdocs_bug_tag = '' # Output file base name for HTML help builder. htmlhelp_basename = '%sdoc' % project # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ('index', '%s.tex' % project, '%s Documentation' % project, 'OpenStack Foundation', 'manual'), ] # Example configuration for intersphinx: refer to the Python standard library. #intersphinx_mapping = {'http://docs.python.org/': None} ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1787907518.061419 python_magnumclient-5.0.0/doc/source/contributing.rst0000664000175000017500000000011415244246676021744 0ustar00zuulzuul============ Contributing ============ .. include:: ../../CONTRIBUTING.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1004193 python_magnumclient-5.0.0/doc/source/index.rst0000664000175000017500000000047715244246676020360 0ustar00zuulzuul=============================================== Welcome to python-magnumclient's documentation! =============================================== Contents -------- .. toctree:: :maxdepth: 2 readme installation usage contributing Indices and tables ------------------ * :ref:`genindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1014192 python_magnumclient-5.0.0/doc/source/installation.rst0000664000175000017500000000034115244246676021740 0ustar00zuulzuul============ Installation ============ At the command line:: $ pip install python-magnumclient Or, if you have virtualenvwrapper installed:: $ mkvirtualenv python-magnumclient $ pip install python-magnumclient././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1014192 python_magnumclient-5.0.0/doc/source/readme.rst0000664000175000017500000000010615244246676020473 0ustar00zuulzuul============ Introduction ============ .. include:: ../../README.rst ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1014192 python_magnumclient-5.0.0/doc/source/usage.rst0000664000175000017500000000406515244246676020352 0ustar00zuulzuul=========================================== Python bindings to the OpenStack Magnum API =========================================== This is a client for the OpenStack Magnum API. It includes a Python API (the :mod:`magnumclient` module) and a command-line script (installed as :program:`magnum`). Python API ========== To use python-magnumclient in a project, create a client instance using the keystoneauth session API:: from keystoneauth1.identity import v3 from keystoneauth1 import session from keystoneclient.v3 import client from magnumclient.client import Client magnum_endpoint = "http://magnum.example.com:9511/v1" auth = v3.Password(auth_url='http://my.keystone.com:5000/v3', username='myuser', password='mypassword', project_name='myproject', user_domain_id='default', project_domain_id='default') sess = session.Session(auth=auth) magnum = Client('1', endpoint_override=magnum_endpoint, session=sess) magnum.clusters.list() For more information on keystoneauth API, see `Using Sessions`_. .. _Using Sessions: https://docs.openstack.org/keystoneauth/latest/using-sessions.html Command-line tool ================= In order to use the CLI, you must provide your OpenStack username, password, project name, user domain ID, project domain ID, and auth endpoint. Use the corresponding configuration options (--os-username, --os-password, --os-project-name, --os-project-domain-id, --os-user-domain-id, and --os-auth-url) or set them in environment variables:: export OS_USERNAME=myuser export OS_PASSWORD=mypassword export OS_PROJECT_NAME=myproject export OS_USER_DOMAIN_ID=default export OS_PROJECT_DOMAIN_ID=default export OS_AUTH_URL=http://my.keystone.com:5000/v3 From there, all shell commands take the form:: magnum [arguments...] Run :program:`magnum help` to see a complete listing of available commands. Run :program:`magnum help ` to get detailed help for that command. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9935782 python_magnumclient-5.0.0/magnumclient/0000775000175000017500000000000015244246764017123 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1014192 python_magnumclient-5.0.0/magnumclient/__init__.py0000664000175000017500000000124315244246676021236 0ustar00zuulzuul# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pbr.version __version__ = pbr.version.VersionInfo( 'python-magnumclient').version_string() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1014192 python_magnumclient-5.0.0/magnumclient/client.py0000664000175000017500000000160615244246676020760 0ustar00zuulzuul# Copyright (c) 2015 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from magnumclient.v1 import client def Client(version='1', **kwargs): """Factory function to create a new container service client.""" if version != '1': raise ValueError( "magnum only has one API version. Valid values for 'version'" " are '1'") return client.Client(**kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9945781 python_magnumclient-5.0.0/magnumclient/common/0000775000175000017500000000000015244246764020413 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1014192 python_magnumclient-5.0.0/magnumclient/common/__init__.py0000664000175000017500000000000015244246676022514 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1014192 python_magnumclient-5.0.0/magnumclient/common/base.py0000664000175000017500000001442215244246676021704 0ustar00zuulzuul# -*- coding: utf-8 -*- # # Copyright 2012 OpenStack LLC. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Base utilities to build API operation managers and objects on top of. """ import copy from urllib import parse as urlparse def getid(obj): """Wrapper to get object's ID. Abstracts the common pattern of allowing both an object or an object's ID (UUID) as a parameter when dealing with relationships. """ return getattr(obj, 'id', obj) class Manager(object): """Provides CRUD operations with a particular API.""" resource_class = None def __init__(self, api): self.api = api def _create(self, url, body): resp, body = self.api.json_request('POST', url, body=body) if body: return self.resource_class(self, body) def _format_body_data(self, body, response_key): if response_key: try: data = body[response_key] except KeyError: return [] else: data = body if not isinstance(data, list): data = [data] return data def _list_pagination(self, url, response_key=None, obj_class=None, limit=None): """Retrieve a list of items. The Magnum API is configured to return a maximum number of items per request, (FIXME: see Magnum's api.max_limit option). This iterates over the 'next' link (pagination) in the responses, to get the number of items specified by 'limit'. If 'limit' is None this function will continue pagination until there are no more values to be returned. :param url: a partial URL, e.g. '/nodes' :param response_key: the key to be looked up in response dictionary, e.g. 'nodes' :param obj_class: class for constructing the returned objects. :param limit: maximum number of items to return. If None returns everything. """ if obj_class is None: obj_class = self.resource_class if limit is not None: limit = int(limit) object_list = [] object_count = 0 limit_reached = False while url: resp, body = self.api.json_request('GET', url) data = self._format_body_data(body, response_key) for obj in data: object_list.append(obj_class(self, obj, loaded=True)) object_count += 1 if limit and object_count >= limit: # break the for loop limit_reached = True break # break the while loop and return if limit_reached: break url = body.get('next') if url: # NOTE(lucasagomes): We need to edit the URL to remove # the scheme and netloc url_parts = list(urlparse.urlparse(url)) url_parts[0] = url_parts[1] = '' url = urlparse.urlunparse(url_parts) return object_list def _list(self, url, response_key=None, obj_class=None, body=None): resp, body = self.api.json_request('GET', url) if obj_class is None: obj_class = self.resource_class data = self._format_body_data(body, response_key) return [obj_class(self, res, loaded=True) for res in data if res] def _update(self, url, body=None, method='PATCH', response_key=None): if body: resp, resp_body = self.api.json_request(method, url, body=body) else: resp, resp_body = self.api.raw_request(method, url) # PATCH/PUT requests may not return a body if resp_body: return self.resource_class(self, resp_body) def _delete(self, url): self.api.raw_request('DELETE', url) class Resource(object): """Represents a particular instance of an object (tenant, user, etc). This is pretty much just a bag for attributes. """ def __init__(self, manager, info, loaded=False): 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) def _add_details(self, info): for (k, v) in info.items(): try: setattr(self, k, v) self._info[k] = v except AttributeError: pass def __getattr__(self, k): if k not in self.__dict__: if not self.is_loaded(): self.get() return self.__getattr__(k) raise AttributeError(k) else: return self.__dict__[k] def get(self): 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 if not isinstance(other, self.__class__): return False if hasattr(self, 'id') and hasattr(other, 'id'): return self.id == other.id return self._info == other._info def __ne__(self, other): return not self.__eq__(other) def is_loaded(self): return self._loaded def set_loaded(self, val): self._loaded = val def to_dict(self): return copy.deepcopy(self._info) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1014192 python_magnumclient-5.0.0/magnumclient/common/cliutils.py0000664000175000017500000003022515244246676022621 0ustar00zuulzuul# Copyright 2012 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. # W0603: Using the global statement # W0621: Redefining name %s from outer scope # pylint: disable=W0603,W0621 import getpass import inspect import os import sys import textwrap import decorator from magnumclient import exceptions from oslo_utils import encodeutils from oslo_utils import strutils import prettytable from magnumclient.i18n import _ class MissingArgs(Exception): """Supplied arguments are not sufficient for calling a function.""" def __init__(self, missing): self.missing = missing msg = _("Missing arguments: %s") % ", ".join(missing) super(MissingArgs, self).__init__(msg) class DuplicateArgs(Exception): """More than one of the same argument type was passed.""" def __init__(self, param, dupes): msg = _('Duplicate "%(param)s" arguments: %(dupes)s') % { 'param': param, 'dupes': ", ".join(dupes)} super(DuplicateArgs, self).__init__(msg) def validate_args(fn, *args, **kwargs): """Check that the supplied args are sufficient for calling a function. >>> validate_args(lambda a: None) Traceback (most recent call last): ... MissingArgs: Missing argument(s): a >>> validate_args(lambda a, b, c, d: None, 0, c=1) Traceback (most recent call last): ... MissingArgs: Missing argument(s): b, d :param fn: the function to check :param arg: the positional arguments supplied :param kwargs: the keyword arguments supplied """ argspec = inspect.getfullargspec(fn) num_defaults = len(argspec.defaults or []) required_args = argspec.args[:len(argspec.args) - num_defaults] def isbound(method): return getattr(method, '__self__', None) is not None if isbound(fn): required_args.pop(0) missing = [arg for arg in required_args if arg not in kwargs] missing = missing[len(args):] if missing: raise MissingArgs(missing) def env(*args, **kwargs): """Returns the first environment variable set. If all are empty, defaults to '' or keyword arg `default`. """ for arg in args: value = os.environ.get(arg) if value: return value return kwargs.get('default', '') def deprecation_message(preamble, new_name): msg = ('%s This parameter is deprecated and will be removed in a future ' 'release. Use --%s instead.' % (preamble, new_name)) return msg def deprecated(message): """Decorator for marking a CLI command as deprecated. Prints the given message before executing the decorated function. """ @decorator.decorator def wrapper(func, *args, **kwargs): print(message) return func(*args, **kwargs) return wrapper def deprecation_map(dep_map): """Decorator for applying a map of deprecating argument aliases. The map connects old argument names to their replacements. The shell uses this map to create mutually exclusive argument groups in argparse and prints a deprecation warning when the old form is used. NOTE: This decorator MUST be the outermost in the chain of argument decorators to work correctly. """ def _decorator(func): if not hasattr(func, 'arguments'): return func func.deprecated_groups = [] for old_param, new_param in dep_map.items(): old_info, new_info = None, None required = False for (args, kwargs) in func.arguments: if old_param in args: old_info = (args, kwargs) if 'required' in kwargs: required = kwargs['required'] kwargs['required'] = False elif new_param in args: new_info = (args, kwargs) kwargs['required'] = False if old_info and new_info: break func.deprecated_groups.append((old_info, new_info, required)) func.arguments.remove(old_info) func.arguments.remove(new_info) return func return _decorator def arg(*args, **kwargs): """Decorator for CLI args.""" def _decorator(func): add_arg(func, *args, **kwargs) return func return _decorator def add_arg(func, *args, **kwargs): """Bind CLI arguments to a shell.py `do_foo` function.""" if not hasattr(func, 'arguments'): func.arguments = [] if (args, kwargs) not in func.arguments: func.arguments.insert(0, (args, kwargs)) def print_list(objs, fields, formatters=None, sortby_index=0, mixed_case_fields=None, field_labels=None): """Print a list or objects as a table, one row per object. :param objs: iterable of :class:`Resource` :param fields: attributes that correspond to columns, in order :param formatters: `dict` of callables for field formatting :param sortby_index: index of the field for sorting table rows :param mixed_case_fields: fields corresponding to object attributes that have mixed case names (e.g., 'serverId') :param field_labels: Labels to use in the heading of the table, default to fields. """ formatters = formatters or {} mixed_case_fields = mixed_case_fields or [] field_labels = field_labels or fields if len(field_labels) != len(fields): raise ValueError(_("Field labels list %(labels)s has different number " "of elements than fields list %(fields)s"), {'labels': field_labels, 'fields': fields}) if sortby_index is None: kwargs = {} else: kwargs = {'sortby': field_labels[sortby_index]} pt = prettytable.PrettyTable(field_labels) pt.align = 'l' for o in objs: row = [] for field in fields: data = '-' if field in formatters: data = formatters[field](o) else: if field in mixed_case_fields: field_name = field.replace(' ', '_') else: field_name = field.lower().replace(' ', '_') data = getattr(o, field_name, '') if data is None: data = '-' row.append(data) pt.add_row(row) print(encodeutils.safe_encode(pt.get_string(**kwargs)).decode()) def keys_and_vals_to_strs(dictionary): """Recursively convert a dictionary's keys and values to strings. :param dictionary: dictionary whose keys/vals are to be converted to strs """ def to_str(k_or_v): if isinstance(k_or_v, dict): return keys_and_vals_to_strs(k_or_v) elif isinstance(k_or_v, str): return str(k_or_v) else: return k_or_v return dict((to_str(k), to_str(v)) for k, v in dictionary.items()) def print_dict(dct, dict_property="Property", wrap=0): """Print a `dict` as a table of two columns. :param dct: `dict` to print :param dict_property: name of the first column :param wrap: wrapping for the second column """ pt = prettytable.PrettyTable([dict_property, 'Value']) pt.align = 'l' for k, v in dct.items(): # convert dict to str to check length if isinstance(v, dict): v = str(keys_and_vals_to_strs(v)) if wrap > 0: v = textwrap.fill(str(v), wrap) # if value has a newline, add in multiple rows # e.g. fault with stacktrace if v and isinstance(v, str) and r'\n' in v: lines = v.strip().split(r'\n') col1 = k for line in lines: pt.add_row([col1, line]) col1 = '' elif isinstance(v, list): val = str([str(i) for i in v]) if val is None: val = '-' pt.add_row([k, val]) else: if v is None: v = '-' pt.add_row([k, v]) print(encodeutils.safe_encode(pt.get_string()).decode()) def get_password(max_password_prompts=3): """Read password from TTY.""" verify = strutils.bool_from_string(env("OS_VERIFY_PASSWORD")) pw = None if hasattr(sys.stdin, "isatty") and sys.stdin.isatty(): # Check for Ctrl-D try: for __ in range(max_password_prompts): pw1 = getpass.getpass("OS Password: ") if verify: pw2 = getpass.getpass("Please verify: ") else: pw2 = pw1 if pw1 == pw2 and pw1: pw = pw1 break except EOFError: pass return pw def _format_field_name(attr): """Format an object attribute in a human-friendly way.""" # Split at ':' and leave the extension name as-is. parts = attr.rsplit(':', 1) name = parts[-1].replace('_', ' ') # Don't title() on mixed case if name.isupper() or name.islower(): name = name.title() parts[-1] = name return ': '.join(parts) def make_field_formatter(attr, filters=None): """Given an object attribute. Return a formatted field name and a formatter suitable for passing to print_list. Optionally pass a dict mapping attribute names to a function. The function will be passed the value of the attribute and should return the string to display. """ filter_ = None if filters: filter_ = filters.get(attr) def get_field(obj): field = getattr(obj, attr, '') if field and filter_: field = filter_(field) return field name = _format_field_name(attr) formatter = get_field return name, formatter def _get_list_table_columns_and_formatters(fields, objs, exclude_fields=(), filters=None): """Check and add fields to output columns. If there is any value in fields that not an attribute of obj, CommandError will be raised. If fields has duplicate values (case sensitive), we will make them unique and ignore duplicate ones. :param fields: A list of string contains the fields to be printed. :param objs: An list of object which will be used to check if field is valid or not. Note, we don't check fields if obj is None or empty. :param exclude_fields: A tuple of string which contains the fields to be excluded. :param filters: A dictionary defines how to get value from fields, this is useful when field's value is a complex object such as dictionary. :return: columns, formatters. columns is a list of string which will be used as table header. formatters is a dictionary specifies how to display the value of the field. They can be [], {}. :raise: magnumclient.exceptions.CommandError. """ if objs and isinstance(objs, list): obj = objs[0] else: obj = None fields = None columns = [] formatters = {} if fields: non_existent_fields = [] exclude_fields = set(exclude_fields) for field in fields.split(','): if not hasattr(obj, field): non_existent_fields.append(field) continue if field in exclude_fields: continue field_title, formatter = make_field_formatter(field, filters) columns.append(field_title) formatters[field_title] = formatter exclude_fields.add(field) if non_existent_fields: raise exceptions.CommandError( _("Non-existent fields are specified: %s") % non_existent_fields ) return columns, formatters ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1014192 python_magnumclient-5.0.0/magnumclient/common/httpclient.py0000664000175000017500000004154515244246676023156 0ustar00zuulzuul# -*- coding: utf-8 -*- # # Copyright 2012 OpenStack LLC. # 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 http import client as http_client import io import logging import os import socket import ssl from urllib import parse as urlparse from keystoneauth1 import adapter from oslo_serialization import jsonutils from oslo_utils import importutils from magnumclient import exceptions osprofiler_web = importutils.try_import("osprofiler.web") LOG = logging.getLogger(__name__) USER_AGENT = 'python-magnumclient' CHUNKSIZE = 1024 * 64 # 64kB API_VERSION = '/v1' DEFAULT_API_VERSION = 'latest' def _extract_error_json_text(body_json): error_json = {} if 'error_message' in body_json: raw_msg = body_json['error_message'] error_json = jsonutils.loads(raw_msg) elif 'error' in body_json: error_body = body_json['error'] error_json = {'faultstring': error_body['title'], 'debuginfo': error_body['message']} else: error_body = body_json['errors'][0] error_json = {'faultstring': error_body['title']} if 'detail' in error_body: error_json['debuginfo'] = error_body['detail'] elif 'description' in error_body: error_json['debuginfo'] = error_body['description'] return error_json def _extract_error_json(body, resp): """Return error_message from the HTTP response body.""" try: content_type = resp.headers.get("Content-Type", "") except AttributeError: content_type = "" if content_type.startswith("application/json"): try: body_json = resp.json() return _extract_error_json_text(body_json) except AttributeError: body_json = jsonutils.loads(body) return _extract_error_json_text(body_json) except ValueError: return {} else: try: body_json = jsonutils.loads(body) return _extract_error_json_text(body_json) except ValueError: return {} class HTTPClient(object): def __init__(self, endpoint, api_version=DEFAULT_API_VERSION, **kwargs): self.endpoint = endpoint self.auth_token = kwargs.get('token') self.auth_ref = kwargs.get('auth_ref') self.api_version = api_version self.connection_params = self.get_connection_params(endpoint, **kwargs) @staticmethod def get_connection_params(endpoint, **kwargs): parts = urlparse.urlparse(endpoint) # trim API version and trailing slash from endpoint path = parts.path path = path.rstrip('/').rstrip(API_VERSION) _args = (parts.hostname, parts.port, path) _kwargs = {'timeout': (float(kwargs.get('timeout')) if kwargs.get('timeout') else 600)} if parts.scheme == 'https': _class = VerifiedHTTPSConnection _kwargs['ca_file'] = kwargs.get('ca_file', None) _kwargs['cert_file'] = kwargs.get('cert_file', None) _kwargs['key_file'] = kwargs.get('key_file', None) _kwargs['insecure'] = kwargs.get('insecure', False) elif parts.scheme == 'http': _class = http_client.HTTPConnection else: msg = 'Unsupported scheme: %s' % parts.scheme raise exceptions.EndpointException(msg) return (_class, _args, _kwargs) def get_connection(self): _class = self.connection_params[0] return _class(*self.connection_params[1][0:2], **self.connection_params[2]) def log_curl_request(self, method, url, kwargs): curl = ['curl -i -X %s' % method] for (key, value) in kwargs['headers'].items(): header = '-H \'%s: %s\'' % (key, value) curl.append(header) conn_params_fmt = [ ('key_file', '--key %s'), ('cert_file', '--cert %s'), ('ca_file', '--cacert %s'), ] for (key, fmt) in conn_params_fmt: value = self.connection_params[2].get(key) if value: curl.append(fmt % value) if self.connection_params[2].get('insecure'): curl.append('-k') if 'body' in kwargs: curl.append('-d \'%s\'' % kwargs['body']) curl.append('%s/%s' % (self.endpoint, url.lstrip(API_VERSION))) LOG.debug(' '.join(curl)) @staticmethod def log_http_response(resp, body=None): status = (resp.version / 10.0, resp.status, resp.reason) dump = ['\nHTTP/%.1f %s %s' % status] dump.extend(['%s: %s' % (k, v) for k, v in resp.getheaders()]) dump.append('') if body: dump.extend([body, '']) LOG.debug('\n'.join(dump)) def _make_connection_url(self, url): (_class, _args, _kwargs) = self.connection_params base_url = _args[2] return '%s/%s' % (base_url, url.lstrip('/')) def _http_request(self, url, method, **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 kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) kwargs['headers'].setdefault('User-Agent', USER_AGENT) if self.api_version: version_string = 'container-infra %s' % self.api_version kwargs['headers'].setdefault( 'OpenStack-API-Version', version_string) if self.auth_token: kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) self.log_curl_request(method, url, kwargs) conn = self.get_connection() try: conn_url = self._make_connection_url(url) conn.request(method, conn_url, **kwargs) resp = conn.getresponse() except socket.gaierror as e: message = ("Error finding address for %(url)s: %(e)s" % dict(url=url, e=e)) raise exceptions.EndpointNotFound(message) except (socket.error, socket.timeout) as e: endpoint = self.endpoint message = ("Error communicating with %(endpoint)s %(e)s" % dict(endpoint=endpoint, e=e)) raise exceptions.ConnectionRefused(message) body_iter = ResponseBodyIterator(resp) # Read body into string if it isn't obviously image data body_str = None if resp.getheader('content-type', None) != 'application/octet-stream': # decoding byte to string is necessary for Python 3.4 compatibility # this issues has not been found with Python 3.4 unit tests # because the test creates a fake http response of type str # the if statement satisfies test (str) and real (bytes) behavior body_list = [ chunk.decode("utf-8") if isinstance(chunk, bytes) else chunk for chunk in body_iter ] body_str = ''.join(body_list) self.log_http_response(resp, body_str) body_iter = io.StringIO(body_str) else: self.log_http_response(resp) if 400 <= resp.status < 600: LOG.warning("Request returned failure status.") error_json = _extract_error_json(body_str, resp) raise exceptions.from_response( resp, error_json.get('faultstring'), error_json.get('debuginfo'), method, url) elif resp.status in (301, 302, 305): # Redirected. Reissue the request to the new location. return self._http_request(resp['location'], method, **kwargs) elif resp.status == 300: raise exceptions.from_response(resp, method=method, url=url) return resp, body_iter def json_request(self, method, url, **kwargs): kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('Content-Type', 'application/json') kwargs['headers'].setdefault('Accept', 'application/json') if 'body' in kwargs: kwargs['body'] = jsonutils.dumps(kwargs['body']) resp, body_iter = self._http_request(url, method, **kwargs) content_type = resp.getheader('content-type', None) if resp.status == 204 or resp.status == 205 or content_type is None: return resp, list() if 'application/json' in content_type: body = ''.join([chunk for chunk in body_iter]) try: body = jsonutils.loads(body) except ValueError: LOG.error('Could not decode response body as JSON') else: body = None return resp, body def raw_request(self, method, url, **kwargs): kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('Content-Type', 'application/octet-stream') return self._http_request(url, method, **kwargs) class VerifiedHTTPSConnection(http_client.HTTPSConnection): """httplib-compatibile connection using client-side SSL authentication :see http://code.activestate.com/recipes/ 577548-https-httplib-client-connection-with-certificate-v/ """ def __init__(self, host, port, key_file=None, cert_file=None, ca_file=None, timeout=None, insecure=False): http_client.HTTPSConnection.__init__(self, host, port) self.key_file = key_file self.cert_file = cert_file if ca_file is not None: self.ca_file = ca_file else: self.ca_file = self.get_system_ca_file() self.timeout = timeout self.insecure = insecure def connect(self): """Connect to a host on a given (SSL) port. If ca_file is pointing somewhere, use it to check Server Certificate. Redefined/copied and extended from httplib.py:1105 (Python 2.6.x). This is needed to pass cert_reqs=ssl.CERT_REQUIRED as parameter to ssl.wrap_socket(), which forces SSL to check server certificate against our client certificate. """ sock = socket.create_connection((self.host, self.port), self.timeout) context = ssl.SSLContext(ssl.PROTOCOL_TLS_CLIENT) if self._tunnel_host: self.sock = sock self._tunnel() if self.insecure is True: context.check_hostname = False context.verify_mode = ssl.CERT_NONE else: context.verify_mode = ssl.CERT_REQUIRED context.load_verify_locations(self.ca_file) if self.cert_file: context.load_cert_chain(self.cert_file, self.key_file) self.sock = context.wrap_socket(sock, server_hostname=self.host) @staticmethod def get_system_ca_file(): """Return path to system default CA file.""" # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, # Suse, FreeBSD/OpenBSD ca_path = ['/etc/ssl/certs/ca-certificates.crt', '/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/ca-bundle.pem', '/etc/ssl/cert.pem'] for ca in ca_path: if os.path.exists(ca): return ca return None class SessionClient(adapter.LegacyJsonAdapter): """HTTP client based on Keystone client session.""" def __init__(self, user_agent=USER_AGENT, logger=LOG, api_version=DEFAULT_API_VERSION, *args, **kwargs): self.user_agent = USER_AGENT self.api_version = api_version super(SessionClient, self).__init__(*args, **kwargs) def _http_request(self, url, method, **kwargs): if url.startswith(API_VERSION): url = url[len(API_VERSION):] kwargs.setdefault('user_agent', self.user_agent) kwargs.setdefault('auth', self.auth) kwargs.setdefault('endpoint_override', self.endpoint_override) # Copy the kwargs so we can reuse the original in case of redirects kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) kwargs['headers'].setdefault('User-Agent', self.user_agent) # NOTE(tovin07): osprofiler_web.get_trace_id_headers does not add any # headers in case if osprofiler is not initialized. if osprofiler_web: kwargs['headers'].update(osprofiler_web.get_trace_id_headers()) if self.api_version: version_string = 'container-infra %s' % self.api_version kwargs['headers'].setdefault( 'OpenStack-API-Version', version_string) endpoint_filter = kwargs.setdefault('endpoint_filter', {}) endpoint_filter.setdefault('interface', self.interface) endpoint_filter.setdefault('service_type', self.service_type) endpoint_filter.setdefault('region_name', self.region_name) resp = self.session.request(url, method, raise_exc=False, **kwargs) if 400 <= resp.status_code < 600: error_json = _extract_error_json(resp.content, resp) raise exceptions.from_response( resp, error_json.get('faultstring'), error_json.get('debuginfo'), method, url) elif resp.status_code in (301, 302, 305): # Redirected. Reissue the request to the new location. location = resp.headers.get('location') resp = self._http_request(location, method, **kwargs) elif resp.status_code == 300: raise exceptions.from_response(resp, method=method, url=url) return resp def json_request(self, method, url, **kwargs): kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('Content-Type', 'application/json') kwargs['headers'].setdefault('Accept', 'application/json') if 'body' in kwargs: kwargs['data'] = jsonutils.dumps(kwargs.pop('body')) resp = self._http_request(url, method, **kwargs) body = resp.content content_type = resp.headers.get('content-type', None) status = resp.status_code if status == 204 or status == 205 or content_type is None: return resp, list() if 'application/json' in content_type: try: body = resp.json() except ValueError: LOG.error('Could not decode response body as JSON') else: body = None return resp, body def raw_request(self, method, url, **kwargs): kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('Content-Type', 'application/octet-stream') resp = self._http_request(url, method, **kwargs) body = resp.content status = resp.status_code content_type = resp.headers.get('content-type', None) if status == 204 or status == 205 or content_type is None: return resp, list() if 'application/json' in content_type: try: body = resp.json() except ValueError: LOG.error('Could not decode response body as JSON') else: body = None return resp, body class ResponseBodyIterator(object): """A class that acts as an iterator over an HTTP response.""" def __init__(self, resp): self.resp = resp def __iter__(self): while True: try: yield self.next() except StopIteration: return def __bool__(self): return hasattr(self, 'items') __nonzero__ = __bool__ # Python 2.x compatibility def next(self): chunk = self.resp.read(CHUNKSIZE) if chunk: return chunk else: raise StopIteration def _construct_http_client(*args, **kwargs): session = kwargs.pop('session', None) auth = kwargs.pop('auth', None) if session: service_type = kwargs.pop('service_type', 'baremetal') interface = kwargs.pop('endpoint_type', None) region_name = kwargs.pop('region_name', None) return SessionClient(session=session, auth=auth, interface=interface, service_type=service_type, region_name=region_name, service_name=None, user_agent='python-magnumclient') else: return HTTPClient(*args, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1014192 python_magnumclient-5.0.0/magnumclient/common/utils.py0000664000175000017500000003156315244246676022137 0ustar00zuulzuul# -*- coding: utf-8 -*- # # Copyright 2012 OpenStack LLC. # 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 cryptography.hazmat.backends import default_backend from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives import serialization from cryptography import x509 from cryptography.x509.oid import NameOID from oslo_serialization import base64 from oslo_serialization import jsonutils from magnumclient import exceptions as exc from magnumclient.i18n import _ def common_filters(marker=None, limit=None, sort_key=None, sort_dir=None): """Generate common filters for any list request. :param marker: entity ID from which to start returning entities. :param limit: maximum number of entities to return. :param sort_key: field to use for sorting. :param sort_dir: direction of sorting: 'asc' or 'desc'. :returns: list of string filters. """ filters = [] if isinstance(limit, int): filters.append('limit=%s' % limit) if marker is not None: filters.append('marker=%s' % marker) if sort_key is not None: filters.append('sort_key=%s' % sort_key) if sort_dir is not None: filters.append('sort_dir=%s' % sort_dir) return filters def split_and_deserialize(string): """Split and try to JSON deserialize a string. Gets a string with the KEY=VALUE format, split it (using '=' as the separator) and try to JSON deserialize the VALUE. :returns: A tuple of (key, value). """ try: key, value = string.split("=", 1) except ValueError: raise exc.CommandError(_('Attributes must be a list of ' 'PATH=VALUE not "%s"') % string) try: value = jsonutils.loads(value) except ValueError: pass return (key, value) def args_array_to_patch(op, attributes): patch = [] for attr in attributes: # Sanitize if not attr.startswith('/'): attr = '/' + attr if op in ['add', 'replace']: path, value = split_and_deserialize(attr) if path == "/labels" or path == "/health_status_reason": a = [] a.append(value) value = str(handle_labels(a)) patch.append({'op': op, 'path': path, 'value': value}) else: patch.append({'op': op, 'path': path, 'value': value}) elif op == "remove": # For remove only the key is needed patch.append({'op': op, 'path': attr}) else: raise exc.CommandError(_('Unknown PATCH operation: %s') % op) return patch def handle_labels(labels): labels = format_labels(labels) if 'mesos_slave_executor_env_file' in labels: environment_variables_data = handle_json_from_file( labels['mesos_slave_executor_env_file']) labels['mesos_slave_executor_env_variables'] = jsonutils.dumps( environment_variables_data) return labels def format_labels(lbls, parse_comma=True): '''Reformat labels into dict of format expected by the API.''' if not lbls: return {} if parse_comma: # expect multiple invocations of --labels but fall back # to either , or ; delimited if only one --labels is specified if len(lbls) == 1 and lbls[0].count('=') > 1: lbls = lbls[0].replace(';', ',').split(',') labels = {} for lbl in lbls: try: (k, v) = lbl.split(('='), 1) except ValueError: raise exc.CommandError(_('labels must be a list of KEY=VALUE ' 'not %s') % lbl) if k not in labels: labels[k] = v else: labels[k] += ",%s" % v return labels _VALID_TAINT_EFFECTS = ('NoSchedule', 'PreferNoSchedule', 'NoExecute') def handle_taints(taints): """Reformat --node-taints values into the API's list of dicts. Accepts one or more strings of the form ``KEY[=VALUE]:EFFECT`` (e.g. ``key1=value1:NoSchedule`` or ``key2:NoExecute``), with semicolons also accepted as a separator, and returns a list of ``{key, value, effect}`` dicts. ``value`` is optional and defaults to an empty string; ``key`` and ``effect`` are required. """ if not taints: return [] if len(taints) == 1: taints = taints[0].replace(';', ',').split(',') parsed = [] for raw in taints: try: head, effect = raw.rsplit(':', 1) except ValueError: raise exc.CommandError(_( 'taints must be a list of KEY[=VALUE]:EFFECT, not %s') % raw) # VALUE is optional: "key:effect" is equivalent to "key=:effect". key, _sep, value = head.partition('=') if not key or effect not in _VALID_TAINT_EFFECTS: raise exc.CommandError(_( "taints must be KEY[=VALUE]:EFFECT where EFFECT is one of " "NoSchedule, PreferNoSchedule, NoExecute, not %s") % raw) parsed.append({'key': key, 'value': value, 'effect': effect}) return parsed def print_list_field(field): return lambda obj: ', '.join(getattr(obj, field)) def handle_json_from_file(json_arg): """Attempts to read JSON file by the file url. :param json_arg: May be a file name containing the JSON. :returns: A list or dictionary parsed from JSON. """ try: with open(json_arg, 'r') as f: json_arg = f.read().strip() json_arg = jsonutils.loads(json_arg) except IOError as e: err = _("Cannot get JSON from file '%(file)s'. " "Error: %(err)s") % {'err': e, 'file': json_arg} raise exc.InvalidAttribute(err) except ValueError as e: err = (_("For JSON: '%(string)s', error: '%(err)s'") % {'err': e, 'string': json_arg}) raise exc.InvalidAttribute(err) return json_arg def config_cluster(cluster, cluster_template, cfg_dir, force=False, certs=None, use_keystone=False, direct_output=False): """Return and write configuration for the given cluster.""" if cluster_template.coe == 'kubernetes': return _config_cluster_kubernetes(cluster, cluster_template, cfg_dir, force, certs, use_keystone, direct_output) elif (cluster_template.coe == 'swarm' or cluster_template.coe == 'swarm-mode'): return _config_cluster_swarm(cluster, cluster_template, cfg_dir, force, certs) def _config_cluster_kubernetes(cluster, cluster_template, cfg_dir, force=False, certs=None, use_keystone=False, direct_output=False): """Return and write configuration for the given kubernetes cluster.""" cfg_file = "%s/config" % cfg_dir if cluster_template.tls_disabled or certs is None: cfg = ("apiVersion: v1\n" "clusters:\n" "- cluster:\n" " server: %(api_address)s\n" " name: %(name)s\n" "contexts:\n" "- context:\n" " cluster: %(name)s\n" " user: %(name)s\n" " name: %(name)s\n" "current-context: %(name)s\n" "kind: Config\n" "preferences: {}\n" "users:\n" "- name: %(name)s\n" % {'name': cluster.name, 'api_address': cluster.api_address}) else: if not use_keystone: cfg = ("apiVersion: v1\n" "clusters:\n" "- cluster:\n" " certificate-authority-data: %(ca)s\n" " server: %(api_address)s\n" " name: %(name)s\n" "contexts:\n" "- context:\n" " cluster: %(name)s\n" " user: admin\n" " name: default\n" "current-context: default\n" "kind: Config\n" "preferences: {}\n" "users:\n" "- name: admin\n" " user:\n" " client-certificate-data: %(cert)s\n" " client-key-data: %(key)s\n" % {'name': cluster.name, 'api_address': cluster.api_address, 'key': base64.encode_as_text(certs['key']), 'cert': base64.encode_as_text(certs['cert']), 'ca': base64.encode_as_text(certs['ca'])}) else: cfg = ("apiVersion: v1\n" "clusters:\n" "- cluster:\n" " certificate-authority-data: %(ca)s\n" " server: %(api_address)s\n" " name: %(name)s\n" "contexts:\n" "- context:\n" " cluster: %(name)s\n" " user: openstackuser\n" " name: openstackuser@kubernetes\n" "current-context: openstackuser@kubernetes\n" "kind: Config\n" "preferences: {}\n" "users:\n" "- name: openstackuser\n" " user:\n" " exec:\n" " command: /bin/bash\n" " apiVersion: client.authentication.k8s.io/v1beta1\n" " args:\n" " - -c\n" " - >\n" " if [ -z ${OS_TOKEN} ]; then\n" " echo 'Error: Missing OpenStack credential from environment variable $OS_TOKEN' > /dev/stderr\n" # noqa " exit 1\n" " else\n" " echo '{ \"apiVersion\": \"client.authentication.k8s.io/v1beta1\", \"kind\": \"ExecCredential\", \"status\": { \"token\": \"'\"${OS_TOKEN}\"'\"}}'\n" # noqa " fi\n" % {'name': cluster.name, 'api_address': cluster.api_address, 'ca': base64.encode_as_text(certs['ca'])}) if direct_output: return cfg if os.path.exists(cfg_file) and not force: raise exc.CommandError("File %s exists, aborting." % cfg_file) else: f = open(cfg_file, "w") f.write(cfg) f.close() if 'csh' in os.environ['SHELL']: return "setenv KUBECONFIG %s\n" % cfg_file else: return "export KUBECONFIG=%s\n" % cfg_file def _config_cluster_swarm(cluster, cluster_template, cfg_dir, force=False, certs=None): """Return and write configuration for the given swarm cluster.""" tls = "" if cluster_template.tls_disabled else True if 'csh' in os.environ['SHELL']: result = ("setenv DOCKER_HOST %(docker_host)s\n" "setenv DOCKER_CERT_PATH %(cfg_dir)s\n" "setenv DOCKER_TLS_VERIFY %(tls)s\n" % {'docker_host': cluster.api_address, 'cfg_dir': cfg_dir, 'tls': tls} ) else: result = ("export DOCKER_HOST=%(docker_host)s\n" "export DOCKER_CERT_PATH=%(cfg_dir)s\n" "export DOCKER_TLS_VERIFY=%(tls)s\n" % {'docker_host': cluster.api_address, 'cfg_dir': cfg_dir, 'tls': tls} ) return result def generate_csr_and_key(): """Return a dict with a new csr and key.""" key = rsa.generate_private_key( public_exponent=65537, key_size=2048, backend=default_backend()) csr = x509.CertificateSigningRequestBuilder().subject_name( x509.Name([ x509.NameAttribute(NameOID.COMMON_NAME, u"admin"), x509.NameAttribute(NameOID.ORGANIZATION_NAME, u"system:masters") ])).sign(key, hashes.SHA256(), default_backend()) result = { 'csr': csr.public_bytes( encoding=serialization.Encoding.PEM).decode("utf-8"), 'key': key.private_bytes( encoding=serialization.Encoding.PEM, format=serialization.PrivateFormat.TraditionalOpenSSL, encryption_algorithm=serialization.NoEncryption()).decode("utf-8"), } return result ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1024191 python_magnumclient-5.0.0/magnumclient/exceptions.py0000664000175000017500000003323115244246676021662 0ustar00zuulzuul# -*- coding: utf-8 -*- # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import inspect import sys from magnumclient.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 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) ) # NOTE(akurilin): These aliases are left here since v.0.1.3 to support # backwards compatibility. InvalidEndpoint = EndpointException CommunicationError = ConnectionRefused HTTPBadRequest = BadRequest HTTPInternalServerError = InternalServerError HTTPNotFound = NotFound HTTPServiceUnavailable = ServiceUnavailable class AmbiguousAuthSystem(ClientException): """Could not obtain token and endpoint using provided credentials.""" pass # Alias for backwards compatibility AmbigiousAuthSystem = AmbiguousAuthSystem class InvalidAttribute(ClientException): pass def from_response(response, message=None, traceback=None, method=None, url=None): """Return an HttpError instance based on response from httplib/requests.""" error_body = {} if message: error_body['message'] = message if traceback: error_body['details'] = traceback if hasattr(response, 'status') and not hasattr(response, 'status_code'): # NOTE(akurilin): These modifications around response object give # ability to get all necessary information in method `from_response` # from common code, which expecting response object from `requests` # library instead of object from `httplib/httplib2` library. response.status_code = response.status response.headers = { 'Content-Type': response.getheader('content-type', "")} if hasattr(response, 'status_code'): # NOTE(hongbin): This allows SessionClient to handle faultstring. response.json = lambda: {'error': error_body} if (response.headers.get('Content-Type', '').startswith('text/') and not hasattr(response, 'text')): # NOTE(clif_h): There seems to be a case where if the content-type # of the response is text/* then it expects the response to have a # 'text' attribute, but that doesn't always seem to necessarily be # the case. This is to work around that problem. response.text = '' 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) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1024191 python_magnumclient-5.0.0/magnumclient/i18n.py0000664000175000017500000000151515244246676020260 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """oslo_i18n integration module for magnumclient. See https://docs.openstack.org/oslo.i18n/latest/user/usage.html . """ import oslo_i18n _translators = oslo_i18n.TranslatorFactory(domain='magnumclient') # The primary translation function using the well-known name "_" _ = _translators.primary ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9945781 python_magnumclient-5.0.0/magnumclient/osc/0000775000175000017500000000000015244246764017707 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1024191 python_magnumclient-5.0.0/magnumclient/osc/__init__.py0000664000175000017500000000000015244246676022010 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1024191 python_magnumclient-5.0.0/magnumclient/osc/plugin.py0000664000175000017500000000462015244246676021563 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import logging from osc_lib import utils LOG = logging.getLogger(__name__) DEFAULT_MAJOR_API_VERSION = '1' DEFAULT_MAGNUM_API_VERSION = 'latest' API_VERSION_OPTION = 'os_container_infra_api_version' API_NAME = 'container_infra' API_VERSIONS = { '1': 'magnumclient.v1.client.Client', } def make_client(instance): """Returns a magnum client.""" requested_version = instance._api_version[API_NAME] # API_VERSIONS is keyed by major version only; a full microversion string # like '1.12' must be reduced to its major part for the class lookup. major_version = requested_version.split('.')[0] magnum_client = utils.get_client_class( API_NAME, major_version, API_VERSIONS) LOG.debug('Instantiating magnum client: %s', magnum_client) # Forward an explicit microversion to the HTTP client; fall back to # 'latest' when only a bare major version was given (the default). api_version = (requested_version if '.' in requested_version else DEFAULT_MAGNUM_API_VERSION) client = magnum_client(session=instance.session, region_name=instance._region_name, interface=instance._interface, insecure=instance._insecure, ca_cert=instance._cacert, api_version=api_version) return client def build_option_parser(parser): """Hook to add global options""" parser.add_argument( '--os-container-infra-api-version', metavar='', default=utils.env( 'OS_CONTAINER_INFRA_API_VERSION', default=DEFAULT_MAJOR_API_VERSION), help='Container-Infra API version, default=' + DEFAULT_MAJOR_API_VERSION + ' (Env: OS_CONTAINER_INFRA_API_VERSION)') return parser ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9965782 python_magnumclient-5.0.0/magnumclient/osc/v1/0000775000175000017500000000000015244246764020235 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1024191 python_magnumclient-5.0.0/magnumclient/osc/v1/__init__.py0000664000175000017500000000000015244246676022336 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1024191 python_magnumclient-5.0.0/magnumclient/osc/v1/certificates.py0000664000175000017500000000730615244246676023264 0ustar00zuulzuul# Copyright 2015 NEC Corporation. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os from magnumclient.i18n import _ from osc_lib.command import command def _show_cert(certificate): try: print(certificate.pem) except AttributeError: return None def _get_target_uuid(cs, args): target = None if args.cluster: target = cs.clusters.get(args.cluster) return target.uuid class RotateCa(command.Command): _description = _("Rotate the CA certificate for cluster to revoke access.") def get_parser(self, prog_name): parser = super(RotateCa, self).get_parser(prog_name) parser.add_argument('cluster', metavar='', help='ID or name of the cluster') return parser def take_action(self, parsed_args): mag_client = self.app.client_manager.container_infra cluster = mag_client.clusters.get(parsed_args.cluster) opts = { 'cluster_uuid': cluster.uuid } mag_client.certificates.rotate_ca(**opts) print("Request to rotate the CA certificate for cluster %s " "has been accepted." % cluster.uuid) class ShowCa(command.Command): _description = _("Show details about the CA certificate for a cluster.") def get_parser(self, prog_name): parser = super(ShowCa, self).get_parser(prog_name) # NOTE: All arguments are positional and, if not provided # with a default, required. parser.add_argument('cluster', metavar='', help='ID or name of the cluster') return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra cluster = mag_client.clusters.get(parsed_args.cluster) cert = mag_client.certificates.get(cluster.uuid) _show_cert(cert) class SignCa(command.Command): _description = _("Generate the CA certificate for a cluster.") def get_parser(self, prog_name): parser = super(SignCa, self).get_parser(prog_name) # NOTE: All arguments are positional and, if not provided # with a default, required. parser.add_argument('cluster', metavar='', help='ID or name of the cluster') parser.add_argument('csr', metavar='', help='File path of csr file to send to Magnum' ' to get signed.') return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra opts = { 'cluster_uuid': _get_target_uuid(mag_client, parsed_args) } if parsed_args.csr is None or not os.path.isfile(parsed_args.csr): print('A CSR must be provided.') return with open(parsed_args.csr, 'r') as f: opts['csr'] = f.read() cert = mag_client.certificates.create(**opts) _show_cert(cert) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1034193 python_magnumclient-5.0.0/magnumclient/osc/v1/cluster_templates.py0000664000175000017500000005766215244246676024370 0ustar00zuulzuul# Copyright 2016 EasyStack. 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 magnumclient.common import utils as magnum_utils from magnumclient.exceptions import InvalidAttribute from magnumclient.i18n import _ from magnumclient.v1.cluster_templates import ( CLUSTER_TEMPLATE_ATTRIBUTES ) from osc_lib.command import command from osc_lib import utils as osc_utils from oslo_log import log as logging def _show_cluster_template(cluster_template): del cluster_template._info['links'] for field in cluster_template._info: if cluster_template._info[field] is None: setattr(cluster_template, field, '-') columns = CLUSTER_TEMPLATE_ATTRIBUTES return columns, osc_utils.get_item_properties(cluster_template, columns) class CreateClusterTemplate(command.ShowOne): """Create a Cluster Template.""" _description = _("Create a Cluster Template.") def get_parser(self, prog_name): parser = super(CreateClusterTemplate, self).get_parser(prog_name) parser.add_argument( 'name', metavar='', help=_('Name of the cluster template to create.')) parser.add_argument( '--coe', required=True, metavar='', help=_('Specify the Container Orchestration Engine to use.')) parser.add_argument( '--image', required=True, metavar='', help=_('The name or UUID of the base image to customize for the ' 'Cluster.')) parser.add_argument( '--external-network', dest='external_network', required=True, metavar='', help=_('The external Neutron network name or UUID to connect to ' 'this Cluster Template.')) parser.add_argument( '--keypair', metavar='', help=_('The name or UUID of the SSH keypair to load into the ' 'Cluster nodes.')) parser.add_argument( '--fixed-network', dest='fixed_network', metavar='', help=_('The private Neutron network name to connect to this ' 'Cluster model.')) parser.add_argument( '--fixed-subnet', dest='fixed_subnet', metavar='', help=_('The private Neutron subnet name to connect to Cluster.')) parser.add_argument( '--network-driver', dest='network_driver', metavar='', help=_('The network driver name for instantiating container ' 'networks.')) parser.add_argument( '--volume-driver', dest='volume_driver', metavar='', help=_('The volume driver name for instantiating container ' 'volume.')) parser.add_argument( '--driver', dest='driver', metavar='', help=_('The name of the Magnum driver to use for this cluster ' 'template.')) parser.add_argument( '--dns-nameserver', dest='dns_nameserver', metavar='', default='8.8.8.8', help=_('The DNS nameserver to use for this cluster template.')) parser.add_argument( '--flavor', metavar='', default='m1.medium', help=_('The nova flavor name or UUID to use when launching the ' 'Cluster.')) parser.add_argument( '--master-flavor', dest='master_flavor', metavar='', help=_('The nova flavor name or UUID to use when launching the ' 'master node of the Cluster.')) parser.add_argument( '--docker-volume-size', dest='docker_volume_size', metavar='', type=int, help=_('Specify the number of size in GB for the docker volume ' 'to use.')) parser.add_argument( '--docker-storage-driver', dest='docker_storage_driver', metavar='', default='overlay2', help=_('Select a docker storage driver. Default: overlay2')) parser.add_argument( '--http-proxy', dest='http_proxy', metavar='', help=_('The http_proxy address to use for nodes in Cluster.')) parser.add_argument( '--https-proxy', dest='https_proxy', metavar='', help=_('The https_proxy address to use for nodes in Cluster.')) parser.add_argument( '--no-proxy', dest='no_proxy', metavar='', help=_('The no_proxy address to use for nodes in Cluster.')) parser.add_argument( '--labels', metavar='', action='append', default=[], help=_('Arbitrary labels in the form of key=value pairs to ' 'associate with a cluster template. May be used multiple ' 'times.')) parser.add_argument( '--tls-disabled', dest='tls_disabled', action='store_true', default=False, help=_('Disable TLS in the Cluster.')) parser.add_argument( '--public', action='store_true', default=False, help=_('Make cluster template public.')) parser.add_argument( '--registry-enabled', dest='registry_enabled', action='store_true', default=False, help=_('Enable docker registry in the Cluster')) parser.add_argument( '--insecure-registry', dest='insecure_registry', help=_('The URL pointing to users own private insecure docker' 'registry to deploy and run docker containers.')) parser.add_argument( '--server-type', dest='server_type', metavar='', default='vm', help=_('Specify the server type to be used for example vm. ' 'For this release default server type will be vm.')) parser.add_argument( '--master-lb-enabled', dest='master_lb_enabled', action='store_true', default=False, help=_('Indicates whether created Clusters should have a load ' 'balancer for master nodes or not.')) parser.add_argument( '--floating-ip-enabled', dest='floating_ip_enabled', default=[], action='append_const', const=True, help=_('Indicates whether created Clusters should have a ' 'floating ip.')) parser.add_argument( '--floating-ip-disabled', dest='floating_ip_enabled', action='append_const', const=False, help=_('Disables floating ip creation on the new Cluster')) parser.add_argument( '--hidden', dest='hidden', action='store_true', default=False, help=_('Indicates the cluster template should be hidden.')) parser.add_argument( '--visible', dest='hidden', action='store_false', help=_('Indicates the cluster template should be visible.')) parser.add_argument( '--tags', action='append', default=[], metavar='<--tags tag1 --tags tag2,tag3>', help=_('Tags to be added to the cluster template.')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra args = { 'name': parsed_args.name, 'image_id': parsed_args.image, 'keypair_id': parsed_args.keypair, 'external_network_id': parsed_args.external_network, 'coe': parsed_args.coe, 'fixed_network': parsed_args.fixed_network, 'fixed_subnet': parsed_args.fixed_subnet, 'network_driver': parsed_args.network_driver, 'volume_driver': parsed_args.volume_driver, 'driver': parsed_args.driver, 'dns_nameserver': parsed_args.dns_nameserver, 'flavor_id': parsed_args.flavor, 'master_flavor_id': parsed_args.master_flavor, 'docker_volume_size': parsed_args.docker_volume_size, 'docker_storage_driver': parsed_args.docker_storage_driver, 'http_proxy': parsed_args.http_proxy, 'https_proxy': parsed_args.https_proxy, 'no_proxy': parsed_args.no_proxy, 'labels': magnum_utils.handle_labels(parsed_args.labels), 'tls_disabled': parsed_args.tls_disabled, 'public': parsed_args.public, 'registry_enabled': parsed_args.registry_enabled, 'insecure_registry': parsed_args.insecure_registry, 'server_type': parsed_args.server_type, 'master_lb_enabled': parsed_args.master_lb_enabled, } # NOTE: Make new arguments backward compatible if parsed_args.hidden: args['hidden'] = parsed_args.hidden if parsed_args.tags: args['tags'] = ','.join(set( (','.join(parsed_args.tags)).split(','))) if len(parsed_args.floating_ip_enabled) > 1: raise InvalidAttribute('--floating-ip-enabled and ' '--floating-ip-disabled are ' 'mutually exclusive and ' 'should be specified only once.') elif len(parsed_args.floating_ip_enabled) == 1: args['floating_ip_enabled'] = parsed_args.floating_ip_enabled[0] ct = mag_client.cluster_templates.create(**args) print("Request to create cluster template %s accepted" % parsed_args.name) return _show_cluster_template(ct) class DeleteClusterTemplate(command.Command): """Delete a Cluster Template.""" _description = _("Delete a Cluster Template.") def get_parser(self, prog_name): parser = super(DeleteClusterTemplate, self).get_parser(prog_name) parser.add_argument( 'cluster-templates', metavar='', nargs='+', help=_('ID or name of the (cluster template)s to delete.')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra for cluster_template in getattr(parsed_args, 'cluster-templates'): try: mag_client.cluster_templates.delete(cluster_template) print( "Request to delete cluster template %s has been accepted." % cluster_template) except Exception as e: print("Delete for cluster template " "%(cluster_template)s failed: %(e)s" % {'cluster_template': cluster_template, 'e': e}) class ListTemplateCluster(command.Lister): """List Cluster Templates.""" _description = _("List Cluster Templates.") log = logging.getLogger(__name__ + ".ListTemplateCluster") def get_parser(self, prog_name): parser = super(ListTemplateCluster, self).get_parser(prog_name) parser.add_argument( '--limit', metavar='', type=int, help=_('Maximum number of cluster templates to return')) parser.add_argument( '--sort-key', metavar='', help=_('Column to sort results by')) parser.add_argument( '--sort-dir', metavar='', choices=['desc', 'asc'], help=_('Direction to sort. "asc" or "desc".')) parser.add_argument( '--fields', default=None, metavar='', help=_('Comma-separated list of fields to display. ' 'Available fields: uuid, name, coe, image_id, public, ' 'link, apiserver_port, server_type, tls_disabled, ' 'registry_enabled' ) ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra columns = ['uuid', 'name', 'tags'] if parsed_args.fields: columns += parsed_args.fields.split(',') cts = mag_client.cluster_templates.list(limit=parsed_args.limit, sort_key=parsed_args.sort_key, sort_dir=parsed_args.sort_dir) return ( columns, (osc_utils.get_item_properties(ct, columns) for ct in cts) ) class ShowClusterTemplate(command.ShowOne): """Show a Cluster Template.""" _description = _("Show a Cluster Template.") log = logging.getLogger(__name__ + ".ShowClusterTemplate") def get_parser(self, prog_name): parser = super(ShowClusterTemplate, self).get_parser(prog_name) parser.add_argument( 'cluster-template', metavar='', help=_('ID or name of the cluster template to show.')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra ct = getattr(parsed_args, 'cluster-template', None) cluster_template = mag_client.cluster_templates.get(ct) return _show_cluster_template(cluster_template) class UpdateClusterTemplate(command.ShowOne): """Update a Cluster Template.""" log = logging.getLogger(__name__ + ".UpdateClusterTemplate") def get_parser(self, prog_name): parser = super(UpdateClusterTemplate, self).get_parser(prog_name) parser.add_argument( 'cluster-template', metavar='', help=_('The name or UUID of the cluster template to update.')) parser.add_argument( '--name', metavar='', help=_('New name for the cluster template.')) parser.add_argument( '--image', metavar='', help=_('The name or UUID of the base image to use.')) parser.add_argument( '--flavor', metavar='', help=_('The nova flavor name or UUID for cluster nodes.')) parser.add_argument( '--master-flavor', dest='master_flavor', metavar='', help=_('The nova flavor name or UUID for master nodes.')) parser.add_argument( '--keypair', metavar='', help=_('The name or UUID of the SSH keypair.')) parser.add_argument( '--external-network', dest='external_network', metavar='', help=_('The external Neutron network name or UUID.')) parser.add_argument( '--fixed-network', dest='fixed_network', metavar='', help=_('The private Neutron network name or UUID.')) parser.add_argument( '--fixed-subnet', dest='fixed_subnet', metavar='', help=_('The private Neutron subnet name or UUID.')) parser.add_argument( '--network-driver', dest='network_driver', metavar='', help=_('The network driver name for container networks.')) parser.add_argument( '--volume-driver', dest='volume_driver', metavar='', help=_('The volume driver name for container volumes.')) parser.add_argument( '--driver', metavar='', help=_('The Magnum driver to use for this cluster template.')) parser.add_argument( '--dns-nameserver', dest='dns_nameserver', metavar='', help=_('The DNS nameserver for this cluster template.')) parser.add_argument( '--docker-volume-size', dest='docker_volume_size', metavar='', type=int, help=_('Size in GB for the docker volume.')) parser.add_argument( '--docker-storage-driver', dest='docker_storage_driver', metavar='', help=_('Docker storage driver (overlay2 recommended).')) parser.add_argument( '--http-proxy', dest='http_proxy', metavar='', help=_('The http_proxy address for cluster nodes.')) parser.add_argument( '--https-proxy', dest='https_proxy', metavar='', help=_('The https_proxy address for cluster nodes.')) parser.add_argument( '--no-proxy', dest='no_proxy', metavar='', help=_('The no_proxy address list for cluster nodes.')) parser.add_argument( '--insecure-registry', dest='insecure_registry', metavar='', help=_('URL of a private insecure docker registry.')) parser.add_argument( '--label', metavar='', action='append', dest='labels', default=[], help=_('Label to add or update on the cluster template ' '(repeat to set multiple labels).')) parser.add_argument( '--no-label', metavar='', action='append', dest='no_labels', default=[], help=_('Label key to remove from the cluster template ' '(repeat to remove multiple labels).')) parser.add_argument( '--tag', metavar='', action='append', dest='tags', default=[], help=_('Tag to add to the cluster template ' '(repeat to set multiple tags).')) parser.set_defaults(hidden=None, public=None, tls_disabled=None, registry_enabled=None, master_lb_enabled=None, floating_ip_enabled=None) parser.add_argument( '--public', dest='public', action='store_true', help=_('Make the cluster template public.')) parser.add_argument( '--private', dest='public', action='store_false', help=_('Make the cluster template private.')) parser.add_argument( '--tls-disabled', dest='tls_disabled', action='store_true', help=_('Disable TLS in the cluster.')) parser.add_argument( '--tls-enabled', dest='tls_disabled', action='store_false', help=_('Enable TLS in the cluster.')) parser.add_argument( '--registry-enabled', dest='registry_enabled', action='store_true', help=_('Enable docker registry in the cluster.')) parser.add_argument( '--registry-disabled', dest='registry_enabled', action='store_false', help=_('Disable docker registry in the cluster.')) parser.add_argument( '--master-lb-enabled', dest='master_lb_enabled', action='store_true', help=_('Enable load balancer for master nodes.')) parser.add_argument( '--master-lb-disabled', dest='master_lb_enabled', action='store_false', help=_('Disable load balancer for master nodes.')) parser.add_argument( '--floating-ip-enabled', dest='floating_ip_enabled', action='store_true', help=_('Enable floating IPs for cluster nodes.')) parser.add_argument( '--floating-ip-disabled', dest='floating_ip_enabled', action='store_false', help=_('Disable floating IPs for cluster nodes.')) parser.add_argument( '--hidden', dest='hidden', action='store_true', help=_('Hide the cluster template.')) parser.add_argument( '--visible', dest='hidden', action='store_false', help=_('Make the cluster template visible.')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra patch = [] str_fields = [ ('name', '/name'), ('image', '/image_id'), ('flavor', '/flavor_id'), ('master_flavor', '/master_flavor_id'), ('keypair', '/keypair_id'), ('external_network', '/external_network_id'), ('fixed_network', '/fixed_network'), ('fixed_subnet', '/fixed_subnet'), ('network_driver', '/network_driver'), ('volume_driver', '/volume_driver'), ('driver', '/driver'), ('dns_nameserver', '/dns_nameserver'), ('docker_storage_driver', '/docker_storage_driver'), ('http_proxy', '/http_proxy'), ('https_proxy', '/https_proxy'), ('no_proxy', '/no_proxy'), ('insecure_registry', '/insecure_registry'), ] for attr, path in str_fields: value = getattr(parsed_args, attr, None) if value is not None: patch.append({'op': 'replace', 'path': path, 'value': value}) if parsed_args.docker_volume_size is not None: patch.append({'op': 'replace', 'path': '/docker_volume_size', 'value': parsed_args.docker_volume_size}) for boolean_attr in ('hidden', 'public', 'tls_disabled', 'registry_enabled', 'master_lb_enabled', 'floating_ip_enabled'): value = getattr(parsed_args, boolean_attr) if value is not None: patch.append({'op': 'replace', 'path': '/%s' % boolean_attr, 'value': value}) for label in parsed_args.labels: k, v = label.split('=', 1) patch.append({'op': 'add', 'path': '/labels/%s' % k, 'value': v}) for key in parsed_args.no_labels: patch.append({'op': 'remove', 'path': '/labels/%s' % key}) if parsed_args.tags: patch.append({'op': 'replace', 'path': '/tags', 'value': ','.join(parsed_args.tags)}) if not patch: raise InvalidAttribute('Nothing to update.') name = getattr(parsed_args, 'cluster-template', None) ct = mag_client.cluster_templates.update(name, patch) return _show_cluster_template(ct) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1034193 python_magnumclient-5.0.0/magnumclient/osc/v1/clusters.py0000664000175000017500000005147515244246676022471 0ustar00zuulzuul# Copyright 2016 EasyStack. 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 magnumclient.common import utils as magnum_utils from magnumclient import exceptions from magnumclient.i18n import _ from magnumclient.v1.clusters import CLUSTER_ATTRIBUTES # noqa: F401 from osc_lib.command import command from osc_lib import utils class CreateCluster(command.Command): _description = _("Create a cluster") def get_parser(self, prog_name): parser = super(CreateCluster, self).get_parser(prog_name) # NOTE: All arguments are positional and, if not provided # with a default, required. parser.add_argument('--cluster-template', dest='cluster_template', required=True, metavar='', help='ID or name of the cluster template.') parser.add_argument('--discovery-url', dest='discovery_url', metavar='', help=('Specifies custom delivery url for ' 'node discovery.')) parser.add_argument('--docker-volume-size', dest='docker_volume_size', type=int, metavar='', help=('The size in GB for the docker volume to ' 'use.')) parser.add_argument('--labels', metavar='', action='append', help=_('Arbitrary labels in the form of key=value ' 'pairs to associate with a cluster ' 'template. May be used multiple times.')) parser.add_argument('--keypair', default=None, metavar='', help='UUID or name of the keypair to use.') parser.add_argument('--master-count', dest='master_count', type=int, default=1, metavar='', help='The number of master nodes for the cluster.') parser.add_argument('name', metavar='', help='Name of the cluster to create.') parser.add_argument('--node-count', dest='node_count', type=int, default=1, metavar='', help='The cluster node count.') parser.add_argument('--timeout', type=int, default=60, metavar='', help=('The timeout for cluster creation time. The ' 'default is 60 minutes.')) parser.add_argument( '--master-flavor', dest='master_flavor', metavar='', help=_('The nova flavor name or UUID to use when launching the ' 'master node of the Cluster.')) parser.add_argument( '--flavor', metavar='', help=_('The nova flavor name or UUID to use when launching the ' 'Cluster.')) parser.add_argument( '--fixed-network', dest='fixed_network', metavar='', help=_('The private Neutron network name to connect to this ' 'Cluster template.')) parser.add_argument( '--fixed-subnet', dest='fixed_subnet', metavar='', help=_('The private Neutron subnet name to connect to Cluster.')) parser.add_argument( '--floating-ip-enabled', dest='floating_ip_enabled', default=[], action='append_const', const=True, help=_('Indicates whether created Clusters should have a ' 'floating ip.')) parser.add_argument( '--floating-ip-disabled', dest='floating_ip_enabled', action='append_const', const=False, help=_('Disables floating ip creation on the new Cluster')) parser.add_argument( '--merge-labels', dest='merge_labels', action='store_true', default=False, help=_('The labels provided will be merged with the labels ' 'configured in the specified cluster template.')) parser.add_argument( '--master-lb-enabled', dest='master_lb_enabled', action='append_const', default=[], const=True, help=_('Enable master LB creation on the new cluster')) parser.add_argument( '--master-lb-disabled', dest='master_lb_enabled', action='append_const', const=False, help=_('Disable master LB creation on the new cluster')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra args = { 'cluster_template_id': parsed_args.cluster_template, 'create_timeout': parsed_args.timeout, 'discovery_url': parsed_args.discovery_url, 'keypair': parsed_args.keypair, 'master_count': parsed_args.master_count, 'name': parsed_args.name, 'node_count': parsed_args.node_count, } if len(parsed_args.floating_ip_enabled) > 1: raise exceptions.InvalidAttribute( '--floating-ip-enabled and ' '--floating-ip-disabled are ' 'mutually exclusive and ' 'should be specified only once.') elif len(parsed_args.floating_ip_enabled) == 1: args['floating_ip_enabled'] = parsed_args.floating_ip_enabled[0] if parsed_args.labels is not None: args['labels'] = magnum_utils.handle_labels(parsed_args.labels) if parsed_args.docker_volume_size is not None: args['docker_volume_size'] = parsed_args.docker_volume_size if parsed_args.master_flavor is not None: args['master_flavor_id'] = parsed_args.master_flavor if parsed_args.flavor is not None: args['flavor_id'] = parsed_args.flavor if parsed_args.fixed_network is not None: args["fixed_network"] = parsed_args.fixed_network if parsed_args.fixed_subnet is not None: args["fixed_subnet"] = parsed_args.fixed_subnet if parsed_args.merge_labels: # We are only sending this if it's True. This # way we avoid breaking older APIs. args["merge_labels"] = parsed_args.merge_labels if parsed_args.master_lb_enabled: args["master_lb_enabled"] = parsed_args.master_lb_enabled if len(parsed_args.master_lb_enabled) > 1: raise exceptions.InvalidAttribute( '--master-lb-enabled and ' '--master-lb-disabled are ' 'mutually exclusive and ' 'should be specified only once.') elif len(parsed_args.master_lb_enabled) == 1: args['master_lb_enabled'] = parsed_args.master_lb_enabled[0] if (not args['master_lb_enabled'] and parsed_args.master_count > 1): raise exceptions.InvalidAttribute( 'Master node count can only be one if master ' 'loadbalancer is disabled.') cluster = mag_client.clusters.create(**args) print("Request to create cluster %s accepted" % cluster.uuid) class DeleteCluster(command.Command): _description = _("Delete a cluster") def get_parser(self, prog_name): parser = super(DeleteCluster, self).get_parser(prog_name) parser.add_argument( 'cluster', nargs='+', metavar='', help='ID or name of the cluster(s) to delete.') return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra for cluster in parsed_args.cluster: mag_client.clusters.delete(cluster) print("Request to delete cluster %s has been accepted." % cluster) class ListCluster(command.Lister): _description = _("List clusters") def get_parser(self, prog_name): parser = super(ListCluster, self).get_parser(prog_name) parser.add_argument( '--limit', metavar='', type=int, help=_('Maximum number of clusters to return')) parser.add_argument( '--sort-key', metavar='', help=_('Column to sort results by')) parser.add_argument( '--sort-dir', metavar='', choices=['desc', 'asc'], help=_('Direction to sort. "asc" or "desc".')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra columns = [ 'uuid', 'name', 'keypair', 'node_count', 'master_count', 'status', 'health_status'] clusters = mag_client.clusters.list(limit=parsed_args.limit, sort_key=parsed_args.sort_key, sort_dir=parsed_args.sort_dir) return ( columns, (utils.get_item_properties(c, columns) for c in clusters) ) class ShowCluster(command.ShowOne): _description = _("Show a Cluster") def get_parser(self, prog_name): parser = super(ShowCluster, self).get_parser(prog_name) parser.add_argument( 'cluster', metavar='', help=_('ID or name of the cluster to show.') ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) columns = CLUSTER_ATTRIBUTES mag_client = self.app.client_manager.container_infra cluster = mag_client.clusters.get(parsed_args.cluster) return (columns, utils.get_item_properties(cluster, columns)) class UpdateCluster(command.Command): _description = _("Update a Cluster") def get_parser(self, prog_name): parser = super(UpdateCluster, self).get_parser(prog_name) parser.add_argument( 'cluster', metavar='', help=_('The name or UUID of the cluster to update')) parser.add_argument( '--name', metavar='', help=_('New name for the cluster.')) parser.add_argument( '--node-count', metavar='', type=int, dest='node_count', help=_('New number of nodes in the cluster.')) parser.add_argument( '--master-count', metavar='', type=int, dest='master_count', help=_('New number of master nodes in the cluster.')) parser.add_argument( '--label', metavar='', action='append', dest='labels', default=[], help=_('Label to add or update on the cluster ' '(repeat to set multiple labels).')) parser.add_argument( '--no-label', metavar='', action='append', dest='no_labels', default=[], help=_('Label key to remove from the cluster ' '(repeat to remove multiple labels).')) parser.add_argument( '--rollback', action='store_true', dest='rollback', default=False, help=_('Rollback cluster on update failure.')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra patch = [] if parsed_args.name: patch.append({'op': 'replace', 'path': '/name', 'value': parsed_args.name}) if parsed_args.node_count is not None: patch.append({'op': 'replace', 'path': '/node_count', 'value': parsed_args.node_count}) if parsed_args.master_count is not None: patch.append({'op': 'replace', 'path': '/master_count', 'value': parsed_args.master_count}) for label in parsed_args.labels: k, v = label.split('=', 1) patch.append({'op': 'add', 'path': '/labels/%s' % k, 'value': v}) for key in parsed_args.no_labels: patch.append({'op': 'remove', 'path': '/labels/%s' % key}) if not patch: raise exceptions.CommandError("Nothing to update.") mag_client.clusters.update(parsed_args.cluster, patch, rollback=parsed_args.rollback) print("Request to update cluster %s has been accepted." % parsed_args.cluster) class ConfigCluster(command.Command): _description = _("Get Configuration for a Cluster") def get_parser(self, prog_name): parser = super(ConfigCluster, self).get_parser(prog_name) parser.add_argument( 'cluster', metavar='', help=_('The name or UUID of cluster to update')) parser.add_argument( '--dir', metavar='', default='.', help=_('Directory to save the certificate and config files.')) parser.add_argument( '--force', action='store_true', dest='force', default=False, help=_('Overwrite files if existing.')) parser.add_argument( '--output-certs', action='store_true', dest='output_certs', default=False, help=_('Output certificates in separate files.')) parser.add_argument( '--use-certificate', action='store_true', dest='use_certificate', default=True, help=_('Use certificate in config files.')) parser.add_argument( '--use-keystone', action='store_true', dest='use_keystone', default=False, help=_('Use Keystone token in config files.')) return parser def take_action(self, parsed_args): """Configure native client to access cluster. You can source the output of this command to get the native client of the corresponding COE configured to access the cluster. """ if parsed_args.use_keystone: parsed_args.use_certificate = False if not parsed_args.use_certificate: parsed_args.use_keystone = True self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra parsed_args.dir = os.path.abspath(parsed_args.dir) cluster = mag_client.clusters.get(parsed_args.cluster) if cluster.api_address is None: self.log.warning("WARNING: The cluster's api_address is" " not known yet.") cluster_template = mag_client.cluster_templates.get( cluster.cluster_template_id) tls = self._fetch_tls( mag_client=mag_client, cluster_uuid=cluster.uuid, tls_required=(not cluster_template.tls_disabled), certkey_required=(not parsed_args.use_keystone) ) if parsed_args.output_certs: self._write_certs( certs=tls, path=parsed_args.dir, force=parsed_args.force ) print(magnum_utils.config_cluster( cluster, cluster_template, parsed_args.dir, force=parsed_args.force, certs=tls, use_keystone=parsed_args.use_keystone)) def _fetch_tls( self, mag_client, cluster_uuid, tls_required, certkey_required=True, ): if not tls_required: return {} opts = { 'cluster_uuid': cluster_uuid, } tls = { 'ca': mag_client.certificates.get(**opts).pem } if certkey_required: csr = magnum_utils.generate_csr_and_key() opts['csr'] = csr['csr'] tls['cert'] = mag_client.certificates.create(**opts).pem tls['key'] = csr['key'] return tls def _write_certs(self, certs, path, force): for k in ('key', 'cert', 'ca'): fname = "%s/%s.pem" % (path, k) if os.path.exists(fname): if not force: raise Exception("File %s exists, aborting." % fname) os.remove(fname) if k not in certs: # key and cert aren't always generated continue with open(fname, "w") as f: f.write(certs[k]) class ResizeCluster(command.Command): _description = _("Resize a Cluster") def get_parser(self, prog_name): parser = super(ResizeCluster, self).get_parser(prog_name) parser.add_argument( 'cluster', metavar='', help=_('The name or UUID of cluster to update')) parser.add_argument( 'node_count', type=int, help=_("Desired node count of the cluser.")) parser.add_argument( '--nodes-to-remove', metavar='', action='append', help=_('Server ID of the nodes to be removed. Repeat to add ' 'more server ID')) parser.add_argument( '--nodegroup', metavar='', help=_('The name or UUID of the nodegroup of current cluster.')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra cluster = mag_client.clusters.get(parsed_args.cluster) mag_client.clusters.resize(cluster.uuid, parsed_args.node_count, parsed_args.nodes_to_remove, parsed_args.nodegroup) print("Request to resize cluster %s has been accepted." % parsed_args.cluster) class UpgradeCluster(command.Command): _description = _("Upgrade a Cluster") def get_parser(self, prog_name): parser = super(UpgradeCluster, self).get_parser(prog_name) parser.add_argument( 'cluster', metavar='', help=_('The name or UUID of cluster to update')) parser.add_argument( 'cluster_template', help=_("The new cluster template ID will be upgraded to.")) parser.add_argument( '--max-batch-size', metavar='', type=int, default=1, help=_("The max batch size for upgrading each time.")) parser.add_argument( '--nodegroup', metavar='', help=_('The name or UUID of the nodegroup of current cluster.')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra cluster = mag_client.clusters.get(parsed_args.cluster) mag_client.clusters.upgrade(cluster.uuid, parsed_args.cluster_template, parsed_args.max_batch_size, parsed_args.nodegroup) print("Request to upgrade cluster %s has been accepted." % parsed_args.cluster) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1294193 python_magnumclient-5.0.0/magnumclient/osc/v1/credentials.py0000664000175000017500000000273415244246676023114 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from osc_lib.command import command from osc_lib.i18n import _ class RotateCredential(command.Command): _description = _("Rotate the credentials for the cluster using the " "current user account.") def get_parser(self, prog_name): parser = super(RotateCredential, self).get_parser(prog_name) parser.add_argument('cluster', metavar='', help='ID or name of the cluster') return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra try: cluster = mag_client.credentials.update(parsed_args.cluster) except Exception as e: print("Credential rotation failed: %s" % e) else: print("Request to rotate credentials for cluster %s accepted." % cluster.uuid) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1294193 python_magnumclient-5.0.0/magnumclient/osc/v1/mservices.py0000664000175000017500000000257115244246676022616 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import logging from osc_lib.command import command from osc_lib import utils def _get_client(obj, parsed_args): obj.log.debug("take_action(%s)" % parsed_args) return obj.app.client_manager.container_infra class ListService(command.Lister): """Print a list of Magnum services.""" log = logging.getLogger(__name__ + ".ListService") def get_parser(self, prog_name): parser = super(ListService, self).get_parser(prog_name) return parser def take_action(self, parsed_args): client = _get_client(self, parsed_args) services = client.mservices.list() columns = ('id', 'host', 'binary', 'state', 'disabled', 'disabled_reason', 'created_at', 'updated_at') return (columns, (utils.get_item_properties(service, columns) for service in services)) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1294193 python_magnumclient-5.0.0/magnumclient/osc/v1/nodegroups.py0000664000175000017500000003303315244246676023000 0ustar00zuulzuul# Copyright (c) 2018 European Organization for Nuclear Research. # 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 magnumclient.common import utils as magnum_utils from magnumclient import exceptions from magnumclient.i18n import _ from magnumclient.v1.nodegroups import NODEGROUP_ATTRIBUTES # noqa: F401 from osc_lib.command import command from osc_lib import utils class CreateNodeGroup(command.Command): _description = _("Create a nodegroup") def get_parser(self, prog_name): parser = super(CreateNodeGroup, self).get_parser(prog_name) # NOTE: All arguments are positional and, if not provided # with a default, required. parser.add_argument('--docker-volume-size', dest='docker_volume_size', type=int, metavar='', help=('The size in GB for the docker volume to ' 'use.')) parser.add_argument('--labels', metavar='', action='append', help=_('Arbitrary labels in the form of key=value' 'pairs to associate with a nodegroup. ' 'May be used multiple times.')) parser.add_argument('cluster', metavar='', help='Name of the nodegroup to create.') parser.add_argument('name', metavar='', help='Name of the nodegroup to create.') parser.add_argument('--node-count', dest='node_count', type=int, default=1, metavar='', help='The nodegroup node count.') parser.add_argument('--min-nodes', dest='min_node_count', type=int, default=0, metavar='', help='The nodegroup minimum node count.') parser.add_argument('--max-nodes', dest='max_node_count', type=int, default=None, metavar='', help='The nodegroup maximum node count.') parser.add_argument('--role', dest='role', type=str, default='worker', metavar='', help=('The role of the nodegroup')) parser.add_argument( '--image', metavar='', help=_('The name or UUID of the base image to customize for the ' 'NodeGroup.')) parser.add_argument( '--flavor', metavar='', help=_('The nova flavor name or UUID to use when launching the ' 'nodes in this NodeGroup.')) parser.add_argument( '--merge-labels', dest='merge_labels', action='store_true', default=False, help=_('The labels provided will be merged with the labels ' 'configured in the specified cluster.')) parser.add_argument( '--node-labels', dest='node_labels', metavar='', action='append', help=_('Kubernetes node labels in the form of key=value pairs to ' 'apply to every node in this nodegroup. May be used ' 'multiple times.')) parser.add_argument( '--node-taints', dest='node_taints', metavar='', action='append', help=_('Kubernetes node taints to apply to every node in this ' 'nodegroup, as KEY[=VALUE]:EFFECT (VALUE is optional). ' 'EFFECT must be one of NoSchedule, PreferNoSchedule or ' 'NoExecute. May be used multiple times.')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra args = { 'name': parsed_args.name, 'node_count': parsed_args.node_count, 'max_node_count': parsed_args.max_node_count, 'min_node_count': parsed_args.min_node_count, 'role': parsed_args.role, } if parsed_args.labels is not None: args['labels'] = magnum_utils.handle_labels(parsed_args.labels) if parsed_args.docker_volume_size is not None: args['docker_volume_size'] = parsed_args.docker_volume_size if parsed_args.flavor is not None: args['flavor_id'] = parsed_args.flavor if parsed_args.image is not None: args['image_id'] = parsed_args.image if parsed_args.merge_labels: # We are only sending this if it's True. This # way we avoid breaking older APIs. args["merge_labels"] = parsed_args.merge_labels if parsed_args.node_labels is not None: args['node_labels'] = magnum_utils.handle_labels( parsed_args.node_labels) if parsed_args.node_taints is not None: args['node_taints'] = magnum_utils.handle_taints( parsed_args.node_taints) cluster_id = parsed_args.cluster nodegroup = mag_client.nodegroups.create(cluster_id, **args) print("Request to create nodegroup %s accepted" % nodegroup.uuid) class DeleteNodeGroup(command.Command): _description = _("Delete a nodegroup") def get_parser(self, prog_name): parser = super(DeleteNodeGroup, self).get_parser(prog_name) parser.add_argument( 'cluster', metavar='', help=_('ID or name of the cluster where the nodegroup(s) ' 'belong(s).')) parser.add_argument( 'nodegroup', nargs='+', metavar='', help='ID or name of the nodegroup(s) to delete.') return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra cluster_id = parsed_args.cluster for ng in parsed_args.nodegroup: mag_client.nodegroups.delete(cluster_id, ng) print("Request to delete nodegroup %s has been accepted." % ng) class ListNodeGroup(command.Lister): _description = _("List nodegroups") def get_parser(self, prog_name): parser = super(ListNodeGroup, self).get_parser(prog_name) parser.add_argument( 'cluster', metavar='', help=_('ID or name of the cluster where the nodegroup belongs.')) parser.add_argument( '--limit', metavar='', type=int, help=_('Maximum number of nodegroups to return')) parser.add_argument( '--sort-key', metavar='', help=_('Column to sort results by')) parser.add_argument( '--sort-dir', metavar='', choices=['desc', 'asc'], help=_('Direction to sort. "asc" or "desc".')) parser.add_argument( '--role', metavar='', help=_('List the nodegroups in the cluster with this role')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra columns = ['uuid', 'name', 'flavor_id', 'image_id', 'node_count', 'status', 'role'] cluster_id = parsed_args.cluster nodegroups = mag_client.nodegroups.list(cluster_id, limit=parsed_args.limit, sort_key=parsed_args.sort_key, sort_dir=parsed_args.sort_dir, role=parsed_args.role) return ( columns, (utils.get_item_properties(n, columns) for n in nodegroups) ) class ShowNodeGroup(command.ShowOne): _description = _("Show a nodegroup") def get_parser(self, prog_name): parser = super(ShowNodeGroup, self).get_parser(prog_name) parser.add_argument( 'cluster', metavar='', help=_('ID or name of the cluster where the nodegroup belongs.')) parser.add_argument( 'nodegroup', metavar='', help=_('ID or name of the nodegroup to show.') ) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) columns = NODEGROUP_ATTRIBUTES mag_client = self.app.client_manager.container_infra cluster_id = parsed_args.cluster nodegroup = mag_client.nodegroups.get(cluster_id, parsed_args.nodegroup) return (columns, utils.get_item_properties(nodegroup, columns)) class UpdateNodeGroup(command.Command): _description = _("Update a Nodegroup") def get_parser(self, prog_name): parser = super(UpdateNodeGroup, self).get_parser(prog_name) parser.add_argument( 'cluster', metavar='', help=_('ID or name of the cluster where the nodegroup belongs.')) parser.add_argument( 'nodegroup', metavar='', help=_('The name or UUID of the nodegroup to update.')) parser.add_argument( '--min-nodes', metavar='', type=int, dest='min_node_count', help=_('The minimum node count for the nodegroup.')) parser.add_argument( '--max-nodes', metavar='', type=int, dest='max_node_count', help=_('The maximum node count for the nodegroup.')) parser.add_argument( '--node-labels', dest='node_labels', metavar='', action='append', help=_('Kubernetes node labels in the form of key=value pairs to ' 'apply to every node in this nodegroup. Replaces any ' 'existing node labels. May be used multiple times.')) parser.add_argument( '--node-taints', dest='node_taints', metavar='', action='append', help=_('Kubernetes node taints to apply to every node in this ' 'nodegroup, as KEY[=VALUE]:EFFECT (VALUE is optional). ' 'EFFECT must be one of NoSchedule, PreferNoSchedule or ' 'NoExecute. Replaces any existing node taints. May be ' 'used multiple times.')) return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra patch = [] if parsed_args.min_node_count is not None: patch.append({'op': 'replace', 'path': '/min_node_count', 'value': parsed_args.min_node_count}) if parsed_args.max_node_count is not None: patch.append({'op': 'replace', 'path': '/max_node_count', 'value': parsed_args.max_node_count}) # The Magnum API's JSON patch value field only accepts text or int, # so dict/list values are sent as their string representation and # deserialized server-side (see magnum.api.utils.apply_jsonpatch). if parsed_args.node_labels is not None: # An empty value (e.g. --node-labels '') clears the field. labels = [x for x in parsed_args.node_labels if x] patch.append({ 'op': 'replace', 'path': '/node_labels', 'value': str(magnum_utils.handle_labels(labels)), }) if parsed_args.node_taints is not None: taints = [x for x in parsed_args.node_taints if x] patch.append({ 'op': 'replace', 'path': '/node_taints', 'value': str(magnum_utils.handle_taints(taints)), }) if not patch: raise exceptions.CommandError("Nothing to update.") mag_client.nodegroups.update(parsed_args.cluster, parsed_args.nodegroup, patch) print("Request to update nodegroup %s has been accepted." % parsed_args.nodegroup) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1294193 python_magnumclient-5.0.0/magnumclient/osc/v1/quotas.py0000664000175000017500000002053315244246676022130 0ustar00zuulzuul# Copyright 2015 NEC Corporation. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from magnumclient.common import cliutils as utils from magnumclient.common import utils as magnum_utils from magnumclient.i18n import _ from magnumclient.v1.quotas import QUOTA_ATTRIBUTES # noqa: F401 from osc_lib.command import command def _show_quota(quota): utils.print_dict(quota._info) class CreateQuotas(command.Command): _description = _("Create a quota.") def get_parser(self, prog_name): parser = super(CreateQuotas, self).get_parser(prog_name) parser.add_argument('--project-id', required=True, metavar='', help='Project ID') parser.add_argument('--resource', required=True, metavar='', help='Resource name.') parser.add_argument('--hard-limit', metavar='', type=int, default=1, help='Max resource limit (default: hard-limit=1)') return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra opts = { 'project_id': parsed_args.project_id, 'resource': parsed_args.resource, 'hard_limit': parsed_args.hard_limit } try: quota = mag_client.quotas.create(**opts) _show_quota(quota) except Exception as e: print("Create quota for project_id %(id)s resource %(res)s failed:" " %(e)s" % {'id': parsed_args.project_id, 'res': parsed_args.resource, 'e': e}) class DeleteQuotas(command.Command): _description = _("Delete specified resource quota.") def get_parser(self, prog_name): parser = super(DeleteQuotas, self).get_parser(prog_name) parser.add_argument('--project-id', required=True, metavar='', help='Project ID') parser.add_argument('--resource', required=True, metavar='', help='Resource name.') return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra try: mag_client.quotas.delete(parsed_args.project_id, parsed_args.resource) print("Request to delete quota for project id %(id)s and resource " "%(res)s has been accepted." % {'id': parsed_args.project_id, 'res': parsed_args.resource}) except Exception as e: print("Quota delete failed for project id %(id)s and resource " "%(res)s :%(e)s" % {'id': parsed_args.project_id, 'res': parsed_args.resource, 'e': e}) class ShowQuotas(command.Command): _description = _("Show details about the given project resource quota.") def get_parser(self, prog_name): parser = super(ShowQuotas, self).get_parser(prog_name) parser.add_argument('--project-id', required=True, metavar='', help='Project ID') parser.add_argument('--resource', required=True, metavar='', help='Resource name.') return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra project_id = parsed_args.project_id resource = parsed_args.resource quota = mag_client.quotas.get(project_id, resource) _show_quota(quota) class UpdateQuotas(command.Command): _description = _("Update information about the given " "project resource quota.") def get_parser(self, prog_name): parser = super(UpdateQuotas, self).get_parser(prog_name) parser.add_argument('--project-id', required=True, metavar='', help='Project ID') parser.add_argument('--resource', required=True, metavar='', help='Resource name.') parser.add_argument('--hard-limit', metavar='', type=int, default=1, help='Max resource limit (default: hard-limit=1)') return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra opts = { 'project_id': parsed_args.project_id, 'resource': parsed_args.resource, 'hard_limit': parsed_args.hard_limit } try: quota = mag_client.quotas.update(parsed_args.project_id, parsed_args.resource, opts) _show_quota(quota) except Exception as e: print("Update quota for project_id %(id)s resource %(res)s failed:" " %(e)s" % {'id': parsed_args.project_id, 'res': parsed_args.resource, 'e': e}) class ListQuotas(command.Command): _description = _("Print a list of available quotas.") def get_parser(self, prog_name): parser = super(ListQuotas, self).get_parser(prog_name) parser.add_argument('--marker', metavar='', default=None, help=_('The last quota UUID of the previous page; ' 'displays list of quotas after "marker".')) parser.add_argument('--limit', metavar='', type=int, help='Maximum number of quotas to return.') parser.add_argument('--sort-key', metavar='', help='Column to sort results by.') parser.add_argument('--sort-dir', metavar='', choices=['desc', 'asc'], help='Direction to sort. "asc" or "desc".') parser.add_argument('--all-tenants', action='store_true', default=False, help='Flag to indicate list all tenant quotas.') return parser def take_action(self, parsed_args): self.log.debug("take_action(%s)", parsed_args) mag_client = self.app.client_manager.container_infra quotas = mag_client.quotas.list(marker=parsed_args.marker, limit=parsed_args.limit, sort_key=parsed_args.sort_key, sort_dir=parsed_args.sort_dir, all_tenants=parsed_args.all_tenants) columns = ['project_id', 'resource', 'hard_limit'] utils.print_list(quotas, columns, {'versions': magnum_utils.print_list_field('versions')}, sortby_index=None) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1294193 python_magnumclient-5.0.0/magnumclient/osc/v1/stats.py0000664000175000017500000000264115244246676021752 0ustar00zuulzuul# Copyright 2015 NEC Corporation. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from magnumclient.common import cliutils as utils from magnumclient.i18n import _ from osc_lib.command import command class ListStats(command.Command): _description = _("Show stats for the given project_id") def get_parser(self, prog_name): parser = super(ListStats, self).get_parser(prog_name) parser.add_argument('project_id', metavar='', help='Project ID') return parser def take_action(self, parsed_args): mag_client = self.app.client_manager.container_infra opts = { 'project_id': parsed_args.project_id } stats = mag_client.stats.list(**opts) try: utils.print_dict(stats._info) except AttributeError: return None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9985783 python_magnumclient-5.0.0/magnumclient/tests/0000775000175000017500000000000015244246764020265 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1294193 python_magnumclient-5.0.0/magnumclient/tests/__init__.py0000664000175000017500000000000015244246676022366 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1294193 python_magnumclient-5.0.0/magnumclient/tests/base.py0000664000175000017500000000363615244246676021563 0ustar00zuulzuul# -*- coding: utf-8 -*- # Copyright 2010-2011 OpenStack Foundation # 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 os import fixtures import testtools _TRUE_VALUES = ('true', '1', 'yes') class TestCase(testtools.TestCase): """Test case base class for all unit tests.""" def setUp(self): """Run before each test method to initialize test environment.""" super(TestCase, self).setUp() test_timeout = os.environ.get('OS_TEST_TIMEOUT', 60) try: test_timeout = int(test_timeout) except ValueError: # If timeout value is invalid, set a default timeout. test_timeout = 60 if test_timeout <= 0: test_timeout = 60 self.useFixture(fixtures.Timeout(test_timeout, gentle=True)) self.useFixture(fixtures.NestedTempfile()) self.useFixture(fixtures.TempHomeDir()) if os.environ.get('OS_STDOUT_CAPTURE') in _TRUE_VALUES: stdout = self.useFixture(fixtures.StringStream('stdout')).stream self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) if os.environ.get('OS_STDERR_CAPTURE') in _TRUE_VALUES: stderr = self.useFixture(fixtures.StringStream('stderr')).stream self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) self.log_fixture = self.useFixture(fixtures.FakeLogger()) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9985783 python_magnumclient-5.0.0/magnumclient/tests/osc/0000775000175000017500000000000015244246764021051 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1294193 python_magnumclient-5.0.0/magnumclient/tests/osc/__init__.py0000664000175000017500000000000015244246676023152 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9995782 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/0000775000175000017500000000000015244246764022030 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1304193 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/__init__.py0000664000175000017500000000000015244246676024131 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1304193 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/osc_fakes.py0000664000175000017500000001525315244246676024347 0ustar00zuulzuul# Copyright 2013 Nebula Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from unittest import mock from oslo_serialization import jsonutils import sys from keystoneauth1 import fixture import requests AUTH_TOKEN = "foobar" AUTH_URL = "http://0.0.0.0" USERNAME = "itchy" PASSWORD = "scratchy" PROJECT_NAME = "poochie" REGION_NAME = "richie" INTERFACE = "catchy" VERSION = "3" TEST_RESPONSE_DICT = fixture.V2Token(token_id=AUTH_TOKEN, user_name=USERNAME) _s = TEST_RESPONSE_DICT.add_service('identity', name='keystone') _s.add_endpoint(AUTH_URL + ':5000/v2.0') _s = TEST_RESPONSE_DICT.add_service('network', name='neutron') _s.add_endpoint(AUTH_URL + ':9696') _s = TEST_RESPONSE_DICT.add_service('compute', name='nova') _s.add_endpoint(AUTH_URL + ':8774/v2.1') _s = TEST_RESPONSE_DICT.add_service('image', name='glance') _s.add_endpoint(AUTH_URL + ':9292') _s = TEST_RESPONSE_DICT.add_service('object', name='swift') _s.add_endpoint(AUTH_URL + ':8080/v1') TEST_RESPONSE_DICT_V3 = fixture.V3Token(user_name=USERNAME) TEST_RESPONSE_DICT_V3.set_project_scope() TEST_VERSIONS = fixture.DiscoveryList(href=AUTH_URL) def to_unicode_dict(catalog_dict): """Converts dict to unicode dict """ if isinstance(catalog_dict, dict): return {to_unicode_dict(key): to_unicode_dict(value) for key, value in catalog_dict.items()} elif isinstance(catalog_dict, list): return [to_unicode_dict(element) for element in catalog_dict] elif isinstance(catalog_dict, str): return catalog_dict + u"" else: return catalog_dict class FakeStdout(object): def __init__(self): self.content = [] def write(self, text): self.content.append(text) def make_string(self): result = '' for line in self.content: result = result + line return result class FakeLog(object): def __init__(self): self.messages = {} def debug(self, msg): self.messages['debug'] = msg def info(self, msg): self.messages['info'] = msg def warning(self, msg): self.messages['warning'] = msg def error(self, msg): self.messages['error'] = msg def critical(self, msg): self.messages['critical'] = msg class FakeApp(object): def __init__(self, _stdout, _log): self.stdout = _stdout self.client_manager = None self.stdin = sys.stdin self.stdout = _stdout or sys.stdout self.stderr = sys.stderr self.log = _log class FakeOptions(object): def __init__(self, **kwargs): self.os_beta_command = False class FakeClient(object): def __init__(self, **kwargs): self.endpoint = kwargs['endpoint'] self.token = kwargs['token'] class FakeClientManager(object): _api_version = { 'image': '2', } def __init__(self): self.compute = None self.identity = None self.image = None self.object_store = None self.volume = None self.network = None self.session = None self.auth_ref = None self.auth_plugin_name = None self.network_endpoint_enabled = True def get_configuration(self): return { 'auth': { 'username': USERNAME, 'password': PASSWORD, 'token': AUTH_TOKEN, }, 'region': REGION_NAME, 'identity_api_version': VERSION, } def is_network_endpoint_enabled(self): return self.network_endpoint_enabled class FakeModule(object): def __init__(self, name, version): self.name = name self.__version__ = version # Workaround for openstacksdk case self.version = mock.Mock() self.version.__version__ = version class FakeResource(object): def __init__(self, manager=None, info=None, loaded=False, methods=None): """Set attributes and methods for a resource. :param manager: The resource manager :param Dictionary info: A dictionary with all attributes :param bool loaded: True if the resource is loaded in memory :param Dictionary methods: A dictionary with all methods """ info = info or {} methods = methods or {} self.__name__ = type(self).__name__ self.manager = manager self._info = info self._add_details(info) self._add_methods(methods) self._loaded = loaded def _add_details(self, info): for (k, v) in info.items(): setattr(self, k, v) def _add_methods(self, methods): """Fake methods with MagicMock objects. For each <@key, @value> pairs in methods, add an callable MagicMock object named @key as an attribute, and set the mock's return_value to @value. When users access the attribute with (), @value will be returned, which looks like a function call. """ for (name, ret) in methods.items(): method = mock.Mock(return_value=ret) setattr(self, name, method) def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) def keys(self): return self._info.keys() def to_dict(self): return self._info @property def info(self): return self._info def __getitem__(self, item): return self._info.get(item) def get(self, item, default=None): return self._info.get(item, default) class FakeResponse(requests.Response): def __init__(self, headers=None, status_code=200, data=None, encoding=None): super(FakeResponse, self).__init__() headers = headers or {} self.status_code = status_code self.headers.update(headers) self._content = jsonutils.dump_as_bytes(data) class FakeModel(dict): def __getattr__(self, key): try: return self[key] except KeyError: raise AttributeError(key) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1304193 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/osc_utils.py0000664000175000017500000000477315244246676024423 0ustar00zuulzuul# Copyright 2012-2013 OpenStack Foundation # Copyright 2013 Nebula Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import fixtures import os import testtools from openstackclient.tests.unit import fakes class ParserException(Exception): pass class TestCase(testtools.TestCase): def setUp(self): testtools.TestCase.setUp(self) if (os.environ.get("OS_STDOUT_CAPTURE") == "True" or os.environ.get("OS_STDOUT_CAPTURE") == "1"): stdout = self.useFixture(fixtures.StringStream("stdout")).stream self.useFixture(fixtures.MonkeyPatch("sys.stdout", stdout)) if (os.environ.get("OS_STDERR_CAPTURE") == "True" or os.environ.get("OS_STDERR_CAPTURE") == "1"): stderr = self.useFixture(fixtures.StringStream("stderr")).stream self.useFixture(fixtures.MonkeyPatch("sys.stderr", stderr)) def assertNotCalled(self, m, msg=None): """Assert a function was not called""" if m.called: if not msg: msg = 'method %s should not have been called' % m self.fail(msg) class TestCommand(TestCase): """Test cliff command classes""" def setUp(self): super(TestCommand, self).setUp() # Build up a fake app self.fake_stdout = fakes.FakeStdout() self.fake_log = fakes.FakeLog() self.app = fakes.FakeApp(self.fake_stdout, self.fake_log) self.app.client_manager = fakes.FakeClientManager() self.app.options = fakes.FakeOptions() def check_parser(self, cmd, args, verify_args): cmd_parser = cmd.get_parser('check_parser') try: parsed_args = cmd_parser.parse_args(args) except SystemExit: raise ParserException("Argument parse failed") for av in verify_args: attr, value = av if attr: self.assertIn(attr, parsed_args) self.assertEqual(value, getattr(parsed_args, attr)) return parsed_args ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1304193 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/test_plugin.py0000664000175000017500000000402715244246676024744 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock import testtools from magnumclient.osc import plugin class TestMakeClient(testtools.TestCase): def _make_instance(self, api_version): instance = mock.Mock() instance._api_version = {'container_infra': api_version} return instance def _call_make_client(self, api_version): instance = self._make_instance(api_version) with mock.patch('osc_lib.utils.get_client_class') as mock_gcc: mock_client_class = mock.Mock(return_value=mock.Mock()) mock_gcc.return_value = mock_client_class plugin.make_client(instance) return mock_gcc, mock_client_class def test_major_version_uses_latest(self): """Bare major version '1' results in api_version='latest'.""" mock_gcc, mock_client_class = self._call_make_client('1') mock_gcc.assert_called_once_with('container_infra', '1', mock.ANY) _, kwargs = mock_client_class.call_args self.assertEqual('latest', kwargs['api_version']) def test_microversion_forwarded(self): """Full microversion '1.12' is forwarded to the HTTP client.""" mock_gcc, mock_client_class = self._call_make_client('1.12') # Major part must be used for the class lookup mock_gcc.assert_called_once_with('container_infra', '1', mock.ANY) # Full microversion must reach the HTTP client _, kwargs = mock_client_class.call_args self.assertEqual('1.12', kwargs['api_version']) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0025783 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/v1/0000775000175000017500000000000015244246764022356 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1304193 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/v1/__init__.py0000664000175000017500000000000015244246676024457 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1304193 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/v1/fakes.py0000664000175000017500000002505015244246676024025 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 copy import datetime import uuid from magnumclient.tests.osc.unit import osc_fakes from magnumclient.tests.osc.unit import osc_utils class FakeBaseModel(object): def __repr__(self): return "<" + self.__class__.model_name + "%s>" % self._info class FakeBaseModelManager(object): def list(self, limit=None, marker=None, sort_key=None, sort_dir=None, detail=False): pass def get(self, id): pass def create(self, **kwargs): pass def delete(self, id): pass def update(self, id, patch): pass def rotate_ca(self, **kwargs): pass class FakeStatsModelManager(object): def list(self, **kwargs): pass class FakeQuotasModelManager(object): def get(self, id, resource): pass def create(self, **kwargs): pass def delete(self, id): pass class FakeNodeGroupManager(object): def list(self, cluster_id, limit=None, marker=None, sort_key=None, sort_dir=None, detail=False): pass def get(self, cluster_id, id): pass def create(self, cluster_id, **kwargs): pass def delete(self, cluster_id, id): pass def update(self, cluster_id, id, patch): pass class FakeCertificatesModelManager(FakeBaseModelManager): def get(self, cluster_uuid): pass class MagnumFakeContainerInfra(object): def __init__(self): self.cluster_templates = FakeBaseModelManager() self.clusters = FakeBaseModelManager() self.mservices = FakeBaseModelManager() self.certificates = FakeCertificatesModelManager() self.stats = FakeStatsModelManager() self.quotas = FakeQuotasModelManager() self.nodegroups = FakeNodeGroupManager() class MagnumFakeClientManager(osc_fakes.FakeClientManager): def __init__(self): super(MagnumFakeClientManager, self).__init__() self.container_infra = MagnumFakeContainerInfra() class MagnumParseException(Exception): """The base exception class for all exceptions this library raises.""" def __init__(self, message=None, details=None): self.message = message or "Argument parse exception" self.details = details or None def __str__(self): return self.message class TestMagnumClientOSCV1(osc_utils.TestCase): def setUp(self): super(TestMagnumClientOSCV1, self).setUp() self.fake_stdout = osc_fakes.FakeStdout() self.fake_log = osc_fakes.FakeLog() self.app = osc_fakes.FakeApp(self.fake_stdout, self.fake_log) self.namespace = argparse.Namespace() self.app.client_manager = MagnumFakeClientManager() def check_parser(self, cmd, args, verify_args): cmd_parser = cmd.get_parser('check_parser') try: parsed_args = cmd_parser.parse_args(args) except SystemExit: raise MagnumParseException() for av in verify_args: attr, value = av if attr: self.assertIn(attr, parsed_args) self.assertEqual(value, getattr(parsed_args, attr)) return parsed_args class FakeClusterTemplate(object): """Fake one or more ClusterTemplate.""" @staticmethod def create_one_cluster_template(attrs=None): """Create a fake ClusterTemplate. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object, with flavor_id, image_id, and so on """ attrs = attrs or {} # set default attributes. ct_info = { 'links': [], 'insecure_registry': None, 'labels': None, 'updated_at': None, 'floating_ip_enabled': True, 'fixed_subnet': None, 'master_flavor_id': None, 'uuid': uuid.uuid4().hex, 'no_proxy': None, 'https_proxy': None, 'tls_disabled': False, 'keypair_id': None, 'public': False, 'http_proxy': None, 'docker_volume_size': None, 'server_type': 'vm', 'external_network_id': 'public', 'cluster_distro': 'fedora-atomic', 'image_id': 'fedora-atomic-latest', 'volume_driver': None, 'registry_enabled': False, 'docker_storage_driver': 'devicemapper', 'apiserver_port': None, 'name': 'fake-ct-' + uuid.uuid4().hex, 'created_at': datetime.datetime.now(), 'network_driver': 'flannel', 'fixed_network': None, 'coe': 'kubernetes', 'flavor_id': 'm1.medium', 'master_lb_enabled': False, 'dns_nameserver': '8.8.8.8', 'project_id': uuid.uuid4().hex, 'hidden': False, 'tags': "", 'driver': None, } # Overwrite default attributes. ct_info.update(attrs) ct = osc_fakes.FakeResource(info=copy.deepcopy(ct_info), loaded=True) return ct @staticmethod def create_cluster_templates(attrs=None, count=2): """Create multiple fake cluster templates. :param Dictionary attrs: A dictionary with all attributes :param int count: The number of cluster templates to fake :return: A list of FakeResource objects faking the cluster templates """ cts = [] for i in range(0, count): cts.append(FakeClusterTemplate.create_one_cluster_template(attrs)) return cts class FakeCluster(object): """Fake one or more Cluster.""" @staticmethod def create_one_cluster(attrs=None): """Create a fake Cluster. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object, with flavor_id, image_id, and so on """ attrs = attrs or {} # set default attributes. cluster_info = { 'status': 'CREATE_IN_PROGRESS', 'health_status': 'HEALTHY', 'cluster_template_id': 'fake-ct', 'node_addresses': [], 'uuid': '3a369884-b6ba-484f-a206-919b4b718aff', 'cluster_id': 'c4554582-77bd-4734-8f1a-72c3c40e5fb4', 'status_reason': None, 'labels': {}, 'labels_overridden': {}, 'labels_added': {}, 'labels_skipped': {}, 'fixed_network': 'fixed-network', 'fixed_subnet': 'fixed-subnet', 'floating_ip_enabled': True, 'created_at': '2017-03-16T18:40:39+00:00', 'updated_at': '2017-03-16T18:40:45+00:00', 'coe_version': None, 'faults': None, 'keypair': 'fakekey', 'api_address': None, 'master_addresses': [], 'create_timeout': 60, 'node_count': 1, 'discovery_url': 'https://fake.cluster', 'docker_volume_size': 1, 'master_count': 1, 'container_version': None, 'name': 'fake-cluster', 'master_flavor_id': None, 'flavor_id': 'm1.medium', 'project_id': None, 'health_status_reason': {'api': 'ok'}, 'master_lb_enabled': False, } # Overwrite default attributes. cluster_info.update(attrs) cluster = osc_fakes.FakeResource(info=copy.deepcopy(cluster_info), loaded=True) return cluster class FakeCert(object): def __init__(self, pem): self.pem = pem class FakeQuota(object): """Fake one or more Quota""" @staticmethod def create_one_quota(attrs=None): """Create a fake quota :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object, with project_id, resource and so on """ attrs = attrs or {} quota_info = { 'resource': 'Cluster', 'created_at': '2017-09-15T05:40:34+00:00', 'updated_at': '2017-09-15T05:40:34+00:00', 'hard_limit': 1, 'project_id': 'be24b6fba2ed4476b2bd01fa8f0ba74e', 'id': 10, 'name': 'fake-quota', } quota_info.update(attrs) quota = osc_fakes.FakeResource(info=copy.deepcopy(quota_info), loaded=True) return quota class FakeNodeGroup(object): """Fake one or more NodeGroup.""" @staticmethod def create_one_nodegroup(attrs=None): """Create a fake NodeGroup. :param Dictionary attrs: A dictionary with all attributes :return: A FakeResource object, with flavor_id, image_id, and so on """ attrs = attrs or {} # set default attributes. nodegroup_info = { 'created_at': '2017-03-16T18:40:39+00:00', 'updated_at': '2017-03-16T18:40:45+00:00', 'uuid': '3a369884-b6ba-484f-a206-919b4b718aff', 'cluster_id': 'fake-cluster', 'docker_volume_size': None, 'node_addresses': [], 'labels': {}, 'labels_overridden': {}, 'labels_added': {}, 'labels_skipped': {}, 'node_count': 1, 'name': 'fake-nodegroup', 'flavor_id': 'm1.medium', 'image_id': 'fedora-latest', 'project_id': None, 'role': 'worker', 'max_node_count': 10, 'min_node_count': 1, 'is_default': False, 'stack_id': '3a369884-b6ba-484f-fake-stackb718aff', 'status': 'CREATE_COMPLETE', 'status_reason': 'None', 'master_lb_enabled': False, 'node_labels': {}, 'node_taints': [], } # Overwrite default attributes. nodegroup_info.update(attrs) nodegroup = osc_fakes.FakeResource(info=copy.deepcopy(nodegroup_info), loaded=True) return nodegroup ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1314194 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/v1/test_certificates.py0000664000175000017500000001071115244246676026436 0ustar00zuulzuul# Copyright 2015 NEC Corporation. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from magnumclient.osc.v1 import certificates as osc_certificates from magnumclient.tests.osc.unit.v1 import fakes as magnum_fakes class TestCertificate(magnum_fakes.TestMagnumClientOSCV1): def setUp(self): super(TestCertificate, self).setUp() self.clusters_mock = self.app.client_manager.container_infra.clusters class TestRotateCa(TestCertificate): def setUp(self): super(TestRotateCa, self).setUp() attr = dict() attr['name'] = 'fake-cluster-1' self._cluster = magnum_fakes.FakeCluster.create_one_cluster(attr) self.clusters_mock.get = mock.Mock() self.clusters_mock.get.return_value = self._cluster self.cmd = osc_certificates.RotateCa(self.app, None) def test_rotate_ca(self): arglist = ['fake-cluster'] verifylist = [ ('cluster', 'fake-cluster') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.get.assert_called_once_with('fake-cluster') def test_rotate_ca_missing_args(self): arglist = [] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestShowCa(TestCertificate): def setUp(self): super(TestShowCa, self).setUp() attr = dict() attr['name'] = 'fake-cluster-1' self._cluster = magnum_fakes.FakeCluster.create_one_cluster(attr) self.clusters_mock.get = mock.Mock() self.clusters_mock.get.return_value = self._cluster self.cmd = osc_certificates.ShowCa(self.app, None) def test_show_ca(self): arglist = ['fake-cluster'] verifylist = [ ('cluster', 'fake-cluster') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.get.assert_called_once_with('fake-cluster') def test_show_ca_missing_args(self): arglist = [] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestSignCa(TestCertificate): test_csr_path = 'magnumclient/tests/test_csr/test.csr' def setUp(self): super(TestSignCa, self).setUp() attr = dict() attr['name'] = 'fake-cluster-1' self._cluster = magnum_fakes.FakeCluster.create_one_cluster(attr) self.clusters_mock.get = mock.Mock() self.clusters_mock.get.return_value = self._cluster self.cmd = osc_certificates.SignCa(self.app, None) def test_sign_ca(self): arglist = ['fake-cluster', self.test_csr_path] verifylist = [ ('cluster', 'fake-cluster'), ('csr', self.test_csr_path) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.get.assert_called_once_with('fake-cluster') def test_sign_ca_without_csr(self): arglist = ['fake-cluster'] verifylist = [ ('cluster', 'fake-cluster') ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_sign_ca_without_cluster(self): arglist = [self.test_csr_path] verifylist = [ ('csr', self.test_csr_path) ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_show_ca_missing_args(self): arglist = [] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1314194 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/v1/test_cluster_templates.py0000664000175000017500000004042115244246676027531 0ustar00zuulzuul# Copyright 2016 Easystack. 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 unittest import mock from unittest.mock import call from magnumclient.exceptions import InvalidAttribute from magnumclient.osc.v1 import cluster_templates as osc_ct from magnumclient.tests.osc.unit.v1 import fakes as magnum_fakes from osc_lib import exceptions as osc_exceptions class TestClusterTemplate(magnum_fakes.TestMagnumClientOSCV1): default_create_args = { 'coe': 'kubernetes', 'dns_nameserver': '8.8.8.8', 'docker_storage_driver': 'overlay2', 'docker_volume_size': None, 'external_network_id': 'public', 'fixed_network': None, 'fixed_subnet': None, 'flavor_id': 'm1.medium', 'http_proxy': None, 'https_proxy': None, 'image_id': 'fedora-atomic-latest', 'keypair_id': None, 'labels': {}, 'master_flavor_id': None, 'master_lb_enabled': False, 'name': 'fake-ct-1', 'network_driver': None, 'no_proxy': None, 'public': False, 'registry_enabled': False, 'insecure_registry': None, 'server_type': 'vm', 'tls_disabled': False, 'volume_driver': None, 'driver': None, } def setUp(self): super(TestClusterTemplate, self).setUp() self.cluster_templates_mock = ( self.app.client_manager.container_infra.cluster_templates) class TestClusterTemplateCreate(TestClusterTemplate): def setUp(self): super(TestClusterTemplateCreate, self).setUp() attr = dict() attr['name'] = 'fake-ct-1' self.new_ct = ( magnum_fakes.FakeClusterTemplate.create_one_cluster_template(attr)) self.cluster_templates_mock.create = mock.Mock() self.cluster_templates_mock.create.return_value = self.new_ct self.cluster_templates_mock.get = mock.Mock() self.cluster_templates_mock.get.return_value = copy.deepcopy( self.new_ct) self.cluster_templates_mock.update = mock.Mock() self.cluster_templates_mock.update.return_value = self.new_ct # Get the command object to test self.cmd = osc_ct.CreateClusterTemplate(self.app, None) self.data = tuple(map(lambda x: getattr(self.new_ct, x), osc_ct.CLUSTER_TEMPLATE_ATTRIBUTES)) def test_cluster_template_create_required_args_pass(self): """Verifies required arguments.""" arglist = [ '--coe', self.new_ct.coe, '--external-network', self.new_ct.external_network_id, '--image', self.new_ct.image_id, self.new_ct.name ] verifylist = [ ('coe', self.new_ct.coe), ('external_network', self.new_ct.external_network_id), ('image', self.new_ct.image_id), ('name', self.new_ct.name) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.cluster_templates_mock.create.assert_called_with( **self.default_create_args) def test_cluster_template_create_missing_required_arg(self): """Verifies missing required arguments.""" arglist = [ '--external-network', self.new_ct.external_network_id, '--image', self.new_ct.image_id ] verifylist = [ ('external_network', self.new_ct.external_network_id), ('image', self.new_ct.image_id) ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) # Verify all required args are checked and not just coe arglist.append('--coe') arglist.append(self.new_ct.coe) verifylist.append(('coe', self.new_ct.coe)) arglist.remove('--image') arglist.remove(self.new_ct.image_id) verifylist.remove(('image', self.new_ct.image_id)) self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) arglist.remove('--external-network') arglist.remove(self.new_ct.external_network_id) verifylist.remove( ('external_network', self.new_ct.external_network_id)) self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_cluster_template_create_with_driver(self): """--driver is passed through to the API.""" arglist = [ '--coe', self.new_ct.coe, '--external-network', self.new_ct.external_network_id, '--image', self.new_ct.image_id, '--driver', 'k8s-fedora-coreos-v1', self.new_ct.name, ] verifylist = [ ('coe', self.new_ct.coe), ('external_network', self.new_ct.external_network_id), ('image', self.new_ct.image_id), ('driver', 'k8s-fedora-coreos-v1'), ('name', self.new_ct.name), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.cluster_templates_mock.create.assert_called_with( **dict(self.default_create_args, driver='k8s-fedora-coreos-v1')) def test_cluster_template_create_floating_ips(self): """Verifies floating ip parameters.""" arglist = [ '--coe', self.new_ct.coe, '--external-network', self.new_ct.external_network_id, '--image', self.new_ct.image_id, '--floating-ip-enabled', self.new_ct.name ] verifylist = [ ('coe', self.new_ct.coe), ('external_network', self.new_ct.external_network_id), ('image', self.new_ct.image_id), ('floating_ip_enabled', [True]), ('name', self.new_ct.name) ] self.default_create_args['floating_ip_enabled'] = True parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.default_create_args.pop('floating_ip_enabled') arglist.append('--floating-ip-disabled') verifylist.remove(('floating_ip_enabled', [True])) verifylist.append(('floating_ip_enabled', [True, False])) parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.assertRaises(InvalidAttribute, self.cmd.take_action, parsed_args) class TestClusterTemplateDelete(TestClusterTemplate): def setUp(self): super(TestClusterTemplateDelete, self).setUp() self.cluster_templates_mock.delete = mock.Mock() self.cluster_templates_mock.delete.return_value = None # Get the command object to test self.cmd = osc_ct.DeleteClusterTemplate(self.app, None) def test_cluster_template_delete_one(self): arglist = ['foo'] verifylist = [('cluster-templates', ['foo'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.cluster_templates_mock.delete.assert_called_with('foo') def test_cluster_template_delete_multiple(self): arglist = ['foo', 'bar'] verifylist = [('cluster-templates', ['foo', 'bar'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.cluster_templates_mock.delete.assert_has_calls([call('foo'), call('bar')]) def test_cluster_template_delete_bad_uuid(self): self.cluster_templates_mock.delete.side_effect = ( osc_exceptions.NotFound(404)) arglist = ['foo'] verifylist = [('cluster-templates', ['foo'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) returns = self.cmd.take_action(parsed_args) self.assertEqual(returns, None) def test_cluster_template_delete_no_uuid(self): arglist = [] verifylist = [('cluster-templates', [])] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestClusterTemplateList(TestClusterTemplate): attr = dict() attr['name'] = 'fake-ct-1' _cluster_template = ( magnum_fakes.FakeClusterTemplate.create_one_cluster_template(attr)) attr['name'] = 'fake-ct-2' _cluster_template2 = ( magnum_fakes.FakeClusterTemplate.create_one_cluster_template(attr)) columns = [ 'uuid', 'name', 'tags', ] datalist = ( (_cluster_template.uuid, _cluster_template.name, _cluster_template.tags), (_cluster_template2.uuid, _cluster_template2.name, _cluster_template.tags) ) def setUp(self): super(TestClusterTemplateList, self).setUp() self.cluster_templates_mock.list = mock.Mock() self.cluster_templates_mock.list.return_value = [ self._cluster_template, self._cluster_template2 ] # Get the command object to test self.cmd = osc_ct.ListTemplateCluster(self.app, None) def test_cluster_template_list_no_options(self): arglist = [] verifylist = [ ('limit', None), ('sort_key', None), ('sort_dir', None), ('fields', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.cluster_templates_mock.list.assert_called_with( limit=None, sort_dir=None, sort_key=None, ) self.assertEqual(self.columns, columns) index = 0 for d in data: self.assertEqual(self.datalist[index], d) index += 1 def test_cluster_template_list_options(self): arglist = [ '--limit', '1', '--sort-key', 'key', '--sort-dir', 'asc', '--fields', 'field1,field2' ] verifylist = [ ('limit', 1), ('sort_key', 'key'), ('sort_dir', 'asc'), ('fields', 'field1,field2'), ] verifycolumns = self.columns + ['field1', 'field2'] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.cluster_templates_mock.list.assert_called_with( limit=1, sort_dir='asc', sort_key='key', ) self.assertEqual(verifycolumns, columns) def test_cluster_template_list_bad_sort_dir_fail(self): arglist = [ '--sort-dir', 'foo' ] verifylist = [ ('limit', None), ('sort_key', None), ('sort_dir', 'foo'), ('fields', None), ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestClusterTemplateShow(TestClusterTemplate): attr = dict() attr['name'] = 'fake-ct-1' _cluster_template = ( magnum_fakes.FakeClusterTemplate.create_one_cluster_template(attr)) columns = osc_ct.CLUSTER_TEMPLATE_ATTRIBUTES def setUp(self): super(TestClusterTemplateShow, self).setUp() datalist = map(lambda x: getattr(self._cluster_template, x), self.columns) self.show_data_list = tuple(map(lambda x: x if x is not None else '-', datalist)) self.cluster_templates_mock.get = mock.Mock() self.cluster_templates_mock.get.return_value = self._cluster_template # Get the command object to test self.cmd = osc_ct.ShowClusterTemplate(self.app, None) def test_cluster_template_show(self): arglist = ['fake-ct-1'] verifylist = [('cluster-template', 'fake-ct-1')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.cluster_templates_mock.get.assert_called_with('fake-ct-1') self.assertEqual(self.columns, columns) self.assertEqual(self.show_data_list, data) def test_cluster_template_show_no_ct_fail(self): arglist = [] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestClusterTemplateUpdate(TestClusterTemplate): def setUp(self): super(TestClusterTemplateUpdate, self).setUp() attr = dict() attr['name'] = 'fake-ct-1' ct = magnum_fakes.FakeClusterTemplate.create_one_cluster_template(attr) self.cluster_templates_mock.update = mock.Mock() self.cluster_templates_mock.update.return_value = ct # Get the command object to test self.cmd = osc_ct.UpdateClusterTemplate(self.app, None) def test_cluster_template_update_name(self): arglist = ['foo', '--name', 'bar'] verifylist = [('cluster-template', 'foo'), ('name', 'bar')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.cluster_templates_mock.update.assert_called_with( 'foo', [{'op': 'replace', 'path': '/name', 'value': 'bar'}]) def test_cluster_template_update_hidden(self): arglist = ['foo', '--hidden'] verifylist = [('cluster-template', 'foo'), ('hidden', True)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.cluster_templates_mock.update.assert_called_with( 'foo', [{'op': 'replace', 'path': '/hidden', 'value': True}]) def test_cluster_template_update_visible(self): arglist = ['foo', '--visible'] verifylist = [('cluster-template', 'foo'), ('hidden', False)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.cluster_templates_mock.update.assert_called_with( 'foo', [{'op': 'replace', 'path': '/hidden', 'value': False}]) def test_cluster_template_update_public_private(self): arglist = ['foo', '--public'] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.cluster_templates_mock.update.assert_called_with( 'foo', [{'op': 'replace', 'path': '/public', 'value': True}]) def test_cluster_template_update_label_add(self): arglist = ['foo', '--label', 'k=v'] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.cluster_templates_mock.update.assert_called_with( 'foo', [{'op': 'add', 'path': '/labels/k', 'value': 'v'}]) def test_cluster_template_update_label_remove(self): arglist = ['foo', '--no-label', 'k'] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.cluster_templates_mock.update.assert_called_with( 'foo', [{'op': 'remove', 'path': '/labels/k'}]) def test_cluster_template_update_tags(self): arglist = ['foo', '--tag', 'tag1', '--tag', 'tag2'] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.cluster_templates_mock.update.assert_called_with( 'foo', [{'op': 'replace', 'path': '/tags', 'value': 'tag1,tag2'}]) def test_cluster_template_update_no_args(self): arglist = ['foo'] verifylist = [('cluster-template', 'foo')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.assertRaises( InvalidAttribute, self.cmd.take_action, parsed_args) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1324193 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/v1/test_clusters.py0000664000175000017500000004601415244246676025642 0ustar00zuulzuul# Copyright 2016 Easystack. 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 io import StringIO import os import sys import tempfile from unittest import mock from contextlib import contextmanager from unittest.mock import call from magnumclient import exceptions from magnumclient.osc.v1 import clusters as osc_clusters from magnumclient.tests.osc.unit.v1 import fakes as magnum_fakes class TestCluster(magnum_fakes.TestMagnumClientOSCV1): def setUp(self): super(TestCluster, self).setUp() self.clusters_mock = self.app.client_manager.container_infra.clusters self.certificates_mock = \ self.app.client_manager.container_infra.certificates class TestClusterCreate(TestCluster): def setUp(self): super(TestClusterCreate, self).setUp() attr = dict() attr['name'] = 'fake-cluster-1' self._cluster = magnum_fakes.FakeCluster.create_one_cluster(attr) self._default_args = { 'cluster_template_id': 'fake-ct', 'create_timeout': 60, 'discovery_url': None, 'keypair': None, 'master_count': 1, 'name': 'fake-cluster-1', 'node_count': 1, } self.clusters_mock.create = mock.Mock() self.clusters_mock.create.return_value = self._cluster self.clusters_mock.get = mock.Mock() self.clusters_mock.get.return_value = copy.deepcopy(self._cluster) self.clusters_mock.update = mock.Mock() self.clusters_mock.update.return_value = self._cluster # Get the command object to test self.cmd = osc_clusters.CreateCluster(self.app, None) self.data = tuple(map(lambda x: getattr(self._cluster, x), osc_clusters.CLUSTER_ATTRIBUTES)) def test_cluster_create_required_args_pass(self): """Verifies required arguments.""" arglist = [ '--cluster-template', self._cluster.cluster_template_id, self._cluster.name ] verifylist = [ ('cluster_template', self._cluster.cluster_template_id), ('name', self._cluster.name) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.create.assert_called_with(**self._default_args) def test_cluster_create_missing_required_arg(self): """Verifies missing required arguments.""" arglist = [ self._cluster.name ] verifylist = [ ('name', self._cluster.name) ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_cluster_create_with_labels(self): """Verifies labels are properly parsed when given as argument.""" expected_args = self._default_args expected_args['labels'] = { 'arg1': 'value1', 'arg2': 'value2' } arglist = [ '--cluster-template', self._cluster.cluster_template_id, '--labels', 'arg1=value1', '--labels', 'arg2=value2', self._cluster.name ] verifylist = [ ('cluster_template', self._cluster.cluster_template_id), ('labels', ['arg1=value1', 'arg2=value2']), ('name', self._cluster.name) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.create.assert_called_with(**expected_args) def test_cluster_create_with_lb_disabled(self): """Verifies master lb disabled properly parsed.""" expected_args = self._default_args expected_args['master_lb_enabled'] = False arglist = [ '--cluster-template', self._cluster.cluster_template_id, '--master-lb-disabled', self._cluster.name ] verifylist = [ ('cluster_template', self._cluster.cluster_template_id), ('master_lb_enabled', [False]), ('name', self._cluster.name) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.create.assert_called_with(**expected_args) class TestClusterDelete(TestCluster): def setUp(self): super(TestClusterDelete, self).setUp() self.clusters_mock.delete = mock.Mock() self.clusters_mock.delete.return_value = None # Get the command object to test self.cmd = osc_clusters.DeleteCluster(self.app, None) def test_cluster_delete_one(self): arglist = ['foo'] verifylist = [('cluster', ['foo'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.delete.assert_called_with('foo') def test_cluster_delete_multiple(self): arglist = ['foo', 'bar'] verifylist = [('cluster', ['foo', 'bar'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.delete.assert_has_calls([call('foo'), call('bar')]) def test_cluster_delete_bad_uuid(self): arglist = ['foo'] verifylist = [('cluster', ['foo'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) returns = self.cmd.take_action(parsed_args) self.assertEqual(returns, None) def test_cluster_delete_no_uuid(self): arglist = [] verifylist = [('cluster', [])] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestClusterList(TestCluster): attr = dict() attr['name'] = 'fake-cluster-1' _cluster = magnum_fakes.FakeCluster.create_one_cluster(attr) columns = [ 'uuid', 'name', 'keypair', 'node_count', 'master_count', 'status', 'health_status' ] datalist = ( ( _cluster.uuid, _cluster.name, _cluster.keypair, _cluster.node_count, _cluster.master_count, _cluster.status, _cluster.health_status, ), ) def setUp(self): super(TestClusterList, self).setUp() self.clusters_mock.list = mock.Mock() self.clusters_mock.list.return_value = [self._cluster] # Get the command object to test self.cmd = osc_clusters.ListCluster(self.app, None) def test_cluster_list_no_options(self): arglist = [] verifylist = [ ('limit', None), ('sort_key', None), ('sort_dir', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.clusters_mock.list.assert_called_with( limit=None, sort_dir=None, sort_key=None, ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) def test_cluster_list_options(self): arglist = [ '--limit', '1', '--sort-key', 'key', '--sort-dir', 'asc' ] verifylist = [ ('limit', 1), ('sort_key', 'key'), ('sort_dir', 'asc') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.list.assert_called_with( limit=1, sort_dir='asc', sort_key='key', ) def test_cluster_list_bad_sort_dir_fail(self): arglist = [ '--sort-dir', 'foo' ] verifylist = [ ('limit', None), ('sort_key', None), ('sort_dir', 'foo'), ('fields', None), ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestClusterUpdate(TestCluster): def setUp(self): super(TestClusterUpdate, self).setUp() self.clusters_mock.update = mock.Mock() self.clusters_mock.update.return_value = None self.cmd = osc_clusters.UpdateCluster(self.app, None) def test_cluster_update_name(self): arglist = ['foo', '--name', 'bar'] verifylist = [('cluster', 'foo'), ('name', 'bar'), ('rollback', False)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.update.assert_called_with( 'foo', [{'op': 'replace', 'path': '/name', 'value': 'bar'}], rollback=False) def test_cluster_update_node_count(self): arglist = ['foo', '--node-count', '5'] verifylist = [('cluster', 'foo'), ('node_count', 5)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.update.assert_called_with( 'foo', [{'op': 'replace', 'path': '/node_count', 'value': 5}], rollback=False) def test_cluster_update_label_add(self): arglist = ['foo', '--label', 'k1=v1', '--label', 'k2=v2'] verifylist = [('cluster', 'foo'), ('labels', ['k1=v1', 'k2=v2'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.update.assert_called_with( 'foo', [{'op': 'add', 'path': '/labels/k1', 'value': 'v1'}, {'op': 'add', 'path': '/labels/k2', 'value': 'v2'}], rollback=False) def test_cluster_update_label_remove(self): arglist = ['foo', '--no-label', 'k1'] verifylist = [('cluster', 'foo'), ('no_labels', ['k1'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.update.assert_called_with( 'foo', [{'op': 'remove', 'path': '/labels/k1'}], rollback=False) def test_cluster_update_rollback(self): arglist = ['foo', '--name', 'bar', '--rollback'] verifylist = [('cluster', 'foo'), ('name', 'bar'), ('rollback', True)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.update.assert_called_with( 'foo', [{'op': 'replace', 'path': '/name', 'value': 'bar'}], rollback=True) def test_cluster_update_nothing_raises(self): arglist = ['foo'] parsed_args = self.check_parser(self.cmd, arglist, []) self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) class TestClusterShow(TestCluster): def setUp(self): super(TestClusterShow, self).setUp() attr = dict() attr['name'] = 'fake-cluster-1' self._cluster = magnum_fakes.FakeCluster.create_one_cluster(attr) self.clusters_mock.get = mock.Mock() self.clusters_mock.get.return_value = self._cluster # Get the command object to test self.cmd = osc_clusters.ShowCluster(self.app, None) self.data = tuple(map(lambda x: getattr(self._cluster, x), osc_clusters.CLUSTER_ATTRIBUTES)) def test_cluster_show_pass(self): arglist = ['fake-cluster'] verifylist = [ ('cluster', 'fake-cluster') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.clusters_mock.get.assert_called_with('fake-cluster') self.assertEqual(osc_clusters.CLUSTER_ATTRIBUTES, columns) self.assertEqual(self.data, data) def test_cluster_show_no_cluster_fail(self): arglist = [] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) @contextmanager def capture(command, *args, **kwargs): out, sys.stdout = sys.stdout, StringIO() try: command(*args, **kwargs) sys.stdout.seek(0) yield sys.stdout.read() finally: sys.stdout = out class TestClusterConfig(TestCluster): def setUp(self): super(TestClusterConfig, self).setUp() attr = dict() attr['name'] = 'fake-cluster-1' attr['status'] = 'CREATE_COMPLETE' self._cluster = magnum_fakes.FakeCluster.create_one_cluster(attr) self.clusters_mock.get = mock.Mock() self.clusters_mock.get.return_value = self._cluster cert = magnum_fakes.FakeCert(pem='foo bar') self.certificates_mock.create = mock.Mock() self.certificates_mock.create.return_value = cert self.certificates_mock.get = mock.Mock() self.certificates_mock.get.return_value = cert # Fake the cluster_template attr = dict() attr['name'] = 'fake-ct' self._cluster_template = \ magnum_fakes.FakeClusterTemplate.create_one_cluster_template(attr) self.cluster_templates_mock = \ self.app.client_manager.container_infra.cluster_templates self.cluster_templates_mock.get = mock.Mock() self.cluster_templates_mock.get.return_value = self._cluster_template # Get the command object to test self.cmd = osc_clusters.ConfigCluster(self.app, None) def test_cluster_config_no_cluster_fail(self): arglist = [] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) @mock.patch.dict(os.environ, {'SHELL': '/bin/bash'}) def test_cluster_config_custom_dir_with_config_only_works_if_force(self): tmp_dir = tempfile.mkdtemp() open(os.path.join(tmp_dir, 'config'), 'a').close() # touch config arglist = ['fake-cluster', '--dir', tmp_dir] verifylist = [ ('cluster', 'fake-cluster'), ('force', False), ('dir', tmp_dir), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) self.clusters_mock.get.assert_called_with('fake-cluster') arglist = ['fake-cluster', '--force', '--dir', tmp_dir] verifylist = [ ('cluster', 'fake-cluster'), ('force', True), ('dir', tmp_dir), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) expected_value = '''\ export KUBECONFIG={}/config '''.format(tmp_dir) with capture(self.cmd.take_action, parsed_args) as output: self.assertEqual(expected_value, output) self.clusters_mock.get.assert_called_with('fake-cluster') @mock.patch.dict(os.environ, {'SHELL': '/bin/bash'}) def test_cluster_config_with_custom_dir(self): tmp_dir = tempfile.mkdtemp() arglist = ['fake-cluster', '--dir', tmp_dir] verifylist = [ ('cluster', 'fake-cluster'), ('dir', tmp_dir), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) expected_value = '''\ export KUBECONFIG={}/config '''.format(tmp_dir) with capture(self.cmd.take_action, parsed_args) as output: self.assertEqual(expected_value, output) self.clusters_mock.get.assert_called_with('fake-cluster') @mock.patch.dict(os.environ, {'SHELL': '/bin/bash'}) def test_cluster_config_creates_config_in_cwd_if_not_dir_specified(self): tmp_dir = tempfile.mkdtemp() os.chdir(tmp_dir) arglist = ['fake-cluster'] verifylist = [ ('cluster', 'fake-cluster'), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) expected_value = '''\ export KUBECONFIG={}/config '''.format(os.getcwd()) with capture(self.cmd.take_action, parsed_args) as output: self.assertEqual(expected_value, output) self.clusters_mock.get.assert_called_with('fake-cluster') class TestClusterResize(TestCluster): def setUp(self): super(TestClusterResize, self).setUp() self.cluster = mock.Mock() self.cluster.uuid = "UUID1" self.clusters_mock.resize = mock.Mock() self.clusters_mock.resize.return_value = None self.clusters_mock.get = mock.Mock() self.clusters_mock.get.return_value = self.cluster # Get the command object to test self.cmd = osc_clusters.ResizeCluster(self.app, None) def test_cluster_resize_pass(self): arglist = ['foo', '2'] verifylist = [ ('cluster', 'foo'), ('node_count', 2), ('nodes_to_remove', None), ('nodegroup', None) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.resize.assert_called_with( "UUID1", 2, None, None ) def test_cluster_resize_to_zero_pass(self): arglist = ['foo', '0'] verifylist = [ ('cluster', 'foo'), ('node_count', 0), ('nodes_to_remove', None), ('nodegroup', None) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.resize.assert_called_with( "UUID1", 0, None, None ) class TestClusterUpgrade(TestCluster): def setUp(self): super(TestClusterUpgrade, self).setUp() self.cluster = mock.Mock() self.cluster.uuid = "UUID1" self.clusters_mock.upgrade = mock.Mock() self.clusters_mock.upgrade.return_value = None self.clusters_mock.get = mock.Mock() self.clusters_mock.get.return_value = self.cluster # Get the command object to test self.cmd = osc_clusters.UpgradeCluster(self.app, None) def test_cluster_upgrade_pass(self): cluster_template_id = 'TEMPLATE_ID' arglist = ['foo', cluster_template_id] verifylist = [ ('cluster', 'foo'), ('cluster_template', cluster_template_id), ('max_batch_size', 1), ('nodegroup', None) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.upgrade.assert_called_with( "UUID1", cluster_template_id, 1, None ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1324193 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/v1/test_mservices.py0000664000175000017500000000332115244246676025770 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from magnumclient.osc.v1 import mservices from magnumclient.tests.osc.unit.v1 import fakes class TestServiceList(fakes.TestMagnumClientOSCV1): columns = ('id', 'host', 'binary', 'state', 'disabled', 'disabled_reason', 'created_at', 'updated_at') def setUp(self): super(TestServiceList, self).setUp() self.mservices_mock = self.app.client_manager.container_infra.mservices self.mservices_mock.list = mock.Mock() fake_service = mock.Mock( Binary='magnum-conductor', Host='Host1', Status='enabled', State='up', Updated_at=None, Disabled_Reason=None, ) fake_service.name = 'test_service' self.mservices_mock.list.return_value = [fake_service] # Get the command object to test self.cmd = mservices.ListService(self.app, None) def test_service_list(self): arglist = [] parsed_args = self.check_parser(self.cmd, arglist, []) columns, data = self.cmd.take_action(parsed_args) self.mservices_mock.list.assert_called_with() self.assertEqual(self.columns, columns) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1324193 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/v1/test_nodegroups.py0000664000175000017500000003671415244246676026171 0ustar00zuulzuul# Copyright (c) 2018 European Organization for Nuclear Research. # 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 unittest import mock from unittest.mock import call from magnumclient import exceptions from magnumclient.osc.v1 import nodegroups as osc_nodegroups from magnumclient.tests.osc.unit.v1 import fakes as magnum_fakes class TestNodeGroup(magnum_fakes.TestMagnumClientOSCV1): def setUp(self): super(TestNodeGroup, self).setUp() self.ng_mock = self.app.client_manager.container_infra.nodegroups class TestNodeGroupCreate(TestNodeGroup): def setUp(self): super(TestNodeGroupCreate, self).setUp() self.nodegroup = magnum_fakes.FakeNodeGroup.create_one_nodegroup() self.ng_mock.create = mock.Mock() self.ng_mock.create.return_value = self.nodegroup self.ng_mock.get = mock.Mock() self.ng_mock.get.return_value = copy.deepcopy(self.nodegroup) self.ng_mock.update = mock.Mock() self.ng_mock.update.return_value = self.nodegroup self._default_args = { 'name': 'fake-nodegroup', 'node_count': 1, 'role': 'worker', 'min_node_count': 0, 'max_node_count': None, } # Get the command object to test self.cmd = osc_nodegroups.CreateNodeGroup(self.app, None) self.data = tuple(map(lambda x: getattr(self.nodegroup, x), osc_nodegroups.NODEGROUP_ATTRIBUTES)) def test_nodegroup_create_required_args_pass(self): """Verifies required arguments.""" arglist = [ self.nodegroup.cluster_id, self.nodegroup.name ] verifylist = [ ('cluster', self.nodegroup.cluster_id), ('name', self.nodegroup.name) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.ng_mock.create.assert_called_with(self.nodegroup.cluster_id, **self._default_args) def test_nodegroup_create_missing_required_arg(self): """Verifies missing required arguments.""" arglist = [ self.nodegroup.name ] verifylist = [ ('name', self.nodegroup.name) ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_nodegroup_create_with_labels(self): """Verifies labels are properly parsed when given as argument.""" expected_args = self._default_args expected_args['labels'] = { 'arg1': 'value1', 'arg2': 'value2' } arglist = [ '--labels', 'arg1=value1', '--labels', 'arg2=value2', self.nodegroup.cluster_id, self.nodegroup.name ] verifylist = [ ('labels', ['arg1=value1', 'arg2=value2']), ('name', self.nodegroup.name), ('cluster', self.nodegroup.cluster_id) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.ng_mock.create.assert_called_with(self.nodegroup.cluster_id, **expected_args) def test_nodegroup_create_with_node_labels(self): """Node labels are forwarded as the node_labels API field.""" expected_args = self._default_args expected_args['node_labels'] = {'workload': 'gpu'} arglist = [ '--node-labels', 'workload=gpu', self.nodegroup.cluster_id, self.nodegroup.name, ] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.ng_mock.create.assert_called_with(self.nodegroup.cluster_id, **expected_args) def test_nodegroup_create_with_node_taints(self): """Node taints are parsed and forwarded as a list of dicts.""" expected_args = self._default_args expected_args['node_taints'] = [ {'key': 'workload', 'value': 'gpu', 'effect': 'NoSchedule'}, {'key': 'dedicated', 'value': '', 'effect': 'NoExecute'}, ] arglist = [ '--node-taints', 'workload=gpu:NoSchedule,dedicated=:NoExecute', self.nodegroup.cluster_id, self.nodegroup.name, ] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.ng_mock.create.assert_called_with(self.nodegroup.cluster_id, **expected_args) class TestNodeGroupDelete(TestNodeGroup): def setUp(self): super(TestNodeGroupDelete, self).setUp() self.ng_mock.delete = mock.Mock() self.ng_mock.delete.return_value = None # Get the command object to test self.cmd = osc_nodegroups.DeleteNodeGroup(self.app, None) def test_nodegroup_delete_one(self): arglist = ['foo', 'fake-nodegroup'] verifylist = [ ('cluster', 'foo'), ('nodegroup', ['fake-nodegroup']) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.ng_mock.delete.assert_called_with('foo', 'fake-nodegroup') def test_nodegroup_delete_multiple(self): arglist = ['foo', 'fake-nodegroup1', 'fake-nodegroup2'] verifylist = [ ('cluster', 'foo'), ('nodegroup', ['fake-nodegroup1', 'fake-nodegroup2']) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.ng_mock.delete.assert_has_calls( [call('foo', 'fake-nodegroup1'), call('foo', 'fake-nodegroup2')] ) def test_nodegroup_delete_no_args(self): arglist = [] verifylist = [ ('cluster', ''), ('nodegroup', []) ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestNodeGroupUpdate(TestNodeGroup): def setUp(self): super(TestNodeGroupUpdate, self).setUp() self.ng_mock.update = mock.Mock() self.ng_mock.update.return_value = None # Get the command object to test self.cmd = osc_nodegroups.UpdateNodeGroup(self.app, None) def test_nodegroup_update_min_nodes(self): arglist = ['foo', 'ng1', '--min-nodes', '2'] verifylist = [('cluster', 'foo'), ('nodegroup', 'ng1'), ('min_node_count', 2)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.ng_mock.update.assert_called_with( 'foo', 'ng1', [{'op': 'replace', 'path': '/min_node_count', 'value': 2}]) def test_nodegroup_update_max_nodes(self): arglist = ['foo', 'ng1', '--max-nodes', '10'] verifylist = [('cluster', 'foo'), ('nodegroup', 'ng1'), ('max_node_count', 10)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.ng_mock.update.assert_called_with( 'foo', 'ng1', [{'op': 'replace', 'path': '/max_node_count', 'value': 10}]) def test_nodegroup_update_both(self): arglist = ['foo', 'ng1', '--min-nodes', '1', '--max-nodes', '5'] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.ng_mock.update.assert_called_with( 'foo', 'ng1', [{'op': 'replace', 'path': '/min_node_count', 'value': 1}, {'op': 'replace', 'path': '/max_node_count', 'value': 5}]) def test_nodegroup_update_node_labels(self): arglist = ['foo', 'ng1', '--node-labels', 'workload=gpu'] verifylist = [ ('cluster', 'foo'), ('nodegroup', 'ng1'), ('node_labels', ['workload=gpu']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.ng_mock.update.assert_called_with( 'foo', 'ng1', [{'op': 'replace', 'path': '/node_labels', 'value': str({'workload': 'gpu'})}] ) def test_nodegroup_update_node_taints(self): arglist = ['foo', 'ng1', '--node-taints', 'workload=gpu:NoSchedule,dedicated=:' 'NoExecute'] verifylist = [ ('cluster', 'foo'), ('nodegroup', 'ng1'), ('node_taints', ['workload=gpu:NoSchedule,dedicated=:NoExecute']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.ng_mock.update.assert_called_with( 'foo', 'ng1', [{'op': 'replace', 'path': '/node_taints', 'value': str([ {'key': 'workload', 'value': 'gpu', 'effect': 'NoSchedule'}, {'key': 'dedicated', 'value': '', 'effect': 'NoExecute'}, ])}] ) def test_nodegroup_update_node_labels_and_taints(self): arglist = ['foo', 'ng1', '--node-labels', 'workload=gpu', '--node-taints', 'workload=gpu:NoSchedule'] verifylist = [ ('cluster', 'foo'), ('nodegroup', 'ng1'), ('node_labels', ['workload=gpu']), ('node_taints', ['workload=gpu:NoSchedule']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.ng_mock.update.assert_called_with( 'foo', 'ng1', [{'op': 'replace', 'path': '/node_labels', 'value': str({'workload': 'gpu'})}, {'op': 'replace', 'path': '/node_taints', 'value': str([{'key': 'workload', 'value': 'gpu', 'effect': 'NoSchedule'}])}] ) def test_nodegroup_update_empty_node_labels_clears_field(self): # Passing an empty value replaces the field with an empty collection. arglist = ['foo', 'ng1', '--node-labels', ''] verifylist = [ ('cluster', 'foo'), ('nodegroup', 'ng1'), ('node_labels', ['']), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.ng_mock.update.assert_called_with( 'foo', 'ng1', [{'op': 'replace', 'path': '/node_labels', 'value': str({})}] ) def test_nodegroup_update_nothing_raises(self): arglist = ['foo', 'ng1'] parsed_args = self.check_parser(self.cmd, arglist, []) self.assertRaises(exceptions.CommandError, self.cmd.take_action, parsed_args) self.ng_mock.update.assert_not_called() class TestNodeGroupShow(TestNodeGroup): def setUp(self): super(TestNodeGroupShow, self).setUp() self.nodegroup = magnum_fakes.FakeNodeGroup.create_one_nodegroup() self.ng_mock.get = mock.Mock() self.ng_mock.get.return_value = self.nodegroup self.data = tuple(map(lambda x: getattr(self.nodegroup, x), osc_nodegroups.NODEGROUP_ATTRIBUTES)) # Get the command object to test self.cmd = osc_nodegroups.ShowNodeGroup(self.app, None) def test_nodegroup_show_pass(self): arglist = ['fake-cluster', 'fake-nodegroup'] verifylist = [ ('cluster', 'fake-cluster'), ('nodegroup', 'fake-nodegroup') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.ng_mock.get.assert_called_with( 'fake-cluster', 'fake-nodegroup') self.assertEqual(osc_nodegroups.NODEGROUP_ATTRIBUTES, columns) self.assertEqual(self.data, data) def test_nodegroup_show_no_nodegroup_fail(self): arglist = ['fake-cluster'] verifylist = [ ('cluster', 'fake-cluster'), ('nodegroup', '') ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_nodegroup_show_no_args(self): arglist = [] verifylist = [ ('cluster', ''), ('nodegroup', '') ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestNodeGroupList(TestNodeGroup): nodegroup = magnum_fakes.FakeNodeGroup.create_one_nodegroup() columns = ['uuid', 'name', 'flavor_id', 'image_id', 'node_count', 'status', 'role'] datalist = ( ( nodegroup.uuid, nodegroup.name, nodegroup.flavor_id, nodegroup.image_id, nodegroup.node_count, nodegroup.status, nodegroup.role, ), ) def setUp(self): super(TestNodeGroupList, self).setUp() self.ng_mock.list = mock.Mock() self.ng_mock.list.return_value = [self.nodegroup] # Get the command object to test self.cmd = osc_nodegroups.ListNodeGroup(self.app, None) def test_nodegroup_list_no_options(self): arglist = [] verifylist = [ ('cluster', ''), ('limit', None), ('sort_key', None), ('sort_dir', None), ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_nodegroup_list_ok(self): arglist = ['fake-cluster'] verifylist = [ ('cluster', 'fake-cluster'), ('limit', None), ('sort_key', None), ('sort_dir', None), ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) self.ng_mock.list.assert_called_with( 'fake-cluster', limit=None, sort_dir=None, sort_key=None, role=None, ) self.assertEqual(self.columns, columns) self.assertEqual(self.datalist, tuple(data)) def test_nodegroup_list_options(self): arglist = [ 'fake-cluster', '--limit', '1', '--sort-key', 'key', '--sort-dir', 'asc' ] verifylist = [ ('cluster', 'fake-cluster'), ('limit', 1), ('sort_key', 'key'), ('sort_dir', 'asc') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.ng_mock.list.assert_called_with( 'fake-cluster', limit=1, sort_dir='asc', sort_key='key', role=None ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1324193 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/v1/test_quotas.py0000664000175000017500000002350215244246676025307 0ustar00zuulzuul# Copyright 2015 NEC Corporation. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from magnumclient.osc.v1 import quotas as osc_quotas from magnumclient.tests.osc.unit.v1 import fakes as magnum_fakes class TestQuotas(magnum_fakes.TestMagnumClientOSCV1): def setUp(self): super(TestQuotas, self).setUp() self.quotas_mock = self.app.client_manager.container_infra.quotas class TestQuotasCreate(TestQuotas): def setUp(self): super(TestQuotasCreate, self).setUp() attr = dict() attr['name'] = 'fake-quota' attr['project_id'] = 'abc' attr['resource'] = 'Cluster' self._quota = magnum_fakes.FakeQuota.create_one_quota(attr) self._default_args = { 'project_id': 'abc', 'resource': 'Cluster', 'hard_limit': 1 } self.quotas_mock.create = mock.Mock() self.quotas_mock.create.return_value = self._quota self.cmd = osc_quotas.CreateQuotas(self.app, None) self.data = tuple(map(lambda x: getattr(self._quota, x), osc_quotas.QUOTA_ATTRIBUTES)) def test_quotas_create(self): arglist = [ '--project-id', 'abc', '--resource', 'Cluster' ] verifylist = [ ('project_id', 'abc'), ('resource', 'Cluster') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.quotas_mock.create.assert_called_with(**self._default_args) def test_quotas_create_with_hardlimit(self): arglist = [ '--project-id', 'abc', '--resource', 'Cluster', '--hard-limit', '10' ] verifylist = [ ('project_id', 'abc'), ('resource', 'Cluster'), ('hard_limit', 10) ] self._default_args['hard_limit'] = 10 parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.quotas_mock.create.assert_called_with(**self._default_args) def test_quotas_create_wrong_projectid(self): arglist = ['abcd'] verifylist = [ ('project_id', 'abcd') ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_quotas_create_missing_args(self): arglist = [] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_quotas_create_with_wrong_args(self): arglist = [ '--project-id', 'abc', '--resources', 'Cluster', # Misspelling 'resources' '--hard-limit', '10' ] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestQuotasDelete(TestQuotas): def setUp(self): super(TestQuotasDelete, self).setUp() self.quotas_mock.delete = mock.Mock() self.quotas_mock.delete.return_value = None self.cmd = osc_quotas.DeleteQuotas(self.app, None) def test_quotas_delete(self): arglist = [ '--project-id', 'abc', '--resource', 'Cluster' ] verifylist = [ ('project_id', 'abc'), ('resource', 'Cluster') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.quotas_mock.delete.assert_called_with('abc', 'Cluster') def test_quotas_delete_no_project_id(self): arglist = [ '--resource', 'Cluster' ] verifylist = [ ('resource', 'Cluster') ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_quotas_delete_no_resource(self): arglist = [ '--project-id', 'abc', ] verifylist = [ ('project_id', 'abc') ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_quotas_delete_missing_args(self): arglist = [] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_quotas_delete_wrong_args(self): arglist = [ '--project-ids', 'abc', # Misspelling 'ids' instead of 'id' '--resource', 'Cluster' ] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestQuotasShow(TestQuotas): def setUp(self): super(TestQuotasShow, self).setUp() attr = dict() attr['name'] = 'fake-quota' attr['project_id'] = 'abc' attr['resource'] = 'Cluster' self._quota = magnum_fakes.FakeQuota.create_one_quota(attr) self._default_args = { 'project_id': 'abc', 'resource': 'Cluster', } self.quotas_mock.get = mock.Mock() self.quotas_mock.get.return_value = self._quota self.cmd = osc_quotas.ShowQuotas(self.app, None) self.data = tuple(map(lambda x: getattr(self._quota, x), osc_quotas.QUOTA_ATTRIBUTES)) def test_quotas_show(self): arglist = [ '--project-id', 'abc', '--resource', 'Cluster' ] verifylist = [ ('project_id', 'abc'), ('resource', 'Cluster') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.quotas_mock.get.assert_called_with('abc', 'Cluster') def test_quotas_show_missing_args(self): arglist = [] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestQuotasUpdate(TestQuotas): def setUp(self): super(TestQuotasUpdate, self).setUp() attr = dict() attr['name'] = 'fake-quota' attr['project_id'] = 'abc' attr['resource'] = 'Cluster' self._quota = magnum_fakes.FakeQuota.create_one_quota(attr) self._default_args = { 'project_id': 'abc', 'resource': 'Cluster', 'hard_limit': 10 } self.quotas_mock.update = mock.Mock() self.quotas_mock.update.return_value = self._quota self.cmd = osc_quotas.UpdateQuotas(self.app, None) def test_quotas_update(self): arglist = [ '--project-id', 'abc', '--resource', 'Cluster', '--hard-limit', '10' ] verifylist = [ ('project_id', 'abc'), ('resource', 'Cluster'), ('hard_limit', 10) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.quotas_mock.update.assert_called_with('abc', 'Cluster', self._default_args) def test_quotas_update_missing_args(self): arglist = ['abcd'] verifylist = [ ('project_id', 'abcd') ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) def test_quotas_update_wrong_args(self): arglist = [ '--project-id', 'abc', '--resource', 'Cluster', '--hard-limits', '10' # Misspelling 'hard-limits' ] verifylist = [ ('project_id', 'abc'), ('resource', 'Cluster'), ('hard_limit', 10) ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) class TestQuotasList(TestQuotas): def setUp(self): super(TestQuotasList, self).setUp() attr = dict() attr['name'] = 'fake-quota' attr['project_id'] = 'abc' attr['resource'] = 'Cluster' self._quota = magnum_fakes.FakeQuota.create_one_quota(attr) self.quotas_mock.list = mock.Mock() self.quotas_mock.list.return_value = [self._quota] self.cmd = osc_quotas.ListQuotas(self.app, None) def test_quotas_list_with_no_options(self): arglist = [ ] verifylist = [ ('limit', None), ('sort_key', None), ('sort_dir', None), ('marker', None), ('all_tenants', False) ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.quotas_mock.list.assert_called_with( limit=None, sort_dir=None, sort_key=None, marker=None, all_tenants=False ) def test_quotas_list_wrong_args(self): arglist = ['--wrong'] verifylist = [ ('wrong') ] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1324193 python_magnumclient-5.0.0/magnumclient/tests/osc/unit/v1/test_stats.py0000664000175000017500000000444315244246676025134 0ustar00zuulzuul# Copyright 2015 NEC Corporation. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from magnumclient.osc.v1 import stats as osc_stats from magnumclient.tests.osc.unit.v1 import fakes as magnum_fakes class TestStats(magnum_fakes.TestMagnumClientOSCV1): def setUp(self): super(TestStats, self).setUp() self.clusters_mock = self.app.client_manager.container_infra.stats class TestStatsList(TestStats): def setUp(self): super(TestStatsList, self).setUp() attr = dict() attr['name'] = 'fake-cluster-1' attr['project_id'] = 'abc' attr['node_count'] = 2 attr['master_count'] = 1 self._cluster = magnum_fakes.FakeCluster.create_one_cluster(attr) self.clusters_mock.list = mock.Mock() self.clusters_mock.list.return_value = self._cluster self.cmd = osc_stats.ListStats(self.app, None) def test_stats_list(self): arglist = ['abc'] verifylist = [ ('project_id', 'abc') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.list.assert_called_once_with(project_id='abc') def test_stats_list_wrong_projectid(self): arglist = ['abcd'] verifylist = [ ('project_id', 'abcd') ] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.clusters_mock.list.assert_called_once_with(project_id='abcd') def test_stats_list_missing_args(self): arglist = [] verifylist = [] self.assertRaises(magnum_fakes.MagnumParseException, self.check_parser, self.cmd, arglist, verifylist) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1324193 python_magnumclient-5.0.0/magnumclient/tests/test_client.py0000664000175000017500000000267615244246676023171 0ustar00zuulzuul# Copyright (c) 2015 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools from unittest import mock from magnumclient import client class ClientTest(testtools.TestCase): @mock.patch('magnumclient.v1.client.Client') def test_no_version_argument(self, mock_magnum_client): client.Client(auth_token='mytoken', magnum_url='http://myurl/') mock_magnum_client.assert_called_with( auth_token='mytoken', magnum_url='http://myurl/') @mock.patch('magnumclient.v1.client.Client') def test_valid_version_argument(self, mock_magnum_client): client.Client(version='1', magnum_url='http://myurl/') mock_magnum_client.assert_called_with(magnum_url='http://myurl/') @mock.patch('magnumclient.v1.client.Client') def test_invalid_version_argument(self, mock_magnum_client): self.assertRaises( ValueError, client.Client, version='2', magnum_url='http://myurl/') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0025783 python_magnumclient-5.0.0/magnumclient/tests/test_csr/0000775000175000017500000000000015244246764022113 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1324193 python_magnumclient-5.0.0/magnumclient/tests/test_csr/test.csr0000664000175000017500000000001315244246676023577 0ustar00zuulzuul'fake-csr' ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1334193 python_magnumclient-5.0.0/magnumclient/tests/test_httpclient.py0000664000175000017500000004500515244246676024062 0ustar00zuulzuul# Copyright 2015 OpenStack LLC. # 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 http import client as http_client import io from unittest import mock from oslo_serialization import jsonutils import socket from magnumclient.common import httpclient as http from magnumclient import exceptions as exc from magnumclient.exceptions import GatewayTimeout from magnumclient.exceptions import MultipleChoices from magnumclient.tests import utils NORMAL_ERROR = 0 ERROR_DICT = 1 ERROR_LIST_WITH_DETAIL = 2 ERROR_LIST_WITH_DESC = 3 def _get_error_body(faultstring=None, debuginfo=None, err_type=NORMAL_ERROR): if err_type == NORMAL_ERROR: error_body = { 'faultstring': faultstring, 'debuginfo': debuginfo } raw_error_body = jsonutils.dumps(error_body) body = {'error_message': raw_error_body} elif err_type == ERROR_DICT: body = {'error': {'title': faultstring, 'message': debuginfo}} elif err_type == ERROR_LIST_WITH_DETAIL: main_body = {'title': faultstring, 'detail': debuginfo} body = {'errors': [main_body]} elif err_type == ERROR_LIST_WITH_DESC: main_body = {'title': faultstring, 'description': debuginfo} body = {'errors': [main_body]} raw_body = jsonutils.dumps(body) return raw_body HTTP_CLASS = http_client.HTTPConnection HTTPS_CLASS = http.VerifiedHTTPSConnection DEFAULT_TIMEOUT = 600 class HttpClientTest(utils.BaseTestCase): def test_url_generation_trailing_slash_in_base(self): client = http.HTTPClient('http://localhost/') url = client._make_connection_url('/v1/resources') self.assertEqual('/v1/resources', url) def test_url_generation_without_trailing_slash_in_base(self): client = http.HTTPClient('http://localhost') url = client._make_connection_url('/v1/resources') self.assertEqual('/v1/resources', url) def test_url_generation_prefix_slash_in_path(self): client = http.HTTPClient('http://localhost/') url = client._make_connection_url('/v1/resources') self.assertEqual('/v1/resources', url) def test_url_generation_without_prefix_slash_in_path(self): client = http.HTTPClient('http://localhost') url = client._make_connection_url('v1/resources') self.assertEqual('/v1/resources', url) def test_server_exception_empty_body(self): error_body = _get_error_body() fake_resp = utils.FakeResponse({'content-type': 'application/json'}, io.StringIO(error_body), version=1, status=500) client = http.HTTPClient('http://localhost/') client.get_connection = ( lambda *a, **kw: utils.FakeConnection(fake_resp)) error = self.assertRaises(exc.InternalServerError, client.json_request, 'GET', '/v1/resources') self.assertEqual('Internal Server Error (HTTP 500)', str(error)) def test_server_exception_msg_only(self): error_msg = 'test error msg' error_body = _get_error_body(error_msg, err_type=ERROR_DICT) fake_resp = utils.FakeResponse({'content-type': 'application/json'}, io.StringIO(error_body), version=1, status=500) client = http.HTTPClient('http://localhost/') client.get_connection = ( lambda *a, **kw: utils.FakeConnection(fake_resp)) error = self.assertRaises(exc.InternalServerError, client.json_request, 'GET', '/v1/resources') self.assertEqual(error_msg + ' (HTTP 500)', str(error)) def test_server_exception_msg_and_traceback(self): error_msg = 'another test error' error_trace = ("\"Traceback (most recent call last):\\n\\n " "File \\\"/usr/local/lib/python2.7/...") error_body = _get_error_body(error_msg, error_trace, ERROR_LIST_WITH_DESC) fake_resp = utils.FakeResponse({'content-type': 'application/json'}, io.StringIO(error_body), version=1, status=500) client = http.HTTPClient('http://localhost/') client.get_connection = ( lambda *a, **kw: utils.FakeConnection(fake_resp)) error = self.assertRaises(exc.InternalServerError, client.json_request, 'GET', '/v1/resources') self.assertEqual( '%(error)s (HTTP 500)\n%(trace)s' % {'error': error_msg, 'trace': error_trace}, "%(error)s\n%(details)s" % {'error': str(error), 'details': str(error.details)}) def test_server_exception_address(self): endpoint = 'https://magnum-host:6385' client = http.HTTPClient(endpoint, token='foobar', insecure=True, ca_file='/path/to/ca_file') client.get_connection = ( lambda *a, **kw: utils.FakeConnection(exc=socket.gaierror)) self.assertRaises(exc.EndpointNotFound, client.json_request, 'GET', '/v1/resources', body='farboo') def test_server_exception_socket(self): client = http.HTTPClient('http://localhost/', token='foobar') client.get_connection = ( lambda *a, **kw: utils.FakeConnection(exc=socket.error)) self.assertRaises(exc.ConnectionRefused, client.json_request, 'GET', '/v1/resources') def test_server_exception_endpoint(self): endpoint = 'https://magnum-host:6385' client = http.HTTPClient(endpoint, token='foobar', insecure=True, ca_file='/path/to/ca_file') client.get_connection = ( lambda *a, **kw: utils.FakeConnection(exc=socket.gaierror)) self.assertRaises(exc.EndpointNotFound, client.json_request, 'GET', '/v1/resources', body='farboo') def test_get_connection(self): endpoint = 'https://magnum-host:6385' client = http.HTTPClient(endpoint) conn = client.get_connection() self.assertTrue(conn, http.VerifiedHTTPSConnection) def test_get_connection_exception(self): endpoint = 'http://magnum-host:6385/' expected = (HTTP_CLASS, ('magnum-host', 6385, ''), {'timeout': DEFAULT_TIMEOUT}) params = http.HTTPClient.get_connection_params(endpoint) self.assertEqual(expected, params) def test_get_connection_params_with_ssl(self): endpoint = 'https://magnum-host:6385' expected = (HTTPS_CLASS, ('magnum-host', 6385, ''), { 'timeout': DEFAULT_TIMEOUT, 'ca_file': None, 'cert_file': None, 'key_file': None, 'insecure': False, }) params = http.HTTPClient.get_connection_params(endpoint) self.assertEqual(expected, params) def test_get_connection_params_with_ssl_params(self): endpoint = 'https://magnum-host:6385' ssl_args = { 'ca_file': '/path/to/ca_file', 'cert_file': '/path/to/cert_file', 'key_file': '/path/to/key_file', 'insecure': True, } expected_kwargs = {'timeout': DEFAULT_TIMEOUT} expected_kwargs.update(ssl_args) expected = (HTTPS_CLASS, ('magnum-host', 6385, ''), expected_kwargs) params = http.HTTPClient.get_connection_params(endpoint, **ssl_args) self.assertEqual(expected, params) def test_get_connection_params_with_timeout(self): endpoint = 'http://magnum-host:6385' expected = (HTTP_CLASS, ('magnum-host', 6385, ''), {'timeout': 300.0}) params = http.HTTPClient.get_connection_params(endpoint, timeout=300) self.assertEqual(expected, params) def test_get_connection_params_with_version(self): endpoint = 'http://magnum-host:6385/v1' expected = (HTTP_CLASS, ('magnum-host', 6385, ''), {'timeout': DEFAULT_TIMEOUT}) params = http.HTTPClient.get_connection_params(endpoint) self.assertEqual(expected, params) def test_get_connection_params_with_version_trailing_slash(self): endpoint = 'http://magnum-host:6385/v1/' expected = (HTTP_CLASS, ('magnum-host', 6385, ''), {'timeout': DEFAULT_TIMEOUT}) params = http.HTTPClient.get_connection_params(endpoint) self.assertEqual(expected, params) def test_get_connection_params_with_subpath(self): endpoint = 'http://magnum-host:6385/magnum' expected = (HTTP_CLASS, ('magnum-host', 6385, '/magnum'), {'timeout': DEFAULT_TIMEOUT}) params = http.HTTPClient.get_connection_params(endpoint) self.assertEqual(expected, params) def test_get_connection_params_with_subpath_trailing_slash(self): endpoint = 'http://magnum-host:6385/magnum/' expected = (HTTP_CLASS, ('magnum-host', 6385, '/magnum'), {'timeout': DEFAULT_TIMEOUT}) params = http.HTTPClient.get_connection_params(endpoint) self.assertEqual(expected, params) def test_get_connection_params_with_subpath_version(self): endpoint = 'http://magnum-host:6385/magnum/v1' expected = (HTTP_CLASS, ('magnum-host', 6385, '/magnum'), {'timeout': DEFAULT_TIMEOUT}) params = http.HTTPClient.get_connection_params(endpoint) self.assertEqual(expected, params) def test_get_connection_params_with_subpath_version_trailing_slash(self): endpoint = 'http://magnum-host:6385/magnum/v1/' expected = (HTTP_CLASS, ('magnum-host', 6385, '/magnum'), {'timeout': DEFAULT_TIMEOUT}) params = http.HTTPClient.get_connection_params(endpoint) self.assertEqual(expected, params) def test_get_connection_params_with_unsupported_scheme(self): endpoint = 'foo://magnum-host:6385/magnum/v1/' self.assertRaises(exc.EndpointException, http.HTTPClient.get_connection_params, endpoint) def test_401_unauthorized_exception(self): error_body = _get_error_body(err_type=ERROR_LIST_WITH_DETAIL) fake_resp = utils.FakeResponse({'content-type': 'text/plain'}, io.StringIO(error_body), version=1, status=401) client = http.HTTPClient('http://localhost/') client.get_connection = (lambda *a, **kw: utils.FakeConnection(fake_resp)) self.assertRaises(exc.Unauthorized, client.json_request, 'GET', '/v1/resources') def test_server_redirect_exception(self): fake_redirect_resp = utils.FakeResponse( {'content-type': 'application/octet-stream'}, 'foo', version=1, status=301) fake_resp = utils.FakeResponse( {'content-type': 'application/octet-stream'}, 'bar', version=1, status=300) client = http.HTTPClient('http://localhost/') conn = utils.FakeConnection(fake_redirect_resp, redirect_resp=fake_resp) client.get_connection = (lambda *a, **kw: conn) self.assertRaises(MultipleChoices, client.json_request, 'GET', '/v1/resources') def test_server_body_undecode_json(self): err = "foo" fake_resp = utils.FakeResponse( {'content-type': 'application/json'}, io.StringIO(err), version=1, status=200) client = http.HTTPClient('http://localhost/') conn = utils.FakeConnection(fake_resp) client.get_connection = (lambda *a, **kw: conn) resp, body = client.json_request('GET', '/v1/resources') self.assertEqual(resp, fake_resp) self.assertEqual(err, body) def test_server_success_body_app(self): fake_resp = utils.FakeResponse( {'content-type': 'application/octet-stream'}, 'bar', version=1, status=200) client = http.HTTPClient('http://localhost/') conn = utils.FakeConnection(fake_resp) client.get_connection = (lambda *a, **kw: conn) resp, body = client.json_request('GET', '/v1/resources') self.assertEqual(resp, fake_resp) self.assertIsNone(body) def test_server_success_body_none(self): fake_resp = utils.FakeResponse( {'content-type': None}, io.StringIO('bar'), version=1, status=200) client = http.HTTPClient('http://localhost/') conn = utils.FakeConnection(fake_resp) client.get_connection = (lambda *a, **kw: conn) resp, body = client.json_request('GET', '/v1/resources') self.assertEqual(resp, fake_resp) self.assertIsInstance(body, list) def test_server_success_body_json(self): err = _get_error_body() fake_resp = utils.FakeResponse( {'content-type': 'application/json'}, io.StringIO(err), version=1, status=200) client = http.HTTPClient('http://localhost/') conn = utils.FakeConnection(fake_resp) client.get_connection = (lambda *a, **kw: conn) resp, body = client.json_request('GET', '/v1/resources') self.assertEqual(resp, fake_resp) self.assertEqual(jsonutils.dumps(body), err) def test_raw_request(self): fake_resp = utils.FakeResponse( {'content-type': 'application/octet-stream'}, 'bar', version=1, status=200) client = http.HTTPClient('http://localhost/') conn = utils.FakeConnection(fake_resp) client.get_connection = (lambda *a, **kw: conn) resp, body = client.raw_request('GET', '/v1/resources') self.assertEqual(resp, fake_resp) self.assertIsInstance(body, http.ResponseBodyIterator) class SessionClientTest(utils.BaseTestCase): def test_server_exception_msg_and_traceback(self): error_msg = 'another test error' error_trace = ("\"Traceback (most recent call last):\\n\\n " "File \\\"/usr/local/lib/python2.7/...") error_body = _get_error_body(error_msg, error_trace) fake_session = utils.FakeSession({'Content-Type': 'application/json'}, error_body, 500) client = http.SessionClient(session=fake_session) error = self.assertRaises(exc.InternalServerError, client.json_request, 'GET', '/v1/resources') self.assertEqual( '%(error)s (HTTP 500)\n%(trace)s' % {'error': error_msg, 'trace': error_trace}, "%(error)s\n%(details)s" % {'error': str(error), 'details': str(error.details)}) def test_server_exception_empty_body(self): error_body = _get_error_body() fake_session = utils.FakeSession({'Content-Type': 'application/json'}, error_body, 500) client = http.SessionClient(session=fake_session) error = self.assertRaises(exc.InternalServerError, client.json_request, 'GET', '/v1/resources') self.assertEqual('Internal Server Error (HTTP 500)', str(error)) def test_bypass_url(self): fake_response = utils.FakeSessionResponse( {}, content="", status_code=201) fake_session = mock.MagicMock() fake_session.request.side_effect = [fake_response] client = http.SessionClient( session=fake_session, endpoint_override='http://magnum') client.json_request('GET', '/v1/clusters') self.assertEqual( fake_session.request.call_args[1]['endpoint_override'], 'http://magnum' ) def test_exception(self): fake_response = utils.FakeSessionResponse( {}, content="", status_code=504) fake_session = mock.MagicMock() fake_session.request.side_effect = [fake_response] client = http.SessionClient( session=fake_session, endpoint_override='http://magnum') self.assertRaises(GatewayTimeout, client.json_request, 'GET', '/v1/resources') def test_construct_http_client_return_httpclient(self): client = http._construct_http_client('http://localhost/') self.assertIsInstance(client, http.HTTPClient) def test_construct_http_client_return_sessionclient(self): fake_session = mock.MagicMock() client = http._construct_http_client(session=fake_session) self.assertIsInstance(client, http.SessionClient) def test_raw_request(self): fake_response = utils.FakeSessionResponse( {'content-type': 'application/octet-stream'}, content="", status_code=200) fake_session = mock.MagicMock() fake_session.request.side_effect = [fake_response] client = http.SessionClient( session=fake_session, endpoint_override='http://magnum') resp, resp_body = client.raw_request('GET', '/v1/clusters') self.assertEqual( fake_session.request.call_args[1]['headers']['Content-Type'], 'application/octet-stream' ) self.assertEqual(None, resp_body) self.assertEqual(fake_response, resp) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1334193 python_magnumclient-5.0.0/magnumclient/tests/test_magnumclient.py0000664000175000017500000000142315244246676024363 0ustar00zuulzuul# -*- 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. """ test_magnumclient ---------------------------------- Tests for `magnumclient` module. """ from magnumclient.tests import base class TestMagnumclient(base.TestCase): def test_something(self): pass ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1334193 python_magnumclient-5.0.0/magnumclient/tests/test_utils.py0000664000175000017500000003020315244246676023036 0ustar00zuulzuul# -*- coding: utf-8 -*- # # Copyright 2013 OpenStack LLC. # 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 builtins import collections from unittest import mock from oslo_serialization import jsonutils import tempfile from magnumclient.common import cliutils from magnumclient.common import utils from magnumclient import exceptions as exc from magnumclient.tests import utils as test_utils class CommonFiltersTest(test_utils.BaseTestCase): def test_limit(self): result = utils.common_filters(limit=42) self.assertEqual(['limit=42'], result) def test_limit_0(self): result = utils.common_filters(limit=0) self.assertEqual(['limit=0'], result) def test_limit_negative_number(self): result = utils.common_filters(limit=-2) self.assertEqual(['limit=-2'], result) def test_other(self): for key in ('marker', 'sort_key', 'sort_dir'): result = utils.common_filters(**{key: 'test'}) self.assertEqual(['%s=test' % key], result) class SplitAndDeserializeTest(test_utils.BaseTestCase): def test_split_and_deserialize(self): ret = utils.split_and_deserialize('str=foo') self.assertEqual(('str', 'foo'), ret) ret = utils.split_and_deserialize('int=1') self.assertEqual(('int', 1), ret) ret = utils.split_and_deserialize('bool=false') self.assertEqual(('bool', False), ret) ret = utils.split_and_deserialize('list=[1, "foo", 2]') self.assertEqual(('list', [1, "foo", 2]), ret) ret = utils.split_and_deserialize('dict={"foo": 1}') self.assertEqual(('dict', {"foo": 1}), ret) ret = utils.split_and_deserialize('str_int="1"') self.assertEqual(('str_int', "1"), ret) def test_split_and_deserialize_fail(self): self.assertRaises(exc.CommandError, utils.split_and_deserialize, 'foo:bar') class ArgsArrayToPatchTest(test_utils.BaseTestCase): def test_args_array_to_patch(self): my_args = { 'attributes': ['str=foo', 'int=1', 'bool=true', 'list=[1, 2, 3]', 'dict={"foo": "bar"}'], 'op': 'add', } patch = utils.args_array_to_patch(my_args['op'], my_args['attributes']) self.assertEqual([{'op': 'add', 'value': 'foo', 'path': '/str'}, {'op': 'add', 'value': 1, 'path': '/int'}, {'op': 'add', 'value': True, 'path': '/bool'}, {'op': 'add', 'value': [1, 2, 3], 'path': '/list'}, {'op': 'add', 'value': {"foo": "bar"}, 'path': '/dict'}], patch) def test_args_array_to_patch_format_error(self): my_args = { 'attributes': ['foobar'], 'op': 'add', } self.assertRaises(exc.CommandError, utils.args_array_to_patch, my_args['op'], my_args['attributes']) def test_args_array_to_patch_remove(self): my_args = { 'attributes': ['/foo', 'extra/bar'], 'op': 'remove', } patch = utils.args_array_to_patch(my_args['op'], my_args['attributes']) self.assertEqual([{'op': 'remove', 'path': '/foo'}, {'op': 'remove', 'path': '/extra/bar'}], patch) def test_args_array_to_patch_invalid_op(self): my_args = { 'attributes': ['/foo', 'extra/bar'], 'op': 'invalid', } self.assertRaises(exc.CommandError, utils.args_array_to_patch, my_args['op'], my_args['attributes']) class FormatLabelsTest(test_utils.BaseTestCase): def test_format_label_none(self): self.assertEqual({}, utils.format_labels(None)) def test_format_labels(self): la = utils.format_labels([ 'K1=V1,K2=V2,' 'K3=V3,K4=V4,' 'K5=V5']) self.assertEqual({'K1': 'V1', 'K2': 'V2', 'K3': 'V3', 'K4': 'V4', 'K5': 'V5' }, la) def test_format_labels_semicolon(self): la = utils.format_labels([ 'K1=V1;K2=V2;' 'K3=V3;K4=V4;' 'K5=V5']) self.assertEqual({'K1': 'V1', 'K2': 'V2', 'K3': 'V3', 'K4': 'V4', 'K5': 'V5' }, la) def test_format_labels_mix_commas_semicolon(self): la = utils.format_labels([ 'K1=V1,K2=V2,' 'K3=V3;K4=V4,' 'K5=V5']) self.assertEqual({'K1': 'V1', 'K2': 'V2', 'K3': 'V3', 'K4': 'V4', 'K5': 'V5' }, la) def test_format_labels_split(self): la = utils.format_labels([ 'K1=V1,' 'K2=V22222222222222222222222222222' '222222222222222222222222222,' 'K3=3.3.3.3']) self.assertEqual({'K1': 'V1', 'K2': 'V22222222222222222222222222222' '222222222222222222222222222', 'K3': '3.3.3.3'}, la) def test_format_labels_multiple(self): la = utils.format_labels([ 'K1=V1', 'K2=V22222222222222222222222222222' '222222222222222222222222222', 'K3=3.3.3.3']) self.assertEqual({'K1': 'V1', 'K2': 'V22222222222222222222222222222' '222222222222222222222222222', 'K3': '3.3.3.3'}, la) def test_format_labels_multiple_colon_values(self): la = utils.format_labels([ 'K1=V1', 'K2=V2,V22,V222,V2222', 'K3=3.3.3.3']) self.assertEqual({'K1': 'V1', 'K2': 'V2,V22,V222,V2222', 'K3': '3.3.3.3'}, la) def test_format_labels_parse_comma_false(self): la = utils.format_labels( ['K1=V1,K2=2.2.2.2,K=V'], parse_comma=False) self.assertEqual({'K1': 'V1,K2=2.2.2.2,K=V'}, la) def test_format_labels_multiple_values_per_labels(self): la = utils.format_labels([ 'K1=V1', 'K1=V2']) self.assertEqual({'K1': 'V1,V2'}, la) def test_format_label_special_label(self): labels = ['K1=V1,K22.2.2.2'] la = utils.format_labels( labels, parse_comma=True) self.assertEqual({'K1': 'V1,K22.2.2.2'}, la) def test_format_multiple_bad_label(self): labels = ['K1=V1', 'K22.2.2.2'] ex = self.assertRaises(exc.CommandError, utils.format_labels, labels) self.assertEqual('labels must be a list of KEY=VALUE ' 'not K22.2.2.2', str(ex)) class HandleTaintsTest(test_utils.BaseTestCase): def test_none(self): self.assertEqual([], utils.handle_taints(None)) def test_empty_list(self): self.assertEqual([], utils.handle_taints([])) def test_single_arg_comma_separated(self): ts = utils.handle_taints([ 'gpu=true:NoSchedule,dedicated=:NoExecute', ]) self.assertEqual([ {'key': 'gpu', 'value': 'true', 'effect': 'NoSchedule'}, {'key': 'dedicated', 'value': '', 'effect': 'NoExecute'}, ], ts) def test_value_optional(self): ts = utils.handle_taints(['nectar.org.au/unreachable:NoSchedule']) self.assertEqual([ {'key': 'nectar.org.au/unreachable', 'value': '', 'effect': 'NoSchedule'}, ], ts) def test_mixed_value_and_no_value(self): ts = utils.handle_taints([ 'gpu=true:NoSchedule,unreachable:NoExecute', ]) self.assertEqual([ {'key': 'gpu', 'value': 'true', 'effect': 'NoSchedule'}, {'key': 'unreachable', 'value': '', 'effect': 'NoExecute'}, ], ts) def test_repeated_flag(self): ts = utils.handle_taints([ 'gpu=true:NoSchedule', 'reserved=ops:PreferNoSchedule', ]) self.assertEqual([ {'key': 'gpu', 'value': 'true', 'effect': 'NoSchedule'}, {'key': 'reserved', 'value': 'ops', 'effect': 'PreferNoSchedule'}, ], ts) def test_semicolon_separator(self): ts = utils.handle_taints([ 'a=1:NoSchedule;b=2:NoExecute', ]) self.assertEqual([ {'key': 'a', 'value': '1', 'effect': 'NoSchedule'}, {'key': 'b', 'value': '2', 'effect': 'NoExecute'}, ], ts) def test_invalid_effect(self): ex = self.assertRaises(exc.CommandError, utils.handle_taints, ['gpu=true:Bogus']) self.assertIn('Bogus', str(ex)) def test_missing_key(self): ex = self.assertRaises(exc.CommandError, utils.handle_taints, ['=true:NoSchedule']) self.assertIn('=true:NoSchedule', str(ex)) def test_no_effect_separator(self): ex = self.assertRaises(exc.CommandError, utils.handle_taints, ['gpu=true']) self.assertIn('gpu=true', str(ex)) class CliUtilsTest(test_utils.BaseTestCase): def test_keys_and_vals_to_strs(self): dict_in = {'a': '1', 'b': {'x': 1, 'y': '2', 'z': '3'}, 'c': 7} dict_exp = collections.OrderedDict([ ('a', '1'), ('b', collections.OrderedDict([ ('x', 1), ('y', '2'), ('z', '3')])), ('c', 7)]) dict_out = cliutils.keys_and_vals_to_strs(dict_in) dict_act = collections.OrderedDict([ ('a', dict_out['a']), ('b', collections.OrderedDict(sorted(dict_out['b'].items()))), ('c', dict_out['c'])]) self.assertEqual(str(dict_exp), str(dict_act)) class HandleJsonFromFileTest(test_utils.BaseTestCase): def test_handle_json_from_file_bad_json(self): contents = 'foo' with tempfile.NamedTemporaryFile(mode='w') as f: f.write(contents) f.flush() self.assertRaisesRegex(exc.InvalidAttribute, 'For JSON', utils.handle_json_from_file, f.name) def test_handle_json_from_file_valid_file(self): contents = '{"step": "upgrade", "interface": "deploy"}' with tempfile.NamedTemporaryFile(mode='w') as f: f.write(contents) f.flush() steps = utils.handle_json_from_file(f.name) self.assertEqual(jsonutils.loads(contents), steps) @mock.patch.object(builtins, 'open', autospec=True) def test_handle_json_from_file_open_fail(self, mock_open): mock_file_object = mock.MagicMock() mock_file_handle = mock.MagicMock() mock_file_handle.__enter__.return_value = mock_file_object mock_open.return_value = mock_file_handle mock_file_object.read.side_effect = IOError with tempfile.NamedTemporaryFile(mode='w') as f: self.assertRaisesRegex(exc.InvalidAttribute, "from file", utils.handle_json_from_file, f.name) mock_open.assert_called_once_with(f.name, 'r') mock_file_object.read.assert_called_once_with() ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1334193 python_magnumclient-5.0.0/magnumclient/tests/utils.py0000664000175000017500000001113615244246676022003 0ustar00zuulzuul# Copyright 2012 OpenStack LLC. # 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 datetime import io import queue import fixtures from oslo_serialization import jsonutils from oslo_utils import timeutils import testtools from magnumclient.common import httpclient as http FAKE_ENV = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_PROJECT_NAME': 'project_name', 'OS_AUTH_URL': 'http://no.where/v2.0'} class BaseTestCase(testtools.TestCase): def setUp(self): super(BaseTestCase, self).setUp() self.useFixture(fixtures.FakeLogger()) class FakeAPI(object): def __init__(self, responses): self.responses = responses self.calls = [] def _request(self, method, url, headers=None, body=None): call = (method, url, headers or {}, body) self.calls.append(call) return self.responses[url][method] def raw_request(self, *args, **kwargs): response = self._request(*args, **kwargs) body_iter = http.ResponseBodyIterator(io.StringIO(response[1])) return FakeResponse(response[0]), body_iter def json_request(self, *args, **kwargs): response = self._request(*args, **kwargs) return FakeResponse(response[0]), response[1] class FakeConnection(object): def __init__(self, response=None, **kwargs): self._response = queue.Queue() self._response.put(response) self._last_request = None self._exc = kwargs['exc'] if 'exc' in kwargs else None if 'redirect_resp' in kwargs: self._response.put(kwargs['redirect_resp']) def request(self, method, conn_url, **kwargs): self._last_request = (method, conn_url, kwargs) if self._exc: raise self._exc def setresponse(self, response): self._response = response def getresponse(self): return self._response.get() class FakeResponse(object): def __init__(self, headers, body=None, version=None, status=None, reason=None): """Fake object to help testing. :param headers: dict representing HTTP response headers :param body: file-like object """ self.headers = headers self.body = body self.version = version self.status = status self.reason = reason def __getitem__(self, key): if key == 'location': return 'fake_url' else: return None 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 FakeServiceCatalog(object): def url_for(self, endpoint_type, service_type, attr=None, filter_value=None): if attr == 'region' and filter_value: return 'http://regionhost:6385/v1/f14b41234' else: return 'http://localhost:6385/v1/f14b41234' class FakeKeystone(object): service_catalog = FakeServiceCatalog() timestamp = timeutils.utcnow() + datetime.timedelta(days=5) def __init__(self, auth_token): self.auth_token = auth_token self.auth_ref = { 'token': {'expires': FakeKeystone.timestamp.strftime( '%Y-%m-%dT%H:%M:%S.%f'), 'id': 'd1a541311782870742235'} } class FakeSessionResponse(object): def __init__(self, headers, content=None, status_code=None): self.headers = headers self.content = content self.status_code = status_code def json(self): if self.content is not None: return jsonutils.loads(self.content) else: return {} class FakeSession(object): def __init__(self, headers, content=None, status_code=None): self.headers = headers self.content = content self.status_code = status_code def request(self, url, method, **kwargs): return FakeSessionResponse(self.headers, self.content, self.status_code) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0045784 python_magnumclient-5.0.0/magnumclient/tests/v1/0000775000175000017500000000000015244246764020613 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1334193 python_magnumclient-5.0.0/magnumclient/tests/v1/__init__.py0000664000175000017500000000000015244246676022714 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1334193 python_magnumclient-5.0.0/magnumclient/tests/v1/test_certificates.py0000664000175000017500000000574315244246676024704 0ustar00zuulzuul# Copyright 2015 IBM Corp. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import testtools from magnumclient import exceptions from magnumclient.tests import utils from magnumclient.v1 import certificates CERT1 = { 'cluster_uuid': '5d12f6fd-a196-4bf0-ae4c-1f639a523a53', 'pem': 'fake-pem' } CERT2 = { 'cluster_uuid': '5d12f6fd-a196-4bf0-ae4c-1f639a523a53', 'pem': 'fake-pem', 'csr': 'fake-csr', } CREATE_CERT = {'cluster_uuid': '5d12f6fd-a196-4bf0-ae4c-1f639a523a53', 'csr': 'fake-csr'} fake_responses = { '/v1/certificates': { 'POST': ( {}, CERT2, ) }, '/v1/certificates/%s' % CERT1['cluster_uuid']: { 'GET': ( {}, CERT1 ), 'PATCH': ( {}, None, ) } } class CertificateManagerTest(testtools.TestCase): def setUp(self): super(CertificateManagerTest, self).setUp() self.api = utils.FakeAPI(fake_responses) self.mgr = certificates.CertificateManager(self.api) def test_cert_show_by_id(self): cert = self.mgr.get(CERT1['cluster_uuid']) expect = [ ('GET', '/v1/certificates/%s' % CERT1['cluster_uuid'], {}, None) ] self.assertEqual(expect, self.api.calls) self.assertEqual(CERT1['cluster_uuid'], cert.cluster_uuid) self.assertEqual(CERT1['pem'], cert.pem) def test_cert_create(self): cert = self.mgr.create(**CREATE_CERT) expect = [ ('POST', '/v1/certificates', {}, CREATE_CERT), ] self.assertEqual(expect, self.api.calls) self.assertEqual(CERT2['cluster_uuid'], cert.cluster_uuid) self.assertEqual(CERT2['pem'], cert.pem) self.assertEqual(CERT2['csr'], cert.csr) def test_create_fail(self): create_cert_fail = copy.deepcopy(CREATE_CERT) create_cert_fail["wrong_key"] = "wrong" self.assertRaisesRegex(exceptions.InvalidAttribute, ("Key must be in %s" % ','.join(certificates.CREATION_ATTRIBUTES)), self.mgr.create, **create_cert_fail) self.assertEqual([], self.api.calls) def test_rotate_ca(self): self.mgr.rotate_ca(cluster_uuid=CERT1['cluster_uuid']) expect = [ ('PATCH', '/v1/certificates/%s' % CERT1['cluster_uuid'], {}, None) ] self.assertEqual(expect, self.api.calls) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1344194 python_magnumclient-5.0.0/magnumclient/tests/v1/test_client.py0000664000175000017500000001612215244246676023506 0ustar00zuulzuul# Copyright (c) 2015 Thales Services SAS # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 magnumclient.v1 import client class ClientInitializeTest(testtools.TestCase): def _load_session_kwargs(self): return { 'username': None, 'project_id': None, 'project_name': None, 'auth_url': None, 'password': None, 'auth_type': 'password', 'insecure': False, 'user_domain_id': None, 'user_domain_name': None, 'project_domain_id': None, 'project_domain_name': None, 'auth_token': None, 'timeout': 600, } def _load_service_type_kwargs(self): return { 'interface': 'public', 'region_name': None, 'service_name': None, 'service_type': 'container-infra', } def _session_client_kwargs(self, session): kwargs = self._load_service_type_kwargs() kwargs['endpoint_override'] = None kwargs['session'] = session kwargs['api_version'] = None return kwargs @mock.patch('magnumclient.common.httpclient.SessionClient') @mock.patch('magnumclient.v1.client._load_session') @mock.patch('magnumclient.v1.client._load_service_type', return_value='container-infra') def test_init_with_session(self, mock_load_service_type, mock_load_session, mock_http_client): session = mock.Mock() client.Client(session=session) mock_load_session.assert_not_called() mock_load_service_type.assert_called_once_with( session, **self._load_service_type_kwargs() ) mock_http_client.assert_called_once_with( **self._session_client_kwargs(session) ) def _test_init_with_secret(self, init_func, mock_load_service_type, mock_load_session, mock_http_client,): expected_password = 'expected_password' session = mock.Mock() mock_load_session.return_value = session init_func(expected_password) load_session_args = self._load_session_kwargs() load_session_args['password'] = expected_password mock_load_session.assert_called_once_with( **load_session_args ) mock_load_service_type.assert_called_once_with( session, **self._load_service_type_kwargs() ) mock_http_client.assert_called_once_with( **self._session_client_kwargs(session) ) @mock.patch('magnumclient.common.httpclient.SessionClient') @mock.patch('magnumclient.v1.client._load_session') @mock.patch('magnumclient.v1.client._load_service_type', return_value='container-infra') def test_init_with_password(self, mock_load_service_type, mock_load_session, mock_http_client): self._test_init_with_secret( lambda x: client.Client(password=x), mock_load_service_type, mock_load_session, mock_http_client ) @mock.patch('magnumclient.common.httpclient.HTTPClient') def test_init_with_auth_token(self, mock_http_client,): expected_token = 'expected_password' expected_magnum_url = 'expected_magnum_url' expected_api_version = 'expected_api_version' expected_insecure = False expected_timeout = 600 expected_kwargs = {'expected_key': 'expected_value'} client.Client(auth_token=expected_token, magnum_url=expected_magnum_url, api_version=expected_api_version, timeout=expected_timeout, insecure=expected_insecure, **expected_kwargs) mock_http_client.assert_called_once_with( expected_magnum_url, token=expected_token, api_version=expected_api_version, timeout=expected_timeout, insecure=expected_insecure, **expected_kwargs) def _test_init_with_interface(self, init_func, mock_load_service_type, mock_load_session, mock_http_client): expected_interface = 'admin' session = mock.Mock() mock_load_session.return_value = session init_func(expected_interface) mock_load_session.assert_called_once_with( **self._load_session_kwargs() ) expected_kwargs = self._load_service_type_kwargs() expected_kwargs['interface'] = expected_interface mock_load_service_type.assert_called_once_with( session, **expected_kwargs ) expected_kwargs = self._session_client_kwargs(session) expected_kwargs['interface'] = expected_interface mock_http_client.assert_called_once_with( **expected_kwargs ) @mock.patch('magnumclient.common.httpclient.SessionClient') @mock.patch('magnumclient.v1.client._load_session') @mock.patch('magnumclient.v1.client._load_service_type', return_value='container-infra') def test_init_with_interface(self, mock_load_service_type, mock_load_session, mock_http_client): self._test_init_with_interface( lambda x: client.Client(interface=x), mock_load_service_type, mock_load_session, mock_http_client ) @mock.patch('magnumclient.common.httpclient.SessionClient') @mock.patch('magnumclient.v1.client._load_session') @mock.patch('magnumclient.v1.client._load_service_type', return_value='container-infra') def test_init_with_endpoint_type(self, mock_load_service_type, mock_load_session, mock_http_client): self._test_init_with_interface( lambda x: client.Client(interface='public', endpoint_type=('%sURL' % x)), mock_load_service_type, mock_load_session, mock_http_client ) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1344194 python_magnumclient-5.0.0/magnumclient/tests/v1/test_clusters.py0000664000175000017500000003114015244246676024071 0ustar00zuulzuul# Copyright 2015 IBM Corp # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import testtools from testtools import matchers from magnumclient import exceptions from magnumclient.tests import utils from magnumclient.v1 import clusters CLUSTER1 = {'id': 123, 'uuid': '66666666-7777-8888-9999-000000000001', 'name': 'cluster1', 'cluster_template_id': 'e74c40e0-d825-11e2-a28f-0800200c9a61', 'cluster_id': '5d12f6fd-a196-4bf0-ae4c-1f639a523a51', 'api_address': '172.17.2.1', 'node_addresses': ['172.17.2.3'], 'node_count': 2, 'master_count': 1, } CLUSTER2 = {'id': 124, 'uuid': '66666666-7777-8888-9999-000000000002', 'name': 'cluster2', 'cluster_template_id': 'e74c40e0-d825-11e2-a28f-0800200c9a62', 'cluster_id': '5d12f6fd-a196-4bf0-ae4c-1f639a523a52', 'api_address': '172.17.2.2', 'node_addresses': ['172.17.2.4'], 'node_count': 2, 'master_count': 1, } CREATE_CLUSTER = copy.deepcopy(CLUSTER1) del CREATE_CLUSTER['id'] del CREATE_CLUSTER['uuid'] del CREATE_CLUSTER['cluster_id'] del CREATE_CLUSTER['api_address'] del CREATE_CLUSTER['node_addresses'] UPDATED_CLUSTER = copy.deepcopy(CLUSTER1) NEW_NAME = 'newcluster' UPDATED_CLUSTER['name'] = NEW_NAME RESIZED_CLUSTER = copy.deepcopy(CLUSTER1) RESIZED_NODE_COUNT = 5 UPDATED_CLUSTER['node_count'] = RESIZED_NODE_COUNT UPGRADED_CLUSTER = copy.deepcopy(CLUSTER1) UPGRADED_TO_TEMPLATE = "eabbc463-0d3f-49dc-8519-cb6b59507bd6" UPGRADED_CLUSTER['cluster_template_id'] = UPGRADED_TO_TEMPLATE fake_responses = { '/v1/clusters': { 'GET': ( {}, {'clusters': [CLUSTER1, CLUSTER2]}, ), 'POST': ( {}, CREATE_CLUSTER, ), }, '/v1/clusters/%s' % CLUSTER1['id']: { 'GET': ( {}, CLUSTER1 ), 'DELETE': ( {}, None, ), 'PATCH': ( {}, UPDATED_CLUSTER, ), }, '/v1/clusters/%s/?rollback=True' % CLUSTER1['id']: { 'PATCH': ( {}, UPDATED_CLUSTER, ), }, '/v1/clusters/%s' % CLUSTER1['name']: { 'GET': ( {}, CLUSTER1 ), 'DELETE': ( {}, None, ), 'PATCH': ( {}, UPDATED_CLUSTER, ), }, '/v1/clusters/?limit=2': { 'GET': ( {}, {'clusters': [CLUSTER1, CLUSTER2]}, ), }, '/v1/clusters/?marker=%s' % CLUSTER2['uuid']: { 'GET': ( {}, {'clusters': [CLUSTER1, CLUSTER2]}, ), }, '/v1/clusters/?limit=2&marker=%s' % CLUSTER2['uuid']: { 'GET': ( {}, {'clusters': [CLUSTER1, CLUSTER2]}, ), }, '/v1/clusters/?sort_dir=asc': { 'GET': ( {}, {'clusters': [CLUSTER1, CLUSTER2]}, ), }, '/v1/clusters/?sort_key=uuid': { 'GET': ( {}, {'clusters': [CLUSTER1, CLUSTER2]}, ), }, '/v1/clusters/?sort_key=uuid&sort_dir=desc': { 'GET': ( {}, {'clusters': [CLUSTER2, CLUSTER1]}, ), }, '/v1/clusters/%s/actions/resize' % CLUSTER1['uuid']: { 'POST': ( {}, UPDATED_CLUSTER ), }, '/v1/clusters/%s/actions/upgrade' % CLUSTER1['uuid']: { 'POST': ( {}, UPGRADED_CLUSTER ), } } class ClusterManagerTest(testtools.TestCase): def setUp(self): super(ClusterManagerTest, self).setUp() self.api = utils.FakeAPI(fake_responses) self.mgr = clusters.ClusterManager(self.api) def test_cluster_list(self): clusters = self.mgr.list() expect = [ ('GET', '/v1/clusters', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertThat(clusters, matchers.HasLength(2)) def _test_cluster_list_with_filters(self, limit=None, marker=None, sort_key=None, sort_dir=None, detail=False, expect=[]): clusters_filter = self.mgr.list(limit=limit, marker=marker, sort_key=sort_key, sort_dir=sort_dir, detail=detail) self.assertEqual(expect, self.api.calls) self.assertThat(clusters_filter, matchers.HasLength(2)) def test_cluster_list_with_limit(self): expect = [ ('GET', '/v1/clusters/?limit=2', {}, None), ] self._test_cluster_list_with_filters( limit=2, expect=expect) def test_cluster_list_with_marker(self): expect = [ ('GET', '/v1/clusters/?marker=%s' % CLUSTER2['uuid'], {}, None), ] self._test_cluster_list_with_filters( marker=CLUSTER2['uuid'], expect=expect) def test_cluster_list_with_marker_limit(self): expect = [ ('GET', '/v1/clusters/?limit=2&marker=%s' % CLUSTER2['uuid'], {}, None), ] self._test_cluster_list_with_filters( limit=2, marker=CLUSTER2['uuid'], expect=expect) def test_cluster_list_with_sort_dir(self): expect = [ ('GET', '/v1/clusters/?sort_dir=asc', {}, None), ] self._test_cluster_list_with_filters( sort_dir='asc', expect=expect) def test_cluster_list_with_sort_key(self): expect = [ ('GET', '/v1/clusters/?sort_key=uuid', {}, None), ] self._test_cluster_list_with_filters( sort_key='uuid', expect=expect) def test_cluster_list_with_sort_key_dir(self): expect = [ ('GET', '/v1/clusters/?sort_key=uuid&sort_dir=desc', {}, None), ] self._test_cluster_list_with_filters( sort_key='uuid', sort_dir='desc', expect=expect) def test_cluster_show_by_id(self): cluster = self.mgr.get(CLUSTER1['id']) expect = [ ('GET', '/v1/clusters/%s' % CLUSTER1['id'], {}, None) ] self.assertEqual(expect, self.api.calls) self.assertEqual(CLUSTER1['name'], cluster.name) self.assertEqual(CLUSTER1['cluster_template_id'], cluster.cluster_template_id) def test_cluster_show_by_name(self): cluster = self.mgr.get(CLUSTER1['name']) expect = [ ('GET', '/v1/clusters/%s' % CLUSTER1['name'], {}, None) ] self.assertEqual(expect, self.api.calls) self.assertEqual(CLUSTER1['name'], cluster.name) self.assertEqual(CLUSTER1['cluster_template_id'], cluster.cluster_template_id) def test_cluster_create(self): cluster = self.mgr.create(**CREATE_CLUSTER) expect = [ ('POST', '/v1/clusters', {}, CREATE_CLUSTER), ] self.assertEqual(expect, self.api.calls) self.assertTrue(cluster) def test_cluster_create_with_keypair(self): cluster_with_keypair = dict() cluster_with_keypair.update(CREATE_CLUSTER) cluster_with_keypair['keypair'] = 'test_key' cluster = self.mgr.create(**cluster_with_keypair) expect = [ ('POST', '/v1/clusters', {}, cluster_with_keypair), ] self.assertEqual(expect, self.api.calls) self.assertTrue(cluster) def test_cluster_create_with_docker_volume_size(self): cluster_with_volume_size = dict() cluster_with_volume_size.update(CREATE_CLUSTER) cluster_with_volume_size['docker_volume_size'] = 20 cluster = self.mgr.create(**cluster_with_volume_size) expect = [ ('POST', '/v1/clusters', {}, cluster_with_volume_size), ] self.assertEqual(expect, self.api.calls) self.assertTrue(cluster) def test_cluster_create_with_labels(self): cluster_with_labels = dict() cluster_with_labels.update(CREATE_CLUSTER) cluster_with_labels['labels'] = "key=val" cluster = self.mgr.create(**cluster_with_labels) expect = [ ('POST', '/v1/clusters', {}, cluster_with_labels), ] self.assertEqual(expect, self.api.calls) self.assertTrue(cluster) def test_cluster_create_with_discovery_url(self): cluster_with_discovery = dict() cluster_with_discovery.update(CREATE_CLUSTER) cluster_with_discovery['discovery_url'] = 'discovery_url' cluster = self.mgr.create(**cluster_with_discovery) expect = [ ('POST', '/v1/clusters', {}, cluster_with_discovery), ] self.assertEqual(expect, self.api.calls) self.assertTrue(cluster) def test_cluster_create_with_cluster_create_timeout(self): cluster_with_timeout = dict() cluster_with_timeout.update(CREATE_CLUSTER) cluster_with_timeout['create_timeout'] = '15' cluster = self.mgr.create(**cluster_with_timeout) expect = [ ('POST', '/v1/clusters', {}, cluster_with_timeout), ] self.assertEqual(expect, self.api.calls) self.assertTrue(cluster) def test_cluster_create_fail(self): CREATE_CLUSTER_FAIL = copy.deepcopy(CREATE_CLUSTER) CREATE_CLUSTER_FAIL["wrong_key"] = "wrong" self.assertRaisesRegex(exceptions.InvalidAttribute, ("Key must be in %s" % ','.join(clusters.CREATION_ATTRIBUTES)), self.mgr.create, **CREATE_CLUSTER_FAIL) self.assertEqual([], self.api.calls) def test_cluster_delete_by_id(self): cluster = self.mgr.delete(CLUSTER1['id']) expect = [ ('DELETE', '/v1/clusters/%s' % CLUSTER1['id'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertIsNone(cluster) def test_cluster_delete_by_name(self): cluster = self.mgr.delete(CLUSTER1['name']) expect = [ ('DELETE', '/v1/clusters/%s' % CLUSTER1['name'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertIsNone(cluster) def test_cluster_update(self): patch = {'op': 'replace', 'value': NEW_NAME, 'path': '/name'} cluster = self.mgr.update(id=CLUSTER1['id'], patch=patch) expect = [ ('PATCH', '/v1/clusters/%s' % CLUSTER1['id'], {}, patch), ] self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_NAME, cluster.name) def test_cluster_update_with_rollback(self): patch = {'op': 'replace', 'value': NEW_NAME, 'path': '/name'} cluster = self.mgr.update(id=CLUSTER1['id'], patch=patch, rollback=True) expect = [ ('PATCH', '/v1/clusters/%s/?rollback=True' % CLUSTER1['id'], {}, patch), ] self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_NAME, cluster.name) def test_cluster_resize(self): body = {'node_count': RESIZED_NODE_COUNT} cluster = self.mgr.resize(CLUSTER1["uuid"], **body) expect = [ ('POST', '/v1/clusters/%s/actions/resize' % CLUSTER1['uuid'], {}, body), ] self.assertEqual(expect, self.api.calls) self.assertEqual(RESIZED_NODE_COUNT, cluster.node_count) def test_cluster_upgrade(self): body = {'cluster_template': UPGRADED_TO_TEMPLATE, 'max_batch_size': 1} cluster = self.mgr.upgrade(CLUSTER1["uuid"], **body) expect = [ ('POST', '/v1/clusters/%s/actions/upgrade' % CLUSTER1['uuid'], {}, body), ] self.assertEqual(expect, self.api.calls) self.assertEqual(UPGRADED_TO_TEMPLATE, cluster.cluster_template_id) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1344194 python_magnumclient-5.0.0/magnumclient/tests/v1/test_clustertemplates.py0000664000175000017500000004060715244246676025635 0ustar00zuulzuul# Copyright 2015 IBM Corp # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import copy import testtools from testtools import matchers from magnumclient import exceptions from magnumclient.tests import utils from magnumclient.v1 import cluster_templates CLUSTERTEMPLATE1 = { 'id': 123, 'uuid': '66666666-7777-8888-9999-000000000001', 'name': 'clustertemplate1', 'image_id': 'clustertemplate1-image', 'master_flavor_id': 'm1.tiny', 'flavor_id': 'm1.small', 'external_network_id': 'd1f02cfb-d27f-4068-9332-84d907cb0e21', 'fixed_network': 'private', 'fixed_subnet': 'private-subnet', 'network_driver': 'libnetwork', 'volume_driver': 'rexray', 'dns_nameserver': '8.8.1.1', 'docker_volume_size': '71', 'docker_storage_driver': 'devicemapper', 'coe': 'swarm', 'http_proxy': 'http_proxy', 'https_proxy': 'https_proxy', 'no_proxy': 'no_proxy', 'labels': 'key1=val1,key11=val11', 'tls_disabled': False, 'public': False, 'registry_enabled': False, 'master_lb_enabled': True, 'floating_ip_enabled': True, 'hidden': False } CLUSTERTEMPLATE2 = { 'id': 124, 'uuid': '66666666-7777-8888-9999-000000000002', 'name': 'clustertemplate2', 'image_id': 'clustertemplate2-image', 'flavor_id': 'm2.small', 'master_flavor_id': 'm2.tiny', 'external_network_id': 'd1f02cfb-d27f-4068-9332-84d907cb0e22', 'fixed_network': 'private2', 'network_driver': 'flannel', 'volume_driver': 'cinder', 'dns_nameserver': '8.8.1.2', 'docker_volume_size': '71', 'docker_storage_driver': 'overlay', 'coe': 'kubernetes', 'labels': 'key2=val2,key22=val22', 'tls_disabled': True, 'public': True, 'registry_enabled': True} CREATE_CLUSTERTEMPLATE = copy.deepcopy(CLUSTERTEMPLATE1) del CREATE_CLUSTERTEMPLATE['id'] del CREATE_CLUSTERTEMPLATE['uuid'] UPDATED_CLUSTERTEMPLATE = copy.deepcopy(CLUSTERTEMPLATE1) NEW_NAME = 'newcluster' UPDATED_CLUSTERTEMPLATE['name'] = NEW_NAME fake_responses = { '/v1/clustertemplates': { 'GET': ( {}, {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, ), 'POST': ( {}, CREATE_CLUSTERTEMPLATE, ), }, '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['id']: { 'GET': ( {}, CLUSTERTEMPLATE1 ), 'DELETE': ( {}, None, ), 'PATCH': ( {}, UPDATED_CLUSTERTEMPLATE, ), }, '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['name']: { 'GET': ( {}, CLUSTERTEMPLATE1 ), 'DELETE': ( {}, None, ), 'PATCH': ( {}, UPDATED_CLUSTERTEMPLATE, ), }, '/v1/clustertemplates/detail': { 'GET': ( {}, {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, ), }, '/v1/clustertemplates/?limit=2': { 'GET': ( {}, {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, ), }, '/v1/clustertemplates/?marker=%s' % CLUSTERTEMPLATE2['uuid']: { 'GET': ( {}, {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, ), }, '/v1/clustertemplates/?limit=2&marker=%s' % CLUSTERTEMPLATE2['uuid']: { 'GET': ( {}, {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, ), }, '/v1/clustertemplates/?sort_dir=asc': { 'GET': ( {}, {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, ), }, '/v1/clustertemplates/?sort_key=uuid': { 'GET': ( {}, {'clustertemplates': [CLUSTERTEMPLATE1, CLUSTERTEMPLATE2]}, ), }, '/v1/clustertemplates/?sort_key=uuid&sort_dir=desc': { 'GET': ( {}, {'clustertemplates': [CLUSTERTEMPLATE2, CLUSTERTEMPLATE1]}, ), }, } class ClusterTemplateManagerTest(testtools.TestCase): def setUp(self): super(ClusterTemplateManagerTest, self).setUp() self.api = utils.FakeAPI(fake_responses) self.mgr = cluster_templates.ClusterTemplateManager(self.api) def test_clustertemplate_list(self): clustertemplates = self.mgr.list() expect = [ ('GET', '/v1/clustertemplates', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertThat(clustertemplates, matchers.HasLength(2)) def _test_clustertemplate_list_with_filters( self, limit=None, marker=None, sort_key=None, sort_dir=None, detail=False, expect=[]): clustertemplates_filter = self.mgr.list(limit=limit, marker=marker, sort_key=sort_key, sort_dir=sort_dir, detail=detail) self.assertEqual(expect, self.api.calls) self.assertThat(clustertemplates_filter, matchers.HasLength(2)) def test_clustertemplate_list_with_detail(self): expect = [ ('GET', '/v1/clustertemplates/detail', {}, None), ] self._test_clustertemplate_list_with_filters( detail=True, expect=expect) def test_clustertemplate_list_with_limit(self): expect = [ ('GET', '/v1/clustertemplates/?limit=2', {}, None), ] self._test_clustertemplate_list_with_filters( limit=2, expect=expect) def test_clustertemplate_list_with_marker(self): expect = [ ('GET', '/v1/clustertemplates/?marker=%s' % CLUSTERTEMPLATE2['uuid'], {}, None), ] self._test_clustertemplate_list_with_filters( marker=CLUSTERTEMPLATE2['uuid'], expect=expect) def test_clustertemplate_list_with_marker_limit(self): expect = [ ('GET', '/v1/clustertemplates/?limit=2&marker=%s' % CLUSTERTEMPLATE2['uuid'], {}, None), ] self._test_clustertemplate_list_with_filters( limit=2, marker=CLUSTERTEMPLATE2['uuid'], expect=expect) def test_clustertemplate_list_with_sort_dir(self): expect = [ ('GET', '/v1/clustertemplates/?sort_dir=asc', {}, None), ] self._test_clustertemplate_list_with_filters( sort_dir='asc', expect=expect) def test_clustertemplate_list_with_sort_key(self): expect = [ ('GET', '/v1/clustertemplates/?sort_key=uuid', {}, None), ] self._test_clustertemplate_list_with_filters( sort_key='uuid', expect=expect) def test_clustertemplate_list_with_sort_key_dir(self): expect = [ ('GET', '/v1/clustertemplates/?sort_key=uuid&sort_dir=desc', {}, None), ] self._test_clustertemplate_list_with_filters( sort_key='uuid', sort_dir='desc', expect=expect) def test_clustertemplate_show_by_id(self): cluster_template = self.mgr.get(CLUSTERTEMPLATE1['id']) expect = [ ('GET', '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['id'], {}, None) ] self.assertEqual(expect, self.api.calls) self.assertEqual(CLUSTERTEMPLATE1['name'], cluster_template.name) self.assertEqual(CLUSTERTEMPLATE1['image_id'], cluster_template.image_id) self.assertEqual(CLUSTERTEMPLATE1['docker_volume_size'], cluster_template.docker_volume_size) self.assertEqual(CLUSTERTEMPLATE1['docker_storage_driver'], cluster_template.docker_storage_driver) self.assertEqual(CLUSTERTEMPLATE1['fixed_network'], cluster_template.fixed_network) self.assertEqual(CLUSTERTEMPLATE1['fixed_subnet'], cluster_template.fixed_subnet) self.assertEqual(CLUSTERTEMPLATE1['coe'], cluster_template.coe) self.assertEqual(CLUSTERTEMPLATE1['http_proxy'], cluster_template.http_proxy) self.assertEqual(CLUSTERTEMPLATE1['https_proxy'], cluster_template.https_proxy) self.assertEqual(CLUSTERTEMPLATE1['no_proxy'], cluster_template.no_proxy) self.assertEqual(CLUSTERTEMPLATE1['network_driver'], cluster_template.network_driver) self.assertEqual(CLUSTERTEMPLATE1['volume_driver'], cluster_template.volume_driver) self.assertEqual(CLUSTERTEMPLATE1['labels'], cluster_template.labels) self.assertEqual(CLUSTERTEMPLATE1['tls_disabled'], cluster_template.tls_disabled) self.assertEqual(CLUSTERTEMPLATE1['public'], cluster_template.public) self.assertEqual(CLUSTERTEMPLATE1['registry_enabled'], cluster_template.registry_enabled) self.assertEqual(CLUSTERTEMPLATE1['master_lb_enabled'], cluster_template.master_lb_enabled) self.assertEqual(CLUSTERTEMPLATE1['floating_ip_enabled'], cluster_template.floating_ip_enabled) self.assertEqual(CLUSTERTEMPLATE1['hidden'], cluster_template.hidden) def test_clustertemplate_show_by_name(self): cluster_template = self.mgr.get(CLUSTERTEMPLATE1['name']) expect = [ ('GET', '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['name'], {}, None) ] self.assertEqual(expect, self.api.calls) self.assertEqual(CLUSTERTEMPLATE1['name'], cluster_template.name) self.assertEqual(CLUSTERTEMPLATE1['image_id'], cluster_template.image_id) self.assertEqual(CLUSTERTEMPLATE1['docker_volume_size'], cluster_template.docker_volume_size) self.assertEqual(CLUSTERTEMPLATE1['docker_storage_driver'], cluster_template.docker_storage_driver) self.assertEqual(CLUSTERTEMPLATE1['fixed_network'], cluster_template.fixed_network) self.assertEqual(CLUSTERTEMPLATE1['fixed_subnet'], cluster_template.fixed_subnet) self.assertEqual(CLUSTERTEMPLATE1['coe'], cluster_template.coe) self.assertEqual(CLUSTERTEMPLATE1['http_proxy'], cluster_template.http_proxy) self.assertEqual(CLUSTERTEMPLATE1['https_proxy'], cluster_template.https_proxy) self.assertEqual(CLUSTERTEMPLATE1['no_proxy'], cluster_template.no_proxy) self.assertEqual(CLUSTERTEMPLATE1['network_driver'], cluster_template.network_driver) self.assertEqual(CLUSTERTEMPLATE1['volume_driver'], cluster_template.volume_driver) self.assertEqual(CLUSTERTEMPLATE1['labels'], cluster_template.labels) self.assertEqual(CLUSTERTEMPLATE1['tls_disabled'], cluster_template.tls_disabled) self.assertEqual(CLUSTERTEMPLATE1['public'], cluster_template.public) self.assertEqual(CLUSTERTEMPLATE1['registry_enabled'], cluster_template.registry_enabled) self.assertEqual(CLUSTERTEMPLATE1['master_lb_enabled'], cluster_template.master_lb_enabled) self.assertEqual(CLUSTERTEMPLATE1['floating_ip_enabled'], cluster_template.floating_ip_enabled) self.assertEqual(CLUSTERTEMPLATE1['hidden'], cluster_template.hidden) def test_clustertemplate_create(self): cluster_template = self.mgr.create(**CREATE_CLUSTERTEMPLATE) expect = [ ('POST', '/v1/clustertemplates', {}, CREATE_CLUSTERTEMPLATE), ] self.assertEqual(expect, self.api.calls) self.assertTrue(cluster_template) self.assertEqual(CLUSTERTEMPLATE1['docker_volume_size'], cluster_template.docker_volume_size) self.assertEqual(CLUSTERTEMPLATE1['docker_storage_driver'], cluster_template.docker_storage_driver) def test_clustertemplate_create_with_keypair(self): cluster_template_with_keypair = dict() cluster_template_with_keypair.update(CREATE_CLUSTERTEMPLATE) cluster_template_with_keypair['keypair_id'] = 'test_key' cluster_template = self.mgr.create(**cluster_template_with_keypair) expect = [ ('POST', '/v1/clustertemplates', {}, cluster_template_with_keypair), ] self.assertEqual(expect, self.api.calls) self.assertTrue(cluster_template) self.assertEqual(CLUSTERTEMPLATE1['docker_volume_size'], cluster_template.docker_volume_size) self.assertEqual(CLUSTERTEMPLATE1['docker_storage_driver'], cluster_template.docker_storage_driver) def test_clustertemplate_create_with_docker_volume_size(self): cluster_template_with_docker_volume_size = dict() cluster_template_with_docker_volume_size.update(CREATE_CLUSTERTEMPLATE) cluster_template_with_docker_volume_size['docker_volume_size'] = 11 cluster_template = self.mgr.create( **cluster_template_with_docker_volume_size) expect = [ ('POST', '/v1/clustertemplates', {}, cluster_template_with_docker_volume_size), ] self.assertEqual(expect, self.api.calls) self.assertTrue(cluster_template) self.assertEqual(CLUSTERTEMPLATE1['docker_volume_size'], cluster_template.docker_volume_size) self.assertEqual(CLUSTERTEMPLATE1['docker_storage_driver'], cluster_template.docker_storage_driver) def test_clustertemplate_create_fail(self): CREATE_CLUSTERTEMPLATE_FAIL = copy.deepcopy(CREATE_CLUSTERTEMPLATE) CREATE_CLUSTERTEMPLATE_FAIL["wrong_key"] = "wrong" self.assertRaisesRegex( exceptions.InvalidAttribute, ("Key must be in %s" % ','.join(cluster_templates.CREATION_ATTRIBUTES)), self.mgr.create, **CREATE_CLUSTERTEMPLATE_FAIL) self.assertEqual([], self.api.calls) def test_clustertemplate_delete_by_id(self): cluster_template = self.mgr.delete(CLUSTERTEMPLATE1['id']) expect = [ ('DELETE', '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['id'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertIsNone(cluster_template) def test_clustertemplate_delete_by_name(self): cluster_template = self.mgr.delete(CLUSTERTEMPLATE1['name']) expect = [ ('DELETE', '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['name'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertIsNone(cluster_template) def test_clustertemplate_update(self): patch = {'op': 'replace', 'value': NEW_NAME, 'path': '/name'} cluster_template = self.mgr.update(id=CLUSTERTEMPLATE1['id'], patch=patch) expect = [ ('PATCH', '/v1/clustertemplates/%s' % CLUSTERTEMPLATE1['id'], {}, patch), ] self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_NAME, cluster_template.name) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1344194 python_magnumclient-5.0.0/magnumclient/tests/v1/test_mservices.py0000664000175000017500000001104515244246676024227 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 testtools import matchers from magnumclient.tests import utils from magnumclient.v1 import mservices SERVICE1 = {'id': 123, 'host': 'fake-host1', 'binary': 'fake-bin1', 'state': 'up', } SERVICE2 = {'id': 124, 'host': 'fake-host2', 'binary': 'fake-bin2', 'state': 'down', } fake_responses = { '/v1/mservices': { 'GET': ( {}, {'mservices': [SERVICE1, SERVICE2]}, ), }, '/v1/mservices/?limit=2': { 'GET': ( {}, {'mservices': [SERVICE1, SERVICE2]}, ), }, '/v1/mservices/?marker=%s' % SERVICE2['id']: { 'GET': ( {}, {'mservices': [SERVICE1, SERVICE2]}, ), }, '/v1/mservices/?limit=2&marker=%s' % SERVICE2['id']: { 'GET': ( {}, {'mservices': [SERVICE2, SERVICE1]}, ), }, '/v1/mservices/?sort_dir=asc': { 'GET': ( {}, {'mservices': [SERVICE1, SERVICE2]}, ), }, '/v1/mservices/?sort_key=id': { 'GET': ( {}, {'mservices': [SERVICE1, SERVICE2]}, ), }, '/v1/mservices/?sort_key=id&sort_dir=desc': { 'GET': ( {}, {'mservices': [SERVICE2, SERVICE1]}, ), }, } class MServiceManagerTest(testtools.TestCase): def setUp(self): super(MServiceManagerTest, self).setUp() self.api = utils.FakeAPI(fake_responses) self.mgr = mservices.MServiceManager(self.api) def test_coe_service_list(self): mservices = self.mgr.list() expect = [ ('GET', '/v1/mservices', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertThat(mservices, matchers.HasLength(2)) def _test_coe_service_list_with_filters( self, limit=None, marker=None, sort_key=None, sort_dir=None, detail=False, expect=[]): mservices_filter = self.mgr.list(limit=limit, marker=marker, sort_key=sort_key, sort_dir=sort_dir, detail=detail) self.assertEqual(expect, self.api.calls) self.assertThat(mservices_filter, matchers.HasLength(2)) def test_coe_service_list_with_limit(self): expect = [ ('GET', '/v1/mservices/?limit=2', {}, None), ] self._test_coe_service_list_with_filters( limit=2, expect=expect) def test_coe_service_list_with_marker(self): expect = [ ('GET', '/v1/mservices/?marker=%s' % SERVICE2['id'], {}, None), ] self._test_coe_service_list_with_filters( marker=SERVICE2['id'], expect=expect) def test_coe_service_list_with_marker_limit(self): expect = [ ('GET', '/v1/mservices/?limit=2&marker=%s' % SERVICE2['id'], {}, None), ] self._test_coe_service_list_with_filters( limit=2, marker=SERVICE2['id'], expect=expect) def test_coe_service_list_with_sort_dir(self): expect = [ ('GET', '/v1/mservices/?sort_dir=asc', {}, None), ] self._test_coe_service_list_with_filters( sort_dir='asc', expect=expect) def test_coe_service_list_with_sort_key(self): expect = [ ('GET', '/v1/mservices/?sort_key=id', {}, None), ] self._test_coe_service_list_with_filters( sort_key='id', expect=expect) def test_coe_service_list_with_sort_key_dir(self): expect = [ ('GET', '/v1/mservices/?sort_key=id&sort_dir=desc', {}, None), ] self._test_coe_service_list_with_filters( sort_key='id', sort_dir='desc', expect=expect) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1344194 python_magnumclient-5.0.0/magnumclient/tests/v1/test_nodegroups.py0000664000175000017500000002461415244246676024422 0ustar00zuulzuul# Copyright (c) 2018 European Organization for Nuclear Research. # 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 testtools from testtools import matchers from magnumclient import exceptions from magnumclient.tests import utils from magnumclient.v1 import nodegroups NODEGROUP1 = { 'id': 123, 'uuid': '66666666-7777-8888-9999-000000000001', 'cluster_id': '66666666-7777-8888-9999-000000000000', 'name': 'test-worker', 'node_addresses': ['172.17.2.3'], 'node_count': 2, 'project_id': 'fake_project', 'labels': {}, 'flavor_id': 'fake_flavor_1', 'image_id': 'fake_image', 'is_default': True, 'role': 'worker', 'max_node_count': 10, 'min_node_count': 0 } NODEGROUP2 = { 'id': 124, 'uuid': '66666666-7777-8888-9999-000000000002', 'cluster_id': '66666666-7777-8888-9999-000000000000', 'name': 'test-master', 'node_addresses': ['172.17.2.4'], 'node_count': 2, 'project_id': 'fake_project', 'labels': {}, 'flavor_id': 'fake_flavor_1', 'image_id': 'fake_image', 'is_default': True, 'role': 'master', 'max_node_count': 10, 'min_node_count': 0 } CREATE_NODEGROUP = copy.deepcopy(NODEGROUP1) del CREATE_NODEGROUP['id'] del CREATE_NODEGROUP['uuid'] del CREATE_NODEGROUP['node_addresses'] del CREATE_NODEGROUP['is_default'] del CREATE_NODEGROUP['cluster_id'] UPDATED_NODEGROUP = copy.deepcopy(NODEGROUP1) NEW_NODE_COUNT = 9 UPDATED_NODEGROUP['node_count'] = NEW_NODE_COUNT fake_responses = { '/v1/clusters/test/nodegroups/': { 'GET': ( {}, {'nodegroups': [NODEGROUP1, NODEGROUP2]}, ), 'POST': ( {}, CREATE_NODEGROUP, ), }, '/v1/clusters/test/nodegroups/%s' % NODEGROUP1['id']: { 'GET': ( {}, NODEGROUP1 ), 'DELETE': ( {}, None, ), 'PATCH': ( {}, UPDATED_NODEGROUP, ), }, '/v1/clusters/test/nodegroups/%s/?rollback=True' % NODEGROUP1['id']: { 'PATCH': ( {}, UPDATED_NODEGROUP, ), }, '/v1/clusters/test/nodegroups/%s' % NODEGROUP1['name']: { 'GET': ( {}, NODEGROUP1 ), 'DELETE': ( {}, None, ), 'PATCH': ( {}, UPDATED_NODEGROUP, ), }, '/v1/clusters/test/nodegroups/?limit=2': { 'GET': ( {}, {'nodegroups': [NODEGROUP1, NODEGROUP2]}, ), }, '/v1/clusters/test/nodegroups/?marker=%s' % NODEGROUP2['uuid']: { 'GET': ( {}, {'nodegroups': [NODEGROUP1, NODEGROUP2]}, ), }, '/v1/clusters/test/nodegroups/?limit=2&marker=%s' % NODEGROUP2['uuid']: { 'GET': ( {}, {'nodegroups': [NODEGROUP1, NODEGROUP2]}, ), }, '/v1/clusters/test/nodegroups/?sort_dir=asc': { 'GET': ( {}, {'nodegroups': [NODEGROUP1, NODEGROUP2]}, ), }, '/v1/clusters/test/nodegroups/?sort_key=uuid': { 'GET': ( {}, {'nodegroups': [NODEGROUP1, NODEGROUP2]}, ), }, '/v1/clusters/test/nodegroups/?sort_key=uuid&sort_dir=desc': { 'GET': ( {}, {'nodegroups': [NODEGROUP2, NODEGROUP1]}, ), }, } class NodeGroupManagerTest(testtools.TestCase): def setUp(self): super(NodeGroupManagerTest, self).setUp() self.api = utils.FakeAPI(fake_responses) self.mgr = nodegroups.NodeGroupManager(self.api) self.cluster_id = 'test' self.base_path = '/v1/clusters/test/nodegroups/' def test_nodegroup_list(self): clusters = self.mgr.list(self.cluster_id) expect = [ ('GET', self.base_path, {}, None), ] self.assertEqual(expect, self.api.calls) self.assertThat(clusters, matchers.HasLength(2)) def _test_nodegroup_list_with_filters(self, cluster_id, limit=None, marker=None, sort_key=None, sort_dir=None, detail=False, expect=[]): nodegroup_filter = self.mgr.list(cluster_id, limit=limit, marker=marker, sort_key=sort_key, sort_dir=sort_dir, detail=detail) self.assertEqual(expect, self.api.calls) self.assertThat(nodegroup_filter, matchers.HasLength(2)) def test_nodegroup_list_with_limit(self): expect = [ ('GET', self.base_path + '?limit=2', {}, None), ] self._test_nodegroup_list_with_filters( self.cluster_id, limit=2, expect=expect) def test_nodegroup_list_with_marker(self): filter_ = '?marker=%s' % NODEGROUP2['uuid'] expect = [ ('GET', self.base_path + filter_, {}, None), ] self._test_nodegroup_list_with_filters( self.cluster_id, marker=NODEGROUP2['uuid'], expect=expect) def test_nodegroup_list_with_marker_limit(self): filter_ = '?limit=2&marker=%s' % NODEGROUP2['uuid'] expect = [ ('GET', self.base_path + filter_, {}, None), ] self._test_nodegroup_list_with_filters( self.cluster_id, limit=2, marker=NODEGROUP2['uuid'], expect=expect) def test_nodegroup_list_with_sort_dir(self): expect = [ ('GET', '/v1/clusters/test/nodegroups/?sort_dir=asc', {}, None), ] self._test_nodegroup_list_with_filters( self.cluster_id, sort_dir='asc', expect=expect) def test_nodegroup_list_with_sort_key(self): expect = [ ('GET', '/v1/clusters/test/nodegroups/?sort_key=uuid', {}, None), ] self._test_nodegroup_list_with_filters( self.cluster_id, sort_key='uuid', expect=expect) def test_nodegroup_list_with_sort_key_dir(self): expect = [ ('GET', self.base_path + '?sort_key=uuid&sort_dir=desc', {}, None), ] self._test_nodegroup_list_with_filters( self.cluster_id, sort_key='uuid', sort_dir='desc', expect=expect) def test_nodegroup_show_by_name(self): nodegroup = self.mgr.get(self.cluster_id, NODEGROUP1['name']) expect = [ ('GET', self.base_path + '%s' % NODEGROUP1['name'], {}, None) ] self.assertEqual(expect, self.api.calls) self.assertEqual(NODEGROUP1['name'], nodegroup.name) def test_nodegroup_show_by_id(self): nodegroup = self.mgr.get(self.cluster_id, NODEGROUP1['id']) expect = [ ('GET', self.base_path + '%s' % NODEGROUP1['id'], {}, None) ] self.assertEqual(expect, self.api.calls) self.assertEqual(NODEGROUP1['name'], nodegroup.name) def test_nodegroup_delete_by_id(self): nodegroup = self.mgr.delete(self.cluster_id, NODEGROUP1['id']) expect = [ ('DELETE', self.base_path + '%s' % NODEGROUP1['id'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertIsNone(nodegroup) def test_nodegroup_delete_by_name(self): nodegroup = self.mgr.delete(self.cluster_id, NODEGROUP1['name']) expect = [ ('DELETE', self.base_path + '%s' % NODEGROUP1['name'], {}, None), ] self.assertEqual(expect, self.api.calls) self.assertIsNone(nodegroup) def test_nodegroup_update(self): patch = {'op': 'replace', 'value': NEW_NODE_COUNT, 'path': '/node_count'} nodegroup = self.mgr.update(self.cluster_id, id=NODEGROUP1['id'], patch=patch) expect = [ ('PATCH', self.base_path + '%s' % NODEGROUP1['id'], {}, patch), ] self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_NODE_COUNT, nodegroup.node_count) def test_nodegroup_create(self): nodegroup = self.mgr.create(self.cluster_id, **CREATE_NODEGROUP) expect = [ ('POST', self.base_path, {}, CREATE_NODEGROUP), ] self.assertEqual(expect, self.api.calls) self.assertTrue(nodegroup) def test_nodegroup_create_with_docker_volume_size(self): ng_with_volume_size = dict() ng_with_volume_size.update(CREATE_NODEGROUP) ng_with_volume_size['docker_volume_size'] = 20 nodegroup = self.mgr.create(self.cluster_id, **ng_with_volume_size) expect = [ ('POST', self.base_path, {}, ng_with_volume_size), ] self.assertEqual(expect, self.api.calls) self.assertTrue(nodegroup) def test_nodegroup_create_with_labels(self): ng_with_labels = dict() ng_with_labels.update(CREATE_NODEGROUP) ng_with_labels['labels'] = "key=val" nodegroup = self.mgr.create(self.cluster_id, **ng_with_labels) expect = [ ('POST', self.base_path, {}, ng_with_labels), ] self.assertEqual(expect, self.api.calls) self.assertTrue(nodegroup) def test_nodegroup_create_fail(self): CREATE_NODEGROUP_FAIL = copy.deepcopy(CREATE_NODEGROUP) CREATE_NODEGROUP_FAIL["wrong_key"] = "wrong" self.assertRaisesRegex(exceptions.InvalidAttribute, ("Key must be in %s" % ','.join(nodegroups.CREATION_ATTRIBUTES)), self.mgr.create, self.cluster_id, **CREATE_NODEGROUP_FAIL) self.assertEqual([], self.api.calls) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1344194 python_magnumclient-5.0.0/magnumclient/tests/v1/test_quotas.py0000664000175000017500000001027415244246676023546 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 testtools from testtools import matchers from magnumclient.tests import utils from magnumclient.v1 import quotas QUOTA1 = { 'id': 123, 'resource': "Cluster", 'hard_limit': 5, 'project_id': 'abc' } QUOTA2 = { 'id': 124, 'resource': "Cluster", 'hard_limit': 10, 'project_id': 'bcd' } CREATE_QUOTA = copy.deepcopy(QUOTA1) del CREATE_QUOTA['id'] UPDATED_QUOTA = copy.deepcopy(QUOTA2) NEW_HARD_LIMIT = 20 UPDATED_QUOTA['hard_limit'] = NEW_HARD_LIMIT fake_responses = { '/v1/quotas?all_tenants=True': { 'GET': ( {}, {'quotas': [QUOTA1, QUOTA2]}, ), }, '/v1/quotas': { 'GET': ( {}, {'quotas': [QUOTA1]}, ), 'POST': ( {}, QUOTA1, ), }, '/v1/quotas/%(id)s/%(res)s' % {'id': QUOTA2['project_id'], 'res': QUOTA2['resource']}: { 'GET': ( {}, QUOTA2, ), 'PATCH': ( {}, UPDATED_QUOTA, ), 'DELETE': ( {}, None, ), }, } class QuotasManagerTest(testtools.TestCase): def setUp(self): super(QuotasManagerTest, self).setUp() self.api = utils.FakeAPI(fake_responses) self.mgr = quotas.QuotasManager(self.api) def test_list_quotas(self): quotas = self.mgr.list() expect = [ ('GET', '/v1/quotas', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertThat(quotas, matchers.HasLength(1)) def test_list_quotas_all(self): quotas = self.mgr.list(all_tenants=True) expect = [ ('GET', '/v1/quotas?all_tenants=True', {}, None), ] self.assertEqual(expect, self.api.calls) self.assertThat(quotas, matchers.HasLength(2)) def test_show_project_resource_quota(self): expect = [ ('GET', '/v1/quotas/%(id)s/%(res)s' % {'id': QUOTA2['project_id'], 'res': QUOTA2['resource']}, {}, None), ] quotas = self.mgr.get(QUOTA2['project_id'], QUOTA2['resource']) self.assertEqual(expect, self.api.calls) expected_quotas = QUOTA2 self.assertEqual(expected_quotas, quotas._info) def test_quota_create(self): quota = self.mgr.create(**CREATE_QUOTA) expect = [ ('POST', '/v1/quotas', {}, CREATE_QUOTA), ] self.assertEqual(expect, self.api.calls) self.assertEqual(QUOTA1, quota._info) def test_quota_update(self): patch = { 'resource': "Cluster", 'hard_limit': NEW_HARD_LIMIT, 'project_id': 'bcd' } quota = self.mgr.update(id=QUOTA2['project_id'], resource=QUOTA2['resource'], patch=patch) expect = [ ('PATCH', '/v1/quotas/%(id)s/%(res)s' % { 'id': QUOTA2['project_id'], 'res': QUOTA2['resource']}, {}, patch), ] self.assertEqual(expect, self.api.calls) self.assertEqual(NEW_HARD_LIMIT, quota.hard_limit) def test_quota_delete(self): quota = self.mgr.delete(QUOTA2['project_id'], QUOTA2['resource']) expect = [ ('DELETE', '/v1/quotas/%(id)s/%(res)s' % {'id': QUOTA2['project_id'], 'res': QUOTA2['resource']}, {}, None), ] self.assertEqual(expect, self.api.calls) self.assertIsNone(quota) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1354194 python_magnumclient-5.0.0/magnumclient/tests/v1/test_stats.py0000664000175000017500000000560015244246676023365 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 magnumclient.tests import utils from magnumclient.v1 import stats CLUSTER1 = {'id': 123, 'uuid': '66666666-7777-8888-9999-000000000001', 'name': 'cluster1', 'cluster_template_id': 'e74c40e0-d825-11e2-a28f-0800200c9a61', 'cluster_id': '5d12f6fd-a196-4bf0-ae4c-1f639a523a51', 'api_address': '172.17.2.1', 'node_addresses': ['172.17.2.3'], 'node_count': 2, 'master_count': 1, 'project_id': 'abc' } CLUSTER2 = {'id': 124, 'uuid': '66666666-7777-8888-9999-000000000002', 'name': 'cluster2', 'cluster_template_id': 'e74c40e0-d825-11e2-a28f-0800200c9a62', 'cluster_id': '5d12f6fd-a196-4bf0-ae4c-1f639a523a52', 'api_address': '172.17.2.2', 'node_addresses': ['172.17.2.4'], 'node_count': 2, 'master_count': 1, 'project_id': 'bcd' } nc = 'node_count' mc = 'master_count' C1 = CLUSTER1 C2 = CLUSTER2 fake_responses = { '/v1/stats': { 'GET': ( {}, {'clusters': 2, 'nodes': C1[nc] + C1[mc] + C2[nc] + C2[mc]}, ) }, '/v1/stats?project_id=%s' % C2['project_id']: { 'GET': ( {}, {'clusters': 1, 'nodes': C2[nc] + C2[mc]}, ) }, } class StatsManagerTest(testtools.TestCase): def setUp(self): super(StatsManagerTest, self).setUp() self.api = utils.FakeAPI(fake_responses) self.mgr = stats.StatsManager(self.api) def test_stats(self): stats = self.mgr.list() expect = [ ('GET', '/v1/stats', {}, None), ] self.assertEqual(expect, self.api.calls) expected_stats = {'clusters': 2, 'nodes': C1[nc] + C1[mc] + C2[nc] + C2[mc]} self.assertEqual(expected_stats, stats._info) def test_stats_with_project_id(self): expect = [ ('GET', '/v1/stats?project_id=%s' % CLUSTER2['project_id'], {}, None), ] stats = self.mgr.list(project_id=CLUSTER2['project_id']) self.assertEqual(expect, self.api.calls) expected_stats = {'clusters': 1, 'nodes': C2[nc] + C2[mc]} self.assertEqual(expected_stats, stats._info) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0085783 python_magnumclient-5.0.0/magnumclient/v1/0000775000175000017500000000000015244246764017451 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1354194 python_magnumclient-5.0.0/magnumclient/v1/__init__.py0000664000175000017500000000000015244246676021552 0ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1354194 python_magnumclient-5.0.0/magnumclient/v1/basemodels.py0000664000175000017500000001074715244246676022154 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 magnumclient.common import base from magnumclient.common import utils from magnumclient import exceptions CREATION_ATTRIBUTES = ['name', 'image_id', 'flavor_id', 'master_flavor_id', 'keypair_id', 'external_network_id', 'fixed_network', 'fixed_subnet', 'dns_nameserver', 'docker_volume_size', 'labels', 'coe', 'http_proxy', 'https_proxy', 'no_proxy', 'network_driver', 'tls_disabled', 'public', 'registry_enabled', 'volume_driver', 'server_type', 'docker_storage_driver', 'master_lb_enabled', 'floating_ip_enabled', 'hidden', 'tags'] OUTPUT_ATTRIBUTES = CREATION_ATTRIBUTES + ['apiserver_port', 'created_at', 'insecure_registry', 'links', 'updated_at', 'cluster_distro', 'uuid'] class BaseModel(base.Resource): # model_name needs to be overridden by any derived class. # model_name should be capitalized and singular, e.g. "Cluster" model_name = '' def __repr__(self): return "<" + self.__class__.model_name + "%s>" % self._info class BaseModelManager(base.Manager): # api_name needs to be overridden by any derived class. # api_name should be pluralized and lowercase, e.g. "clustertemplates", as # it shows up in the URL path: "/v1/{api_name}" api_name = '' @classmethod def _path(cls, id=None): return '/v1/' + cls.api_name + \ '/%s' % id if id else '/v1/' + cls.api_name def list(self, limit=None, marker=None, sort_key=None, sort_dir=None, detail=False): """Retrieve a list of cluster templates. :param marker: Optional, the UUID of a template, eg the last template from a previous result set. Return the next result set. :param limit: The maximum number of results to return per request, if: 1) limit > 0, the maximum number of cluster templates to return. 2) limit == 0, return the entire list of cluster templates. 3) limit param is NOT specified (None), the number of items returned respect the maximum imposed by the Magnum API (see Magnum's api.max_limit option). :param sort_key: Optional, field used for sorting. :param sort_dir: Optional, direction of sorting, either 'asc' (the default) or 'desc'. :param detail: Optional, boolean whether to return detailed information about cluster templates. :returns: A list of cluster templates. """ if limit is not None: limit = int(limit) filters = utils.common_filters(marker, limit, sort_key, sort_dir) path = '' if detail: path += 'detail' if filters: path += '?' + '&'.join(filters) if limit is None: return self._list(self._path(path), self.__class__.api_name) else: return self._list_pagination(self._path(path), self.__class__.api_name, limit=limit) def get(self, id): try: return self._list(self._path(id))[0] except IndexError: return None def create(self, **kwargs): new = {} for (key, value) in kwargs.items(): if key in CREATION_ATTRIBUTES: new[key] = value else: raise exceptions.InvalidAttribute( "Key must be in %s" % ",".join(CREATION_ATTRIBUTES)) return self._create(self._path(), new) def delete(self, id): return self._delete(self._path(id)) def update(self, id, patch): return self._update(self._path(id), patch) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1364193 python_magnumclient-5.0.0/magnumclient/v1/baseunit.py0000664000175000017500000000755715244246676021655 0ustar00zuulzuul# Copyright 2014 NEC Corporation. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from magnumclient.common import base from magnumclient.common import utils from magnumclient import exceptions # Derived classes may append their own custom attributes to this default list CREATION_ATTRIBUTES = ['name', 'node_count', 'discovery_url', 'master_count'] class BaseTemplate(base.Resource): # template_name must be overridden by any derived class. # template_name should be an uppercase plural, e.g. "Clusters" template_name = '' def __repr__(self): return "<" + self.__class__.template_name + " %s>" % self._info class BaseTemplateManager(base.Manager): # template_name must be overridden by any derived class. # template_name should be a lowercase plural, e.g. "clusters" template_name = '' @classmethod def _path(cls, id=None): return '/v1/' + cls.template_name + \ '/%s' % id if id else '/v1/' + cls.template_name def list(self, limit=None, marker=None, sort_key=None, sort_dir=None, detail=False): """Retrieve a list of clusters. :param marker: Optional, the UUID of a cluster, eg the last cluster from a previous result set. Return the next result set. :param limit: The maximum number of results to return per request, if: 1) limit > 0, the maximum number of clusters to return. 2) limit == 0, return the entire list of clusters. 3) limit param is NOT specified (None), the number of items returned respect the maximum imposed by the Magnum API (see Magnum's api.max_limit option). :param sort_key: Optional, field used for sorting. :param sort_dir: Optional, direction of sorting, either 'asc' (the default) or 'desc'. :param detail: Optional, boolean whether to return detailed information about clusters. :returns: A list of clusters. """ if limit is not None: limit = int(limit) filters = utils.common_filters(marker, limit, sort_key, sort_dir) path = '' if detail: path += 'detail' if filters: path += '?' + '&'.join(filters) if limit is None: return self._list(self._path(path), self.__class__.template_name) else: return self._list_pagination(self._path(path), self.__class__.template_name, limit=limit) def get(self, id): try: return self._list(self._path(id))[0] except IndexError: return None def create(self, **kwargs): new = {} for (key, value) in kwargs.items(): if key in CREATION_ATTRIBUTES: new[key] = value else: raise exceptions.InvalidAttribute( "Key must be in %s" % ",".join(CREATION_ATTRIBUTES)) return self._create(self._path(), new) def delete(self, id): return self._delete(self._path(id)) def update(self, id, patch, rollback=False): url = self._path(id) if rollback: url += '/?rollback=True' return self._update(url, patch) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1364193 python_magnumclient-5.0.0/magnumclient/v1/certificates.py0000664000175000017500000000320315244246676022470 0ustar00zuulzuul# Copyright 2015 Rackspace, Inc. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from magnumclient.common import base from magnumclient import exceptions CREATION_ATTRIBUTES = ['cluster_uuid', 'csr'] class Certificate(base.Resource): def __repr__(self): return "" % self._info class CertificateManager(base.Manager): resource_class = Certificate @staticmethod def _path(id=None): return '/v1/certificates/%s' % id if id else '/v1/certificates' def get(self, cluster_uuid): try: return self._list(self._path(cluster_uuid))[0] except IndexError: return None def create(self, **kwargs): new = {} for (key, value) in kwargs.items(): if key in CREATION_ATTRIBUTES: new[key] = value else: raise exceptions.InvalidAttribute( "Key must be in %s" % ",".join(CREATION_ATTRIBUTES)) return self._create(self._path(), new) def rotate_ca(self, **kwargs): return self._update(self._path(id=kwargs['cluster_uuid'])) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1364193 python_magnumclient-5.0.0/magnumclient/v1/client.py0000664000175000017500000001610615244246676021307 0ustar00zuulzuul# Copyright 2014 # The Cloudscaling Group, 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 keystoneauth1 import session as ksa_session from openstack import config as occ from oslo_utils import importutils from magnumclient.common import httpclient from magnumclient.v1 import certificates from magnumclient.v1 import cluster_templates from magnumclient.v1 import clusters from magnumclient.v1 import credentials from magnumclient.v1 import mservices from magnumclient.v1 import nodegroups from magnumclient.v1 import quotas from magnumclient.v1 import stats profiler = importutils.try_import("osprofiler.profiler") DEFAULT_SERVICE_TYPE = 'container-infra' def _load_session(cloud=None, insecure=False, timeout=None, **kwargs): cloud_config = occ.OpenStackConfig() cloud_config = cloud_config.get_one( cloud=cloud, verify=not insecure, **kwargs) verify, cert = cloud_config.get_requests_verify_args() auth = cloud_config.get_auth() session = ksa_session.Session( auth=auth, verify=verify, cert=cert, timeout=timeout) return session def _load_service_type(session, service_type=None, service_name=None, interface=None, region_name=None): try: session.get_endpoint( service_type=service_type, service_name=service_name, interface=interface, region_name=region_name) except Exception as e: raise RuntimeError(str(e)) return service_type def _load_session_client(session=None, endpoint_override=None, username=None, project_id=None, project_name=None, auth_url=None, password=None, auth_type=None, insecure=None, user_domain_id=None, user_domain_name=None, project_domain_id=None, project_domain_name=None, auth_token=None, timeout=None, service_type=None, service_name=None, interface=None, region_name=None, api_version=None, **kwargs): if not session: session = _load_session( username=username, project_id=project_id, project_name=project_name, auth_url=auth_url, password=password, auth_type=auth_type, insecure=insecure, user_domain_id=user_domain_id, user_domain_name=user_domain_name, project_domain_id=project_domain_id, project_domain_name=project_domain_name, auth_token=auth_token, timeout=timeout, **kwargs ) if not endpoint_override: service_type = _load_service_type( session, service_type=service_type, service_name=service_name, interface=interface, region_name=region_name, ) return httpclient.SessionClient( service_type=service_type, service_name=service_name, interface=interface, region_name=region_name, session=session, endpoint_override=endpoint_override, api_version=api_version, ) class Client(object): def __init__(self, username=None, project_id=None, project_name=None, auth_url=None, magnum_url=None, endpoint_override=None, service_type=DEFAULT_SERVICE_TYPE, region_name=None, session=None, password=None, auth_type='password', interface=None, endpoint_type=None, service_name=None, insecure=False, user_domain_id=None, user_domain_name=None, project_domain_id=None, project_domain_name=None, auth_token=None, timeout=600, api_version=None, **kwargs): if endpoint_type: interface = endpoint_type # osc sometimes give 'None' value if not interface: interface = 'public' if interface.endswith('URL'): interface = interface[:-3] # fix (yolanda): os-cloud-config is using endpoint_override # instead of magnum_url if magnum_url and not endpoint_override: endpoint_override = magnum_url if endpoint_override and auth_token: self.http_client = httpclient.HTTPClient( endpoint_override, token=auth_token, api_version=api_version, timeout=timeout, insecure=insecure, **kwargs ) else: self.http_client = _load_session_client( session=session, endpoint_override=endpoint_override, username=username, project_id=project_id, project_name=project_name, auth_url=auth_url, password=password, auth_type=auth_type, insecure=insecure, user_domain_id=user_domain_id, user_domain_name=user_domain_name, project_domain_id=project_domain_id, project_domain_name=project_domain_name, auth_token=auth_token, timeout=timeout, service_type=service_type, service_name=service_name, interface=interface, region_name=region_name, api_version=api_version, **kwargs ) self.clusters = clusters.ClusterManager(self.http_client) self.certificates = certificates.CertificateManager(self.http_client) self.cluster_templates = \ cluster_templates.ClusterTemplateManager(self.http_client) self.mservices = mservices.MServiceManager(self.http_client) profile = kwargs.pop("profile", None) if profiler and profile: # Initialize the root of the future trace: the created trace ID # will be used as the very first parent to which all related # traces will be bound to. The given HMAC key must correspond to # the one set in magnum-api magnum.conf, otherwise the latter # will fail to check the request signature and will skip # initialization of osprofiler on the server side. profiler.init(profile) self.stats = stats.StatsManager(self.http_client) self.quotas = quotas.QuotasManager(self.http_client) self.nodegroups = nodegroups.NodeGroupManager(self.http_client) self.credentials = credentials.CredentialManager(self.http_client) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1364193 python_magnumclient-5.0.0/magnumclient/v1/cluster_templates.py0000664000175000017500000000317215244246676023567 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 magnumclient.v1 import basemodels CLUSTER_TEMPLATE_ATTRIBUTES = [ 'insecure_registry', 'labels', 'updated_at', 'floating_ip_enabled', 'fixed_subnet', 'master_flavor_id', 'uuid', 'no_proxy', 'https_proxy', 'tls_disabled', 'keypair_id', 'public', 'http_proxy', 'docker_volume_size', 'server_type', 'external_network_id', 'cluster_distro', 'image_id', 'volume_driver', 'registry_enabled', 'docker_storage_driver', 'apiserver_port', 'name', 'created_at', 'network_driver', 'fixed_network', 'coe', 'flavor_id', 'master_lb_enabled', 'dns_nameserver', 'project_id', 'hidden', 'tags', 'driver', ] CREATION_ATTRIBUTES = basemodels.CREATION_ATTRIBUTES CREATION_ATTRIBUTES.append('insecure_registry') CREATION_ATTRIBUTES.append('driver') class ClusterTemplate(basemodels.BaseModel): model_name = "ClusterTemplate" class ClusterTemplateManager(basemodels.BaseModelManager): api_name = "clustertemplates" resource_class = ClusterTemplate ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1364193 python_magnumclient-5.0.0/magnumclient/v1/clusters.py0000664000175000017500000000736115244246676021700 0ustar00zuulzuul# Copyright 2014 NEC Corporation. All rights reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from magnumclient.v1 import baseunit CLUSTER_ATTRIBUTES = [ 'status', 'health_status', 'cluster_template_id', 'node_addresses', 'uuid', 'cluster_id', 'status_reason', 'created_at', 'updated_at', 'coe_version', 'labels', 'labels_overridden', 'labels_skipped', 'labels_added', 'fixed_network', 'fixed_subnet', 'floating_ip_enabled', 'faults', 'keypair', 'api_address', 'master_addresses', 'master_lb_enabled', 'create_timeout', 'node_count', 'discovery_url', 'docker_volume_size', 'master_count', 'container_version', 'name', 'master_flavor_id', 'flavor_id', 'health_status_reason', 'project_id', ] CREATION_ATTRIBUTES = baseunit.CREATION_ATTRIBUTES CREATION_ATTRIBUTES.append('cluster_template_id') CREATION_ATTRIBUTES.append('create_timeout') CREATION_ATTRIBUTES.append('keypair') CREATION_ATTRIBUTES.append('docker_volume_size') CREATION_ATTRIBUTES.append('labels') CREATION_ATTRIBUTES.append('master_flavor_id') CREATION_ATTRIBUTES.append('flavor_id') CREATION_ATTRIBUTES.append('fixed_network') CREATION_ATTRIBUTES.append('fixed_subnet') CREATION_ATTRIBUTES.append('floating_ip_enabled') CREATION_ATTRIBUTES.append('merge_labels') CREATION_ATTRIBUTES.append('master_lb_enabled') class Cluster(baseunit.BaseTemplate): template_name = "Clusters" class ClusterManager(baseunit.BaseTemplateManager): resource_class = Cluster template_name = 'clusters' @staticmethod def _normalize(cluster): # Pre-1.13 servers return stack_id instead of cluster_id; normalise # so callers always see cluster_id regardless of server microversion. stack_id = cluster._info.get('stack_id') if stack_id and not cluster._info.get('cluster_id'): cluster._info['cluster_id'] = stack_id return cluster def get(self, id): cluster = super().get(id) return self._normalize(cluster) if cluster else None def list(self, **kwargs): return [self._normalize(c) for c in super().list(**kwargs)] def resize(self, cluster_uuid, node_count, nodes_to_remove=[], nodegroup=None): url = self._path(cluster_uuid) + "/actions/resize" post_body = {"node_count": node_count} if nodes_to_remove: post_body.update({"nodes_to_remove": nodes_to_remove}) if nodegroup: post_body.update({"nodegroup": nodegroup}) resp, resp_body = self.api.json_request("POST", url, body=post_body) if resp_body: return self.resource_class(self, resp_body) def upgrade(self, cluster_uuid, cluster_template, max_batch_size=1, nodegroup=None): url = self._path(cluster_uuid) + "/actions/upgrade" post_body = {"cluster_template": cluster_template} if max_batch_size: post_body.update({"max_batch_size": max_batch_size}) if nodegroup: post_body.update({"nodegroup": nodegroup}) resp, resp_body = self.api.json_request("POST", url, body=post_body) if resp_body: return self.resource_class(self, resp_body) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1374195 python_magnumclient-5.0.0/magnumclient/v1/credentials.py0000664000175000017500000000202715244246676022323 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 magnumclient.common import base class Cluster(base.Resource): def __repr__(self): return "" % self._info class CredentialManager(base.Manager): api_name = "credentials" resource_class = Cluster @staticmethod def _path(id=None): return "/v1/credentials/%s" % id def update(self, id): resp, resp_body = self.api.json_request('PATCH', self._path(id=id)) return self.resource_class(self, resp_body) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1374195 python_magnumclient-5.0.0/magnumclient/v1/mservices.py0000664000175000017500000000474615244246676022040 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 magnumclient.common import base from magnumclient.common import utils class MService(base.Resource): def __repr__(self): return "" % self._info class MServiceManager(base.Manager): resource_class = MService @staticmethod def _path(id=None): return '/v1/mservices/%s' % id if id else '/v1/mservices' def list(self, marker=None, limit=None, sort_key=None, sort_dir=None, detail=False): """Retrieve list of magnum services. :param marker: Optional, the ID of a magnum service, eg the last services from a previous result set. Return the next result set. :param limit: The maximum number of results to return per request, if: 1) limit > 0, the maximum number of services to return. 2) limit == 0, return the entire list of services. 3) limit param is NOT specified (None), the number of items returned respect the maximum imposed by the Magnum API (see Magnum's api.max_limit option). :param sort_key: Optional, field used for sorting. :param sort_dir: Optional, direction of sorting, either 'asc' (the default) or 'desc'. :param detail: Optional, boolean whether to return detailed information about services. :returns: A list of services. """ if limit is not None: limit = int(limit) filters = utils.common_filters(marker, limit, sort_key, sort_dir) path = '' if detail: path += 'detail' if filters: path += '?' + '&'.join(filters) if limit is None: return self._list(self._path(path), "mservices") else: return self._list_pagination(self._path(path), "mservices", limit=limit) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1374195 python_magnumclient-5.0.0/magnumclient/v1/nodegroups.py0000664000175000017500000000660415244246676022220 0ustar00zuulzuul# Copyright (c) 2018 European Organization for Nuclear Research. # 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 magnumclient.common import utils from magnumclient import exceptions from magnumclient.v1 import baseunit NODEGROUP_ATTRIBUTES = [ 'uuid', 'name', 'cluster_id', 'project_id', 'docker_volume_size', 'labels', 'labels_overridden', 'labels_skipped', 'labels_added', 'flavor_id', 'image_id', 'node_addresses', 'node_count', 'role', 'max_node_count', 'min_node_count', 'is_default', 'stack_id', 'status', 'status_reason', 'node_labels', 'node_taints', ] CREATION_ATTRIBUTES = ['docker_volume_size', 'labels', 'flavor_id', 'image_id', 'project_id', 'node_count', 'name', 'role', 'min_node_count', 'max_node_count', 'merge_labels', 'node_labels', 'node_taints'] class NodeGroup(baseunit.BaseTemplate): template_name = "NodeGroups" class NodeGroupManager(baseunit.BaseTemplateManager): resource_class = NodeGroup template_name = 'nodegroups' api_name = 'nodegroups' @classmethod def _path(cls, cluster_id, id=None): path = '/v1/clusters/%s/%s/' % (cluster_id, cls.template_name) if id: path += str(id) return path def list(self, cluster_id, limit=None, marker=None, sort_key=None, sort_dir=None, role=None, detail=False): if limit is not None: limit = int(limit) filters = utils.common_filters(marker, limit, sort_key, sort_dir) path = '' if role: filters.append('role=%s' % role) if detail: path += 'detail' if filters: path += '?' + '&'.join(filters) if limit is None: return self._list(self._path(cluster_id, id=path), self.__class__.api_name) else: return self._list_pagination(self._path(cluster_id, id=path), self.__class__.api_name, limit=limit) def get(self, cluster_id, id): try: return self._list(self._path(cluster_id, id=id))[0] except IndexError: return None def create(self, cluster_id, **kwargs): new = {} for (key, value) in kwargs.items(): if key in CREATION_ATTRIBUTES: new[key] = value else: raise exceptions.InvalidAttribute( "Key must be in %s" % ",".join(CREATION_ATTRIBUTES)) return self._create(self._path(cluster_id), new) def delete(self, cluster_id, id): return self._delete(self._path(cluster_id, id=id)) def update(self, cluster_id, id, patch): return self._update(self._path(cluster_id, id=id), patch) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1374195 python_magnumclient-5.0.0/magnumclient/v1/quotas.py0000664000175000017500000000501415244246676021341 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 magnumclient.common import utils from magnumclient import exceptions from magnumclient.v1 import basemodels QUOTA_ATTRIBUTES = [ 'resource', 'created_at', 'updated_at', 'hard_limit', 'project_id', 'id', ] CREATION_ATTRIBUTES = ['project_id', 'resource', 'hard_limit'] class Quotas(basemodels.BaseModel): model_name = "Quotas" class QuotasManager(basemodels.BaseModelManager): api_name = "quotas" resource_class = Quotas @staticmethod def _path(id=None, resource=None): if not id: return '/v1/quotas' return '/v1/quotas/%(id)s/%(res)s' % {'id': id, 'res': resource} def list(self, limit=None, marker=None, sort_key=None, sort_dir=None, all_tenants=False): if limit is not None: limit = int(limit) filters = utils.common_filters(marker, limit, sort_key, sort_dir) if all_tenants: filters.append('all_tenants=True') path = self._path() if filters: path += '?' + '&'.join(filters) if limit is None: return self._list(path, self.api_name) else: return self._list_pagination(path, self.api_name, limit=limit) def get(self, id, resource): try: return self._list(self._path(id, resource))[0] except IndexError: return None def create(self, **kwargs): new = {} for (key, value) in kwargs.items(): if key in CREATION_ATTRIBUTES: new[key] = value else: raise exceptions.InvalidAttribute( "Key must be in %s" % ",".join(CREATION_ATTRIBUTES)) return self._create(self._path(), new) def delete(self, id, resource): return self._delete(self._path(id, resource)) def update(self, id, resource, patch): url = self._path(id, resource) return self._update(url, patch) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1384194 python_magnumclient-5.0.0/magnumclient/v1/stats.py0000664000175000017500000000201715244246676021163 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 magnumclient.common import base class Stats(base.Resource): def __repr__(self): return "" % self._info class StatsManager(base.Manager): resource_class = Stats @staticmethod def _path(id=None): return '/v1/stats?project_id=%s' % id if id else '/v1/stats' def list(self, project_id=None): try: return self._list(self._path(project_id))[0] except IndexError: return None ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1384194 python_magnumclient-5.0.0/magnumclient/version.py0000664000175000017500000000124215244246676021163 0ustar00zuulzuul# Copyright 2014 # The Cloudscaling Group, 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 pbr import version version_info = version.VersionInfo('python-magnumclient') ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1384194 python_magnumclient-5.0.0/pyproject.toml0000664000175000017500000000633715244246676017367 0ustar00zuulzuul[build-system] requires = ["pbr>=6.1.1"] build-backend = "pbr.build" [project] name = "python-magnumclient" description = "Client library for Magnum API" authors = [ {name = "OpenStack", email = "openstack-discuss@lists.openstack.org"}, ] readme = {file = "README.rst", content-type = "text/x-rst"} license = "Apache-2.0" dynamic = ["version", "dependencies"] requires-python = ">=3.11" classifiers = [ "Environment :: OpenStack", "Intended Audience :: Information Technology", "Intended Audience :: System Administrators", "Operating System :: POSIX :: Linux", "Programming Language :: Python", "Programming Language :: Python :: 3", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: Implementation :: CPython", ] [project.urls] Homepage = "https://docs.openstack.org/python-magnumclient/latest/" Repository = "https://opendev.org/openstack/python-magnumclient" [tool.setuptools] packages = [ "magnumclient" ] [project.entry-points."openstack.cli.extension"] container_infra = "magnumclient.osc.plugin" [project.entry-points."openstack.container_infra.v1"] coe_ca_rotate = "magnumclient.osc.v1.certificates:RotateCa" coe_ca_show = "magnumclient.osc.v1.certificates:ShowCa" coe_ca_sign = "magnumclient.osc.v1.certificates:SignCa" coe_cluster_create = "magnumclient.osc.v1.clusters:CreateCluster" coe_cluster_list = "magnumclient.osc.v1.clusters:ListCluster" coe_cluster_delete = "magnumclient.osc.v1.clusters:DeleteCluster" coe_cluster_show = "magnumclient.osc.v1.clusters:ShowCluster" coe_cluster_update = "magnumclient.osc.v1.clusters:UpdateCluster" coe_cluster_config = "magnumclient.osc.v1.clusters:ConfigCluster" coe_cluster_resize = "magnumclient.osc.v1.clusters:ResizeCluster" coe_cluster_upgrade = "magnumclient.osc.v1.clusters:UpgradeCluster" coe_cluster_template_create = "magnumclient.osc.v1.cluster_templates:CreateClusterTemplate" coe_cluster_template_delete = "magnumclient.osc.v1.cluster_templates:DeleteClusterTemplate" coe_cluster_template_list = "magnumclient.osc.v1.cluster_templates:ListTemplateCluster" coe_cluster_template_show = "magnumclient.osc.v1.cluster_templates:ShowClusterTemplate" coe_cluster_template_update = "magnumclient.osc.v1.cluster_templates:UpdateClusterTemplate" coe_credential_rotate = "magnumclient.osc.v1.credentials:RotateCredential" coe_nodegroup_list = "magnumclient.osc.v1.nodegroups:ListNodeGroup" coe_nodegroup_show = "magnumclient.osc.v1.nodegroups:ShowNodeGroup" coe_nodegroup_create = "magnumclient.osc.v1.nodegroups:CreateNodeGroup" coe_nodegroup_delete = "magnumclient.osc.v1.nodegroups:DeleteNodeGroup" coe_nodegroup_update = "magnumclient.osc.v1.nodegroups:UpdateNodeGroup" coe_quotas_create = "magnumclient.osc.v1.quotas:CreateQuotas" coe_quotas_delete = "magnumclient.osc.v1.quotas:DeleteQuotas" coe_quotas_update = "magnumclient.osc.v1.quotas:UpdateQuotas" coe_quotas_show = "magnumclient.osc.v1.quotas:ShowQuotas" coe_quotas_list = "magnumclient.osc.v1.quotas:ListQuotas" coe_service_list = "magnumclient.osc.v1.mservices:ListService" coe_stats_list = "magnumclient.osc.v1.stats:ListStats" ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0095785 python_magnumclient-5.0.0/python_magnumclient.egg-info/0000775000175000017500000000000015244246764022216 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9405775 python_magnumclient-5.0.0/python_magnumclient.egg-info/PKG-INFO0000644000175000017500000000522415244246764023314 0ustar00zuulzuulMetadata-Version: 2.4 Name: python-magnumclient Version: 5.0.0 Summary: Client library for Magnum API Author-email: OpenStack License-Expression: Apache-2.0 Project-URL: Homepage, https://docs.openstack.org/python-magnumclient/latest/ Project-URL: Repository, https://opendev.org/openstack/python-magnumclient Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.11 Classifier: Programming Language :: Python :: 3.12 Classifier: Programming Language :: Python :: 3.13 Classifier: Programming Language :: Python :: 3.14 Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: Implementation :: CPython Requires-Python: >=3.11 Description-Content-Type: text/x-rst License-File: LICENSE Requires-Dist: pbr>=6.1.1 Requires-Dist: keystoneauth1>=3.4.0 Requires-Dist: stevedore>=1.20.0 Requires-Dist: requests>=2.14.2 Requires-Dist: oslo.i18n>=3.15.3 Requires-Dist: oslo.log>=3.36.0 Requires-Dist: oslo.serialization!=2.19.1,>=2.18.0 Requires-Dist: oslo.utils>=3.33.0 Requires-Dist: openstacksdk>=0.10.0 Requires-Dist: osc-lib>=1.8.0 Requires-Dist: PrettyTable>=0.7.2 Requires-Dist: cryptography>=3.0 Requires-Dist: decorator>=3.4.0 Dynamic: license-file Dynamic: requires-dist ================================= Python bindings to the Magnum API ================================= .. image:: https://governance.openstack.org/tc/badges/python-magnumclient.svg .. image:: https://img.shields.io/pypi/v/python-magnumclient.svg :target: https://pypi.org/project/python-magnumclient/ :alt: Latest Version .. image:: https://img.shields.io/pypi/dm/python-magnumclient.svg :target: https://pypi.org/project/python-magnumclient/ :alt: Downloads This is a client library for Magnum built on the Magnum API. It provides a Python API (the ``magnumclient`` module) and a command-line tool (``magnum``). Development takes place via the usual OpenStack processes as outlined in the `developer guide `_. * License: Apache License, Version 2.0 * `PyPi`_ - package installation * `Online Documentation`_ * `Bugs`_ - issue tracking * `Source`_ .. _PyPi: https://pypi.org/project/python-magnumclient .. _Online Documentation: https://docs.openstack.org/python-magnumclient/latest/ .. _Bugs: https://bugs.launchpad.net/magnum .. _Source: https://opendev.org/openstack/python-magnumclient ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9755778 python_magnumclient-5.0.0/python_magnumclient.egg-info/SOURCES.txt0000664000175000017500000001003515244246764024101 0ustar00zuulzuul.coveragerc .mailmap .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog LICENSE README.rst pyproject.toml requirements.txt setup.cfg setup.py test-requirements.txt tox.ini doc/requirements.txt doc/source/conf.py doc/source/contributing.rst doc/source/index.rst doc/source/installation.rst doc/source/readme.rst doc/source/usage.rst magnumclient/__init__.py magnumclient/client.py magnumclient/exceptions.py magnumclient/i18n.py magnumclient/version.py magnumclient/common/__init__.py magnumclient/common/base.py magnumclient/common/cliutils.py magnumclient/common/httpclient.py magnumclient/common/utils.py magnumclient/osc/__init__.py magnumclient/osc/plugin.py magnumclient/osc/v1/__init__.py magnumclient/osc/v1/certificates.py magnumclient/osc/v1/cluster_templates.py magnumclient/osc/v1/clusters.py magnumclient/osc/v1/credentials.py magnumclient/osc/v1/mservices.py magnumclient/osc/v1/nodegroups.py magnumclient/osc/v1/quotas.py magnumclient/osc/v1/stats.py magnumclient/tests/__init__.py magnumclient/tests/base.py magnumclient/tests/test_client.py magnumclient/tests/test_httpclient.py magnumclient/tests/test_magnumclient.py magnumclient/tests/test_utils.py magnumclient/tests/utils.py magnumclient/tests/osc/__init__.py magnumclient/tests/osc/unit/__init__.py magnumclient/tests/osc/unit/osc_fakes.py magnumclient/tests/osc/unit/osc_utils.py magnumclient/tests/osc/unit/test_plugin.py magnumclient/tests/osc/unit/v1/__init__.py magnumclient/tests/osc/unit/v1/fakes.py magnumclient/tests/osc/unit/v1/test_certificates.py magnumclient/tests/osc/unit/v1/test_cluster_templates.py magnumclient/tests/osc/unit/v1/test_clusters.py magnumclient/tests/osc/unit/v1/test_mservices.py magnumclient/tests/osc/unit/v1/test_nodegroups.py magnumclient/tests/osc/unit/v1/test_quotas.py magnumclient/tests/osc/unit/v1/test_stats.py magnumclient/tests/test_csr/test.csr magnumclient/tests/v1/__init__.py magnumclient/tests/v1/test_certificates.py magnumclient/tests/v1/test_client.py magnumclient/tests/v1/test_clusters.py magnumclient/tests/v1/test_clustertemplates.py magnumclient/tests/v1/test_mservices.py magnumclient/tests/v1/test_nodegroups.py magnumclient/tests/v1/test_quotas.py magnumclient/tests/v1/test_stats.py magnumclient/v1/__init__.py magnumclient/v1/basemodels.py magnumclient/v1/baseunit.py magnumclient/v1/certificates.py magnumclient/v1/client.py magnumclient/v1/cluster_templates.py magnumclient/v1/clusters.py magnumclient/v1/credentials.py magnumclient/v1/mservices.py magnumclient/v1/nodegroups.py magnumclient/v1/quotas.py magnumclient/v1/stats.py python_magnumclient.egg-info/PKG-INFO python_magnumclient.egg-info/SOURCES.txt python_magnumclient.egg-info/dependency_links.txt python_magnumclient.egg-info/entry_points.txt python_magnumclient.egg-info/not-zip-safe python_magnumclient.egg-info/pbr.json python_magnumclient.egg-info/requires.txt python_magnumclient.egg-info/top_level.txt releasenotes/notes/add-ct-tags-argument-3129c5038e95757e.yaml releasenotes/notes/bug-1870264-1b4f14618b8422f9.yaml releasenotes/notes/credential-rotate-02cc95069e3329fb.yaml releasenotes/notes/drop-bay-and-baymodel-79afa8d8161687ef.yaml releasenotes/notes/nodegroup-update-labels-taints-0d8c4b6e2f1a3c5d.yaml releasenotes/notes/partial_osc_implementation_for_certificate-4597c20b59c152e1.yaml releasenotes/notes/partial_osc_implementation_for_quotas-33f44c0496d721f8.yaml releasenotes/notes/rework-coe-cluster-set-ux-ddcbb30dfa0d336f.yaml releasenotes/source/2023.1.rst releasenotes/source/2023.2.rst releasenotes/source/2024.1.rst releasenotes/source/2024.2.rst releasenotes/source/2025.1.rst releasenotes/source/2025.2.rst releasenotes/source/2026.1.rst releasenotes/source/conf.py releasenotes/source/index.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/locale/en_GB/LC_MESSAGES/releasenotes.po tools/magnum.bash_completion././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9405775 python_magnumclient-5.0.0/python_magnumclient.egg-info/dependency_links.txt0000664000175000017500000000000115244246764026264 0ustar00zuulzuul ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9425774 python_magnumclient-5.0.0/python_magnumclient.egg-info/entry_points.txt0000664000175000017500000000376315244246764025525 0ustar00zuulzuul[openstack.cli.extension] container_infra = magnumclient.osc.plugin [openstack.container_infra.v1] coe_ca_rotate = magnumclient.osc.v1.certificates:RotateCa coe_ca_show = magnumclient.osc.v1.certificates:ShowCa coe_ca_sign = magnumclient.osc.v1.certificates:SignCa coe_cluster_config = magnumclient.osc.v1.clusters:ConfigCluster coe_cluster_create = magnumclient.osc.v1.clusters:CreateCluster coe_cluster_delete = magnumclient.osc.v1.clusters:DeleteCluster coe_cluster_list = magnumclient.osc.v1.clusters:ListCluster coe_cluster_resize = magnumclient.osc.v1.clusters:ResizeCluster coe_cluster_show = magnumclient.osc.v1.clusters:ShowCluster coe_cluster_template_create = magnumclient.osc.v1.cluster_templates:CreateClusterTemplate coe_cluster_template_delete = magnumclient.osc.v1.cluster_templates:DeleteClusterTemplate coe_cluster_template_list = magnumclient.osc.v1.cluster_templates:ListTemplateCluster coe_cluster_template_show = magnumclient.osc.v1.cluster_templates:ShowClusterTemplate coe_cluster_template_update = magnumclient.osc.v1.cluster_templates:UpdateClusterTemplate coe_cluster_update = magnumclient.osc.v1.clusters:UpdateCluster coe_cluster_upgrade = magnumclient.osc.v1.clusters:UpgradeCluster coe_credential_rotate = magnumclient.osc.v1.credentials:RotateCredential coe_nodegroup_create = magnumclient.osc.v1.nodegroups:CreateNodeGroup coe_nodegroup_delete = magnumclient.osc.v1.nodegroups:DeleteNodeGroup coe_nodegroup_list = magnumclient.osc.v1.nodegroups:ListNodeGroup coe_nodegroup_show = magnumclient.osc.v1.nodegroups:ShowNodeGroup coe_nodegroup_update = magnumclient.osc.v1.nodegroups:UpdateNodeGroup coe_quotas_create = magnumclient.osc.v1.quotas:CreateQuotas coe_quotas_delete = magnumclient.osc.v1.quotas:DeleteQuotas coe_quotas_list = magnumclient.osc.v1.quotas:ListQuotas coe_quotas_show = magnumclient.osc.v1.quotas:ShowQuotas coe_quotas_update = magnumclient.osc.v1.quotas:UpdateQuotas coe_service_list = magnumclient.osc.v1.mservices:ListService coe_stats_list = magnumclient.osc.v1.stats:ListStats ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.4215705 python_magnumclient-5.0.0/python_magnumclient.egg-info/not-zip-safe0000664000175000017500000000000115244246763024443 0ustar00zuulzuul ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9395773 python_magnumclient-5.0.0/python_magnumclient.egg-info/pbr.json0000664000175000017500000000005615244246764023675 0ustar00zuulzuul{"git_version": "43fc0de", "is_release": true}././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9425774 python_magnumclient-5.0.0/python_magnumclient.egg-info/requires.txt0000664000175000017500000000036715244246764024624 0ustar00zuulzuulpbr>=6.1.1 keystoneauth1>=3.4.0 stevedore>=1.20.0 requests>=2.14.2 oslo.i18n>=3.15.3 oslo.log>=3.36.0 oslo.serialization!=2.19.1,>=2.18.0 oslo.utils>=3.33.0 openstacksdk>=0.10.0 osc-lib>=1.8.0 PrettyTable>=0.7.2 cryptography>=3.0 decorator>=3.4.0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907571.9425774 python_magnumclient-5.0.0/python_magnumclient.egg-info/top_level.txt0000664000175000017500000000001515244246764024744 0ustar00zuulzuulmagnumclient ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1787907571.985578 python_magnumclient-5.0.0/releasenotes/0000775000175000017500000000000015244246764017131 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0125785 python_magnumclient-5.0.0/releasenotes/notes/0000775000175000017500000000000015244246764020261 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1384194 python_magnumclient-5.0.0/releasenotes/notes/add-ct-tags-argument-3129c5038e95757e.yaml0000664000175000017500000000051715244246676027074 0ustar00zuulzuul--- features: - | When creating a cluster template the administrator can use --tags argument to add any information that he considers important. The received text is a comma separated list with the pretended tags. This information is also shown when the user lists all the available cluster templates. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1384194 python_magnumclient-5.0.0/releasenotes/notes/bug-1870264-1b4f14618b8422f9.yaml0000664000175000017500000000026115244246676024633 0ustar00zuulzuul--- fixes: - | [`bug 1870264 `_] Fixed bug where the error message from the response could not be parsed. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1394193 python_magnumclient-5.0.0/releasenotes/notes/credential-rotate-02cc95069e3329fb.yaml0000664000175000017500000000022715244246676026716 0ustar00zuulzuul--- features: - | Implemented OpenStack command for credential-rotate which can be invoked with `openstack coe credential rotate `. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1394193 python_magnumclient-5.0.0/releasenotes/notes/drop-bay-and-baymodel-79afa8d8161687ef.yaml0000664000175000017500000000011115244246676027450 0ustar00zuulzuul--- deprecations: - | Remove support for bays and baymodels. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1394193 python_magnumclient-5.0.0/releasenotes/notes/nodegroup-update-labels-taints-0d8c4b6e2f1a3c5d.yaml0000664000175000017500000000103415244246676031535 0ustar00zuulzuul--- features: - | The ``openstack coe nodegroup update`` command gained ``--node-labels`` and ``--node-taints`` flags for updating the Kubernetes node labels and node taints on an existing nodegroup, e.g.:: openstack coe nodegroup update \ --node-labels workload=gpu \ --node-taints workload=gpu:NoSchedule \ Each flag replaces the corresponding field in full and may be repeated. Passing an empty value (e.g. ``--node-labels ''``) clears the field. ././@PaxHeader0000000000000000000000000000022300000000000010212 xustar00119 path=python_magnumclient-5.0.0/releasenotes/notes/partial_osc_implementation_for_certificate-4597c20b59c152e1.yaml 28 mtime=1787907518.1394193 python_magnumclient-5.0.0/releasenotes/notes/partial_osc_implementation_for_certificate-4597c20b59c10000664000175000017500000000015215244246676032500 0ustar00zuulzuul--- features: - | Implemented Openstack command for ca-show, ca-sign, ca-rotate and stats-list. ././@PaxHeader0000000000000000000000000000021600000000000010214 xustar00114 path=python_magnumclient-5.0.0/releasenotes/notes/partial_osc_implementation_for_quotas-33f44c0496d721f8.yaml 28 mtime=1787907518.1394193 python_magnumclient-5.0.0/releasenotes/notes/partial_osc_implementation_for_quotas-33f44c0496d721f8.0000664000175000017500000000021315244246676032212 0ustar00zuulzuul--- features: - | Implemented Openstack command for quotas-create, quotas-update, quotas-delete, quotas-show and quotas-list ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1394193 python_magnumclient-5.0.0/releasenotes/notes/rework-coe-cluster-set-ux-ddcbb30dfa0d336f.yaml0000664000175000017500000000264715244246676030637 0ustar00zuulzuul--- features: - | ``openstack coe cluster set``, ``openstack coe nodegroup set``, and ``openstack coe cluster template set`` now accept named flags instead of the previous JSON Patch positional arguments (`` ``). ``coe cluster set`` gains ``--name``, ``--node-count``, ``--master-count``, ``--label `` (repeatable, adds or replaces a single label), ``--no-label `` (repeatable, removes a single label), and ``--rollback``. The ``--rollback`` flag is also now correctly forwarded to the API, fixing a pre-existing silent bug. ``coe nodegroup set`` gains ``--min-nodes`` and ``--max-nodes`` (the only fields the Magnum API permits patching on a nodegroup; node count changes go through the resize action). ``coe cluster template set`` gains a full set of named flags matching the create command, including ``--label``/``--no-label`` for per-key label operations, ``--tag`` for tags, and boolean ``--X``/``--no-X`` pairs for toggles such as ``--public``/``--private``, ``--hidden``/``--visible``, ``--tls-disabled``/``--tls-enabled``, etc. upgrade: - | The ``openstack coe cluster set``, ``openstack coe nodegroup set``, and ``openstack coe cluster template set`` commands no longer accept the positional ```` and ```` arguments. Callers must migrate to the new named flags described in the features section above. ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0165784 python_magnumclient-5.0.0/releasenotes/source/0000775000175000017500000000000015244246764020431 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1394193 python_magnumclient-5.0.0/releasenotes/source/2023.1.rst0000664000175000017500000000021015244246676021703 0ustar00zuulzuul=========================== 2023.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2023.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1394193 python_magnumclient-5.0.0/releasenotes/source/2023.2.rst0000664000175000017500000000020215244246676021705 0ustar00zuulzuul=========================== 2023.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2023.2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1404195 python_magnumclient-5.0.0/releasenotes/source/2024.1.rst0000664000175000017500000000021015244246676021704 0ustar00zuulzuul=========================== 2024.1 Series Release Notes =========================== .. release-notes:: :branch: unmaintained/2024.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1404195 python_magnumclient-5.0.0/releasenotes/source/2024.2.rst0000664000175000017500000000020215244246676021706 0ustar00zuulzuul=========================== 2024.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1414194 python_magnumclient-5.0.0/releasenotes/source/2025.1.rst0000664000175000017500000000020215244246676021706 0ustar00zuulzuul=========================== 2025.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1414194 python_magnumclient-5.0.0/releasenotes/source/2025.2.rst0000664000175000017500000000020215244246676021707 0ustar00zuulzuul=========================== 2025.2 Series Release Notes =========================== .. release-notes:: :branch: stable/2025.2 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1424193 python_magnumclient-5.0.0/releasenotes/source/2026.1.rst0000664000175000017500000000020215244246676021707 0ustar00zuulzuul=========================== 2026.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2026.1 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1424193 python_magnumclient-5.0.0/releasenotes/source/conf.py0000664000175000017500000002174015244246676021736 0ustar00zuulzuul# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. # Magnum-Client documentation build configuration file, created by # sphinx-quickstart on Tue Dec 20 11:53:00 2016. # # This file is execfile()d with the current directory set to its # containing dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration ------------------------------------------------ # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom # ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'MagnumClientReleaseNotes' copyright = '2016, OpenStack Foundation' # Release notes are version independent. # The short X.Y version. version = '' # The full version, including alpha/beta/rc tags. 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 = 'sphinx' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # If true, keep warnings as "system message" paragraphs in the built documents. # keep_warnings = False # -- Options for HTML output ---------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # Add any extra paths that contain custom files (such as robots.txt or # .htaccess) here, relative to this directory. These files are copied # directly to the root of the documentation. # html_extra_path = [] # If 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 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 = 'MagnumClientReleaseNotesdoc' # -- Options for LaTeX output --------------------------------------------- # The item in 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': '', latex_elements = {} # 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', 'MagnumClientReleaseNotes.tex', 'MagnumClient ReleaseNotes Documentation', 'OpenStack Foundation', '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', 'MagnumClientReleaseNotes', 'MagnumClient ReleaseNotes Documentation', ['OpenStack Foundation'], 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', 'MagnumClientReleaseNotes', 'MagnumClient ReleaseNotes Documentation', 'OpenStack Foundation', 'MagnumClientReleaseNotes', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # If true, do not generate a @detailmenu in the "Top" node's menu. # texinfo_no_detailmenu = False # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] # -- Options for openstackdocstheme ------------------------------------------- openstackdocs_repo_name = 'openstack/python-magnumclient' openstackdocs_bug_project = 'python-magnumclient' openstackdocs_bug_tag = '' openstackdocs_auto_name = False ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1424193 python_magnumclient-5.0.0/releasenotes/source/index.rst0000664000175000017500000000155015244246676022275 0ustar00zuulzuul============================ Magnum Client Release Notes ============================ .. toctree:: :maxdepth: 1 unreleased 2026.1 2025.2 2025.1 2024.2 2024.1 2023.2 2023.1 zed yoga xena wallaby victoria ussuri train stein rocky OpenStack Releases ------------------ The MagnumClient release that was current when the corresponding OpenStack release was made is shown below: ================= ==================== OpenStack Release MagnumClient Release ================= ==================== Rocky 2.10.0 ================= ==================== Further details for historical OpenStack releases are found at the `OpenStack Releases`_ page. .. _`OpenStack Releases`: https://releases.openstack.org/ Indices and tables ================== * :ref:`genindex` * :ref:`modindex` * :ref:`search` ././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1787907571.985578 python_magnumclient-5.0.0/releasenotes/source/locale/0000775000175000017500000000000015244246764021670 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003300000000000010211 xustar0027 mtime=1787907571.985578 python_magnumclient-5.0.0/releasenotes/source/locale/en_GB/0000775000175000017500000000000015244246764022642 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0165784 python_magnumclient-5.0.0/releasenotes/source/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000015244246764024427 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1434195 python_magnumclient-5.0.0/releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po0000664000175000017500000000714015244246676027464 0ustar00zuulzuul# Andi Chandler , 2022. #zanata msgid "" msgstr "" "Project-Id-Version: MagnumClientReleaseNotes\n" "Report-Msgid-Bugs-To: \n" "POT-Creation-Date: 2022-05-11 09:57+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2022-06-13 07:37+0000\n" "Last-Translator: Andi Chandler \n" "Language-Team: English (United Kingdom)\n" "Language: en_GB\n" "X-Generator: Zanata 4.3.3\n" "Plural-Forms: nplurals=2; plural=(n != 1)\n" msgid "2.10.0" msgstr "2.10.0" msgid "3.1.0" msgstr "3.1.0" msgid "3.4.0" msgstr "3.4.0" msgid ":ref:`genindex`" msgstr ":ref:`genindex`" msgid ":ref:`modindex`" msgstr ":ref:`modindex`" msgid ":ref:`search`" msgstr ":ref:`search`" msgid "Bug Fixes" msgstr "Bug Fixes" msgid "Current Release Notes" msgstr "Current Release Notes" msgid "" "Further details for historical OpenStack releases are found at the " "`OpenStack Releases`_ page." msgstr "" "Further details for historical OpenStack releases are found at the " "`OpenStack Releases`_ page." msgid "" "Implemented Openstack command for ca-show, ca-sign, ca-rotate and stats-list." msgstr "" "Implemented Openstack command for ca-show, ca-sign, ca-rotate and stats-list." msgid "" "Implemented Openstack command for quotas-create, quotas-update, quotas-" "delete, quotas-show and quotas-list" msgstr "" "Implemented Openstack command for quotas-create, quotas-update, quotas-" "delete, quotas-show and quotas-list" msgid "Indices and tables" msgstr "Indices and tables" msgid "Magnum Client Release Notes" msgstr "Magnum Client Release Notes" msgid "MagnumClient Release" msgstr "MagnumClient Release" msgid "New Features" msgstr "New Features" msgid "OpenStack Release" msgstr "OpenStack Release" msgid "OpenStack Releases" msgstr "OpenStack Releases" msgid "Rocky" msgstr "Rocky" msgid "Rocky Series Release Notes" msgstr "Rocky Series Release Notes" msgid "Stein Series Release Notes" msgstr "Stein Series Release Notes" msgid "" "The MagnumClient release that was current when the corresponding OpenStack " "release was made is shown below:" msgstr "" "The MagnumClient release that was current when the corresponding OpenStack " "release was made is shown below:" msgid "Train Series Release Notes" msgstr "Train Series Release Notes" msgid "Ussuri Series Release Notes" msgstr "Ussuri Series Release Notes" msgid "Victoria Series Release Notes" msgstr "Victoria Series Release Notes" msgid "Wallaby Series Release Notes" msgstr "Wallaby Series Release Notes" msgid "" "When creating a cluster template the administrator can use --tags " "argument to add any information that he considers important. The received " "text is a comma separated list with the pretended tags. This information is " "also shown when the user lists all the available cluster templates." msgstr "" "When creating a cluster template the administrator can use --tags " "argument to add any information that he considers important. The received " "text is a comma-separated list with the pretended tags. This information is " "also shown when the user lists all the available cluster templates." msgid "Xena Series Release Notes" msgstr "Xena Series Release Notes" msgid "Yoga Series Release Notes" msgstr "Yoga Series Release Notes" msgid "" "[`bug 1870264 `_] Fixed bug where the error message from the response could " "not be parsed." msgstr "" "[`bug 1870264 `_] Fixed bug where the error message from the response could " "not be parsed." ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1434195 python_magnumclient-5.0.0/releasenotes/source/rocky.rst0000664000175000017500000000022115244246676022307 0ustar00zuulzuul=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1434195 python_magnumclient-5.0.0/releasenotes/source/stein.rst0000664000175000017500000000022115244246676022302 0ustar00zuulzuul=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1434195 python_magnumclient-5.0.0/releasenotes/source/train.rst0000664000175000017500000000017615244246676022306 0ustar00zuulzuul========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1434195 python_magnumclient-5.0.0/releasenotes/source/unreleased.rst0000664000175000017500000000012615244246676023313 0ustar00zuulzuul===================== Current Release Notes ===================== .. release-notes:: ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1434195 python_magnumclient-5.0.0/releasenotes/source/ussuri.rst0000664000175000017500000000020215244246676022511 0ustar00zuulzuul=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1434195 python_magnumclient-5.0.0/releasenotes/source/victoria.rst0000664000175000017500000000021215244246676023000 0ustar00zuulzuul============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: stable/victoria ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1434195 python_magnumclient-5.0.0/releasenotes/source/wallaby.rst0000664000175000017500000000020615244246676022616 0ustar00zuulzuul============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: stable/wallaby ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1434195 python_magnumclient-5.0.0/releasenotes/source/xena.rst0000664000175000017500000000017215244246676022120 0ustar00zuulzuul========================= Xena Series Release Notes ========================= .. release-notes:: :branch: stable/xena ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1434195 python_magnumclient-5.0.0/releasenotes/source/yoga.rst0000664000175000017500000000020015244246676022114 0ustar00zuulzuul========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: unmaintained/yoga ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1434195 python_magnumclient-5.0.0/releasenotes/source/zed.rst0000664000175000017500000000017415244246676021751 0ustar00zuulzuul======================== Zed Series Release Notes ======================== .. release-notes:: :branch: unmaintained/zed ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1444194 python_magnumclient-5.0.0/requirements.txt0000664000175000017500000000117215244246676017727 0ustar00zuulzuul# Requirements lower bounds listed here are our best effort to keep them up to # date but we do not test them so no guarantee of having them all correct. If # you find any incorrect lower bounds, let us know or propose a fix. pbr>=6.1.1 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0 stevedore>=1.20.0 # Apache-2.0 requests>=2.14.2 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.log>=3.36.0 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 openstacksdk>=0.10.0 # Apache-2.0 osc-lib>=1.8.0 # Apache-2.0 PrettyTable>=0.7.2 # BSD cryptography>=3.0 # BSD/Apache-2.0 decorator>=3.4.0 # BSD ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0185785 python_magnumclient-5.0.0/setup.cfg0000664000175000017500000000011515244246764016256 0ustar00zuulzuul[metadata] name = python-magnumclient [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1444194 python_magnumclient-5.0.0/setup.py0000664000175000017500000000122115244246676016150 0ustar00zuulzuul# 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(pbr=True) ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1444194 python_magnumclient-5.0.0/test-requirements.txt0000664000175000017500000000055015244246676020703 0ustar00zuulzuulhacking>=8.1.0,<8.2.0 # Apache-2.0 bandit!=1.6.0,>=1.1.0 # Apache-2.0 coverage!=4.4,>=4.0 # Apache-2.0 fixtures>=3.0.0 # Apache-2.0/BSD python-openstackclient>=3.12.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 stestr>=2.0.0 # Apache-2.0 testscenarios>=0.4 # Apache-2.0/BSD testtools>=2.2.0 # MIT ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907572.0165784 python_magnumclient-5.0.0/tools/0000775000175000017500000000000015244246764015600 5ustar00zuulzuul././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1444194 python_magnumclient-5.0.0/tools/magnum.bash_completion0000664000175000017500000000211315244246676022153 0ustar00zuulzuul_magnum_opts="" # lazy init _magnum_flags="" # lazy init _magnum_opts_exp="" # lazy init _magnum() { local cur prev nbc cflags COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" if [ "x$_magnum_opts" == "x" ] ; then nbc="`magnum bash-completion | sed -e "s/ *-h */ /" -e "s/ *-i */ /"`" _magnum_opts="`echo "$nbc" | sed -e "s/--[a-z0-9_-]*//g" -e "s/ */ /g"`" _magnum_flags="`echo " $nbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/ */ /g"`" _magnum_opts_exp="`echo "$_magnum_opts" | tr ' ' '|'`" fi if [[ " ${COMP_WORDS[@]} " =~ " "($_magnum_opts_exp)" " && "$prev" != "help" ]] ; then COMPLETION_CACHE=~/.magnumclient/*/*-cache cflags="$_magnum_flags "$(cat $COMPLETION_CACHE 2> /dev/null | tr '\n' ' ') COMPREPLY=($(compgen -W "${cflags}" -- ${cur})) else COMPREPLY=($(compgen -W "${_magnum_opts}" -- ${cur})) fi return 0 } complete -F _magnum magnum ././@PaxHeader0000000000000000000000000000003400000000000010212 xustar0028 mtime=1787907518.1454194 python_magnumclient-5.0.0/tox.ini0000664000175000017500000000342415244246676015760 0ustar00zuulzuul[tox] minversion = 3.18 envlist = py3,pep8 [testenv] usedevelop = True allowlist_externals = find setenv = VIRTUAL_ENV={envdir} PYTHONWARNINGS=default::DeprecationWarning deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = find . -type f -name "*.py[c|o]" -delete stestr run --slowest {posargs} [testenv:bandit] basepython = python3 deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/test-requirements.txt commands = bandit -r magnumclient -x tests -n5 -ll [testenv:debug] basepython = python3 commands = oslo_debug_helper -t magnumclient/tests {posargs} [testenv:pep8] basepython = python3 commands = flake8 # Run security linter bandit -r magnumclient -x tests -n5 -ll [testenv:docs] basepython = python3 deps = -r{toxinidir}/doc/requirements.txt commands = sphinx-build -W -b html doc/source doc/build/html [testenv:venv] basepython = python3 commands = {posargs} [testenv:cover] basepython = python3 setenv = PYTHON=coverage run --source magnumclient --parallel-mode commands = stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml coverage report [flake8] # E123, E125 skipped as they are invalid PEP-8. show-source = True ignore = E123,E125,W503,W504 builtins = _ exclude=.venv,.git,.tox,dist,doc,,*lib/python*,*egg,build [hacking] import_exceptions = magnumclient._i18n [testenv:releasenotes] basepython = python3 deps = -r{toxinidir}/doc/requirements.txt allowlist_externals = rm commands = rm -rf releasenotes/build sphinx-build -a -E -W -d releasenotes/build/doctrees -b html releasenotes/source releasenotes/build/html