././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.6220894 python-muranoclient-2.8.0/0000775000175000017500000000000000000000000015563 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/.coveragerc0000664000175000017500000000014000000000000017677 0ustar00zuulzuul00000000000000[run] source = muranoclient omit = .tox/* muranoclient/tests/* [report] ignore_errors = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/.stestr.conf0000664000175000017500000000011300000000000020027 0ustar00zuulzuul00000000000000[DEFAULT] test_path=${OS_TEST_PATH:-./muranoclient/tests/unit} top_dir=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/.zuul.yaml0000664000175000017500000000207400000000000017527 0ustar00zuulzuul00000000000000- project: templates: - openstack-cover-jobs - openstack-python3-jobs - check-requirements - release-notes-jobs-python3 - publish-openstack-docs-pti - openstackclient-plugin-jobs check: jobs: - muranoclient-functional-test-mysql-backend gate: jobs: - muranoclient-functional-test-mysql-backend - job: name: muranoclient-functional-test-mysql-backend parent: devstack-tox-functional timeout: 4200 voting: false vars: openrc_enable_export: true devstack_plugins: heat: https://opendev.org/openstack/heat murano: https://opendev.org/openstack/murano devstack_localrc: KEYSTONE_ADMIN_ENDPOINT: true irrelevant-files: - ^(test-|)requirements.txt$ - ^setup.cfg$ - ^doc/.*$ - ^.*\.rst$ - ^releasenotes/.*$ - ^muranoclient/tests/.*$ required-projects: - openstack/heat - openstack/murano - openstack/murano-dashboard - openstack/python-heatclient - openstack/python-muranoclient ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301724.0 python-muranoclient-2.8.0/AUTHORS0000664000175000017500000001237000000000000016636 0ustar00zuulzuul0000000000000098k <18552437190@163.com> Aaron-DH Alexander Koryagin Alexander Shlykov Alexander Tivelkov Anastasia Kuznetsova Andreas Jaeger Andrew Pashkin Andy Botting Anh Tran Ankur Rishi Artem Tiumentcev AvnishPal Bertrand Lallau Cao Xuan Hoang Chen Christian Berendt Corey Bryant Dai Dang Van Dmitry Teselkin Dmytro Dovbii Doug Hellmann Ekaterina Chernova Ekaterina Fedorova Ellen Batbouta Elod Illes Felipe Monteiro Feng Shengqin Filip Blaha Flavio Percoco ForestLee Georgiy Okrokvertskhov Georgy Dyuldin Georgy Okrokvertskhov Ghanshyam Mann Hangdong Zhang Henar Muñoz Frutos Hervé Beraud Hidekazu Nakamura Ian Wienand Igor Yozhikov Ilya Popov Ivan Udovichenko Jacek Tomasiak Janonymous Jeremy Stanley Jesus Perez Jose Phillips Junyuan Leng KATO Tomoyuki Kirill Zaitsev Konstantin Snihyr Lin Yang LiuNanke Longgeek Luigi Toscano M V P Nitesh MStolyarenko Maria Zlatkova Nam Nguyen Hoai Nguyen Hai Nikolay Mahotkin Nikolay Starodubtsev OlehBaran Oleksii Chuprykov Olivier Lemasle Omar Shykhkerimov OpenStack Release Bot Ravi Shekhar Jethani Rui Chen Ruslan Kamaldinov Ryan Peters Sascha Peilicke Sean McGinnis Serg Melikyan Serg Melikyan Sergey Melikyan Sergey Murashov Sergey Turivnyi Sergey Vilgelm Stan Lagun Stan Lagun Stanislav Lagun Stephen Finucane Steve Martinelli Steve Martinelli Steve McLellan Steve McLellan Swapnil Kulkarni (coolsvap) Tang Chen Tatyana Kuterina Tetiana Lashchova Timur Nurlygayanov Timur Sufiev TimurNurlygayanov Tovin Seven Valerii Kovalchuk Victor Ryzhenkin Vu Cong Tuan XiaojueGuan Yosef Hoffman bhagyashris bharaththiruveedula chenaidong1 enthurohini gecong1973 gugug howardlee hparekh huang.zhiping huangsm jacky06 leizhang lidong lingyongxu liyingjun liyingjun ljhuang luqitao malei melissaml pawnesh.kumar pengyuesheng qingszhao ricolin ricolin srushti sslypushenko venkatamahesh visitor wu.chunyang wu.shiming xiangxinyong yuyafei zhangyanxian zhu.rong zhurong ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/CONTRIBUTING.rst0000664000175000017500000000121300000000000020221 0ustar00zuulzuul00000000000000The source repository for this project can be found at: https://opendev.org/openstack/python-muranoclient Pull requests submitted through GitHub are not monitored. To start contributing to OpenStack, follow the steps in the contribution guide to set up and use Gerrit: https://docs.openstack.org/contributors/code-and-documentation/quick-start.html Bugs should be filed on Launchpad: https://bugs.launchpad.net/python-muranoclient For more specific information about contributing to this repository, see the python-muranoclient contributor guide: https://docs.openstack.org/python-muranoclient/latest/contributor/contributing.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301724.0 python-muranoclient-2.8.0/ChangeLog0000664000175000017500000011323300000000000017340 0ustar00zuulzuul00000000000000CHANGES ======= 2.8.0 ----- * Fix gate * Imported Translations from Zanata * Update master for stable/2023.1 2.6.0 ----- * Fix tox 4 compatibility * Switch to 2023.1 Python3 unit tests and generic template name * Update master for stable/zed 2.5.0 ----- * Add Python3 zed unit tests * Imported Translations from Zanata * Update master for stable/yoga 2.4.1 ----- * Fix tempest job * Remove unused scripts * Add Python3 yoga unit tests 2.4.0 ----- * requirements: Uncap prettytable, remove l-c * Update master for stable/xena 2.3.0 ----- * [community goal] Update contributor documentation * Add Python3 xena unit tests * Update master for stable/wallaby 2.2.0 ----- * Remove install unnecessary packages * Add Python3 wallaby unit tests * Update master for stable/victoria 2.1.1 ----- * Native Zuul v3 version of the functional legacy job * [goal] Migrate testing to ubuntu focal * Replace assertItemsEqual with assertCountEqual * Small cleanups 2.1.0 ----- * drop mock from lower-constraints * Stop to use the \_\_future\_\_ module * Switch to newer openstackdocstheme and reno versions * Fix hacking min version to 3.0.1 * Cleanup py27 support * Bump default tox env from py37 to py38 * Add py38 package metadata * Remove Python 2.4 compat code * Use unittest.mock instead of third party mock * Remove six usage tests/functional * Remove six usage muranoclient/v1 * Remove six usage * Remove six usage muranoclient/osc * Remove six usage muranoclient/glance * Remove six usage muranoclient/apiclient * Remove six usage muranoclient/common * Add Python3 victoria unit tests * Update master for stable/ussuri 2.0.1 ----- * Cleanup py27 support * Update to hacking 3.0 * Update hacking for Python3 2.0.0 ----- * Fix function tests failed in python3 * Fix muranoclient-functional-test-mysql-backend failed * [ussuri][goal] Drop python 2.7 support and testing * Update master for stable/train 1.3.0 ----- * Bump the openstackdocstheme extension to 1.20 * Blacklist sphinx 2.1.0 (autodoc bug) * Add Python 3 Train unit tests * Update contraints url * Bump the sphinx to 1.6.2 for lower-constraints.txt * Add irrelevant-files for muranoclient-functional-test-mysql-backend job * Fix sphinx requirements * Replace git.openstack.org URLs with opendev.org URLs * OpenDev Migration Patch * Dropping the py35 testing * Replace openstack.org git:// URLs with https:// * Update master for stable/stein 1.2.0 ----- * add python 3.7 unit test job * Change openstack-dev to openstack-discuss * Add Python 3.6 classifier to setup.cfg * Update min tox version to 2.0 * Use standard cover tox env * Use templates for cover and lower-constraints * add python 3.6 unit test job * switch documentation job to new PTI * import zuul job settings from project-config * Imported Translations from Zanata * Remove the export pre\_test\_hook from murano repo * Update py27-ocata to py27-queens * Move legacy-muranoclient-dsvm-functional-mysql-backend job to muranoclient * Update reno for stable/rocky 1.1.1 ----- * Switch to stestr * Add release note link in README 1.1.0 ----- * Remove PyPI downloads * fix tox python3 overrides * Trivial: update url to new url * Ignore default values for deprecated security parameters * Trivial: Update pypi url to new url * Follow the new PTI for document build * Fix incompatible requirement * Updated from global requirements * add lower-constraints job * Updated from global requirements * Updated from global requirements * Imported Translations from Zanata * Add os-testr to test-requirements.txt * Updated from global requirements * Imported Translations from Zanata * Updated from global requirements * Update reno for stable/queens 1.0.1 ----- * Updated from global requirements * Updated from global requirements 1.0.0 ----- * Updated from global requirements * Avoid tox\_install.sh for constraints support * Remove setting of version/release from releasenotes * Updated from global requirements * Updated from global requirements * Imported Translations from Zanata * Use generic user for both zuul v2 and v3 * update env template output * Updated from global requirements * Fix to use "." to source script files * Updated from global requirements * Update links in README * Add package download to openstack CLI * Add package update to openstack CLI * Add package show to openstack CLI * Updated from global requirements * Updated from global requirements * Add bundle import to openstack CLI * Skip two test due to apps.openstack.org is retired * Updated from global requirements * Update the documentation link for doc migration * Update reno for stable/pike * Updated from global requirements 0.14.0 ------ * Updated from global requirements * Update the documentation link for doc migration * Updated from global requirements * Updated from global requirements * Using the latest openstackdocstheme settings * Remove long-ago deprecated show\_categories * Updated from global requirements * import content from cli-reference in openstack-manuals * move existing content into the new standard structure * Turn on warning-is-error in sphinx build * switch to openstackdocstheme * Updated from global requirements 0.13.0 ------ * Updated from global requirements * Updated from global requirements * Updated from global requirements * Modify Default Domain * Updated from global requirements * Updated from global requirements * Add package import to openstack CLI * delete bash\_completion in subcommand * Updated from global requirements * Optimize the link address * Correct the unit test name * Updated from global requirements * Replace six.iteritems() with .items() * Updated from global requirements * Imported Translations from Zanata * Replaces uuid.uuid4 with uuidutils.generate\_uuid() * Remove log translations * Make method import\_versioned\_module work * Updated from global requirements * Update test requirement * Updated from global requirements * Updated from global requirements * Remove support for py34 * Allows fetching of deployments from all environments * Update reno for stable/ocata 0.12.0 ------ * Exclude build dir for flake8 test * Add debug to tox enviroment * Remove white space between print () * Fixes filtering applications by name with glare * Remove the data assert to pass the gate * Add package delete to openstack CLI * Using sys.exit(main()) instead of main() * Return error code when a error occurred during package-import * Fix Murano client to use V3 and MultiDomain Authentication * Use assertGreater() and assertLess() * Support i18n for LOG.warning * Update author in setup.cfg * Updated from global requirements * Show team and repo badges on README * Support for environment list filtering by project id * Delete python bytecode file * Fix typos in cover.sh * Fix removes date\_time items from dictionaries * Fix removes date\_time items from dictionaries * Updated from global requirements * move old apiclient code out of openstack/common * Updated from global requirements * Add validation to package import * Updated from global requirements * Updated from global requirements * Add plug-in summary for osc doc * Updated from global requirements * Add functional test for environment-model-show * Fix osc plugin gives the error when using keystone v3 * Add docstrings for environment-model-edit and environment-model-show commands * Fix OpenStack Licensing * OSC plugin should be using region/interface * Enable release notes translation * Add support for environment edit API * Fixing environment creation from template * Unskip the test due to bugfix * Skip some tests when using glare in a right way * Mark .testr.conf as non-executable * Make some OSC tests more clean * Updated from global requirements * TrivialFix: Fix typo in the bash shell file * Add package list to openstack CLI * Updated from global requirements * Updated from global requirements * TrivialFix: Using assertTrue() instead of assertEqual(True) * Remove unnecessary setUp * Updated from global requirements * Make OSC plugin be able to use glare backend * Updated from global requirements * Make --limit argument in package-list command robust * Add opportunity to import package from directory * Add Glare to python-muranloclient tests * Fix 'owned' flag when Glare is used * Populate tenant information in client * Cleanup, clarify newton release-notes * Update reno for stable/newton * Deprecate usage of 'glance' for --murano-pcakages-service 0.11.0 ------ * Sync tools/tox\_install.sh * Increase yaql quotas * [docs] Update Readme with correct Doc URL * Replace functions 'Dict.get' and 'del' with 'Dict.pop' * Updated from global requirements * Updated from global requirements * Decode PrettyTable output before printing for Python3 * Use upper constraints for all jobs in tox.ini * Updated from global requirements * Updated from global requirements * Add script for unit test coverage job * Trivial: clean up oslo-incubator related stuff * Add package create to openstack CLI 0.10.0 ------ * Correct default class version for static action call * Support for Schemas API was added * Support for multi-class yamls in client * Add Python 3.5 classifier and venv * Updated from global requirements * Change image for package in the code taken from Glance * Remove discover from test-requirements * Make environment-action-call command accept JSON arguments * Updated from global requirements * Add support for static actions API * Updated from global requirements * Add deployment list to openstack CLI * Add environment apps edit to openstack CLI * Add py27-mitaka tox target * Updated from global requirements * Use upper-constraints in tox test environments * Add \_\_ne\_\_ built-in function * Updated from global requirements * Use osc\_lib instead of cliff * Add 'description\_text' filed to test\_table\_struct\_env\_show * Use osc-lib instead of openstackclient 0.9.0 ----- * Updated from global requirements * Use DummyYaqlYamlLoader to load classes during upload * Use yaml.SafeLoader instead of yaml.Loader * Replace print statment with print function * Updated from global requirements * Fix python 2,3 compatibility issue with six * Fix client could not init glance client * Import package and dependencies in correct order * Fix typo in env-template-create-env arg description * Replace tempest\_lib with tempest.lib * Add environment deploy to openstack CLI * Support resources-dir for hot-packages * Reorder releasenotes and increase toctree depth * Add '--dep-exists-action' argument to murano CLI * Utilize enpoint\_type argument for client creation * Modified docstrings to comply with pep8 H405 style check * Store transitive inheritance information in glare * Updated from global requirements * Updated from global requirements * Remove unused httplib2 requirement * Updated from global requirements * Further refactor of Glare API urls * Improve muranoclient install scripts syntax * Updated from global requirements * Updated from global requirements * Refactor urls to Glare API * Fall back to glance API v1 * Correct variable name in code adopted from Glance * Updated from global requirements * Fix test case of test\_category\_create\_long\_name * Typo fix for python-muranoclient * GetPackageUI API is now called even if Glare is used * Updated from global requirements * Correct check for artifact package visibility * Move OpenStack client to test-requirements * Make use of version passed to glare artifacts\_client * Fix displaying packages in category-show subcommand * Improve dictionary representation of package from Glare * Set "glare" as valid choice for "--murano-packages-service" option * Refactor request methods in HTTP/SessionClient classes * Fix authentication in glare-api via keystone * Adds improvements for verification of values from created and listed package * Fix env-template-add-app command failed * Do not log contents of the download request * Updated from global requirements * Add test to check error message for bundle-import with invalid file format * Add test to check delete environment by environment ID * Add test to check error message for bundle-import with non-existing name * Add Category support for openstack CLI * Add environment create/delete to openstack CLI * Add ability to override MURANO\_URL for murano OSC plugin * Add Environment support for openstack CLI * Update reno for stable/mitaka * Updated from global requirements * Add reauth if token is expired and username/password are available * Update dummy application for testing after changes in murano * Distinguish between glance and glare endpoints 0.8.3 ----- * Remove unused pngmath Sphinx extension * Add test to check error message for category-create with long category name * Import bundle with a not regular-bundle json give no error * Add test to check error message for bundle-import with non-existed package * Add "Version" column in the package-list output * Add test to check error message for bundle-import without bundle name * Fix version in requests to GLARE * Updated from global requirements * Add test to check category-show error message * Add test to check category-delete error message 0.8.2 ----- * Remove unexpected kwargs when authenticating using token * Updated from global requirements * Updated from global requirements * Deletes redundant line in shell.py file * Make CLI outputs consistent with other OpenStack clients * Rename the package-import version parameter * Fix reading of app template file when adding to env template * Fix inconsistent argument name/placeholder in EnvTemplateManager.delete\_app * Add Status field\_labels for environment list * Fix checks for Project/Tenant command line arguments * Improving python3 compatibility * Improve is-public argument in env-template-create * environment-action-get-result now calls correct get\_result API * Adds region parameter to environment-create * Fixes incorrect endpoint\_override handling * Fixes TypeError in Client constructor * Updated from global requirements * Fix first argument of murano env-template-add-app usage * Add filter for do\_package\_list * Fix spellings for some words * Fix package delete on update when using Glare * Update translation setup * Updated from global requirements * CLI shell can now properly interact with Glance Artifacts * Fixed visibility parameter for create package glare call * Fix module's import order * Add is-public for environment-template list * Add environment template clone * Initial commit for openstack-client support in python-muranoclient * Add the cover case for 'body' kwarg in http json\_request * Updated from global requirements * Add python 3 support * Updated from global requirements 0.8.1 ----- * Updated from global requirements * Delete unuse variable in shell.py * Repair package-update command * Remove unnecessary statements for default argument * Updated from global requirements * Updated from global requirements * use keystoneclient exceptions instead of oslo-incubator code * Add machinery for translations via babel * Remove argparse dependency from requirements.txt file * Put py34 first in the envlist order of tox * Change LOG.warn to LOG.warning * Updated from global requirements * Delete the special character in README.rst * Remove arguments "{}" and "[]" in function definitions * Add Type field\_labels for package list * Make package-update help more clear * Let the SessionClient raw\_request only return response * Delete the extra space in delete package print * Updated from global requirements * Use oslo\_i18n instend of gettextutils * Updated from global requirements * Pass environment variables of proxy to tox * Change logging level in SessionClient * Fix Resource.\_\_eq\_\_ mismatch semantics of object equal * Update setup.cfg entries * Updated from global requirements 0.8.0 ----- * Remove py26 support * Add limit param for package-list * Catch correct exceptions in shell * Add missing space to error message * Fixed an incorrect call to the artifacts client * Fixed a download method wrapper in glare adaptor * 'to\_dict' method added to PackageWrapper class * Glare client now properly filters by class name * Updated from global requirements * Add reno for RElease NOtes * Replacing application\_catalog with application-catalog * New operation env-template-create-env * Show detailed info about app * Add python 3 support * Updated from global requirements * Rename folder with CLI tests * Add keystone v3 support to client * Add CLI tests for environment redeploy * Add unit-tests for categories * Import images publicly when package is public * Add test for environment deployment using CLI * Updated from global requirements * Use standard formatters for environment-create-session output * Small refactoring of CLI tests for packages * Updated from global requirements * Improve README contents * Updated from global requirements * Add more tests for categories, env-templates and packages * Updated from global requirements * Add Active field\_labels for package list * Updated from global requirements * Use non-absolute paths, to allow urljoin join them correctly * Hide token id in logs * Fix incorrect output after error in package-save * Add missing parameter 'acquired\_by' to environment model in tests * Updated from global requirements * Fix archive name generation in package\_create command 0.7.1 ----- * Fix the common/utils typo * Fixed YAQL tag leakage to YAML loader * Check package-download for stdout redirection * Update the python-muranoclient docs * Update MURANO\_REPO\_URL * Output detail informations about murano --debug command * Updated from global requirements * Update path to subunit2html in post\_test\_hook and fix env-show test * Updated from global requirements * Enable F812 and H904 style check * Fix app-show command * Fix test for environment table structure * Delete the unused LOG code * Return detailed info when get environment by name 0.7.0 ----- * Added the support of Glance Artifact Repository * Copy the code of Glance V3 (artifacts) client * Fixed issue with cacert parameter * Strip json and html from error messages * Update the git ingore * Fix the reversed incoming parameters of assertEqual * Add olso.log into muranoclient's requirements.txt * Updated from global requirements * Standardise help parameter of CLI commands * Fix some spelling mistakes of setup files 0.6.3 ----- * Support for YAQL 1.0.0 * Updated from global requirements * Generate ids in environment-apps-edit command * Allow editing packages with package-update command * Add parameter support to pagination-list * Add CLI command package-save * Add check for dashes in template name * Add environment-apps-edit command * Add environment-session-create command * Add action controlling commands * Add params to environment\_show command * Updated from global requirements * Add environment-deploy command * Updated package-import help description * Add sanity tests for testing actions with environment CLI command * Add ability to filter from categories with unicode * Allow joining existing networks when creating environment * Switch to oslo\_log in murano client * Add basic tests for Murano CLI client. Add CleanUp after tests * Add bundle-save CLI command * Place generated hot package to work directory * Updated from global requirements * Add description to CLI command category-show * Remove unused module cliutils and timeutils 0.6.2 ----- * Rename all visible occurrences of 'services' name * Add docstring to service-show command * Fix the unit tests with wrong using of mock * Add passenv parameter for tox G.variables passthrough * Allows congress to fetch environments from all tenants * Remove all vim modelines * Edits the help-text for python-muranoclient * Add abandon parameter to delete method * Add --version parameter for murano python client * Update requirements.txt * Adding config-like auth configuration for functional tests 0.6.0 ----- * Point default MURANO\_REPO\_URL to http://storage.apps.openstack.org * Support multiple filenames for package commands * Remove hash check during image upload * Make post\_test\_hook.sh executable * Add post\_test\_hook for functional tests * First pass at tempest\_lib based functional testing * Add OS\_TEST\_PATH to testr * Move unit tests into unit test directory * Drop use of 'oslo' namespace package * Only delete 'owned' packages for --exists-action update * Updated YAQL requirement to >= 0.2.6 * Update Readme * Limit parameter is no longer ignored for packages * Update .gitreview file to reflect repo rename * Better error logging for package/bundle import * Update package composing command * Tests for repository machinery * Bash completion script for murano * Add bash-completion subparser to shell client * Update from global requirements 0.5.6 ----- * Use HTTPClient in package\_create manager method * Removed default Content-Type HTTP request header * Improve image and bundle handling * Support local bundles * Fix service-show command * Fix category list CLI command * Enable Bundle.packages() to yield Package objects * Environment Template CLI * Handle duplicate packages during uploading via CLI * Add support for required images file * Client support for Require section in manifest * Allow importing bundles of packages * Allow importing packages name * Allow importing packages by url * Adds client support for action results * Use name or ID of environment for murano CLI commands * Enable category management support * Fixed pagination parameter to conform to API specs * Reverted "Add keystone v3 support to client" * Fixes pagination in "packages-list" command * Add keystone v3 support to client * Add 'verify' option to package.create * Do not parse empty body * Remove obsolete checks from tox.ini * Use pretty-tox for better test output * Update from global requirements * Use modules from oslo istead of openstack-common * Add opportunity to create public packages via shell * Revert "Add keystone v3 support to client" * Add keystone v3 support to client * Catch ConnectionError exception from requests * Updated from global requirements 0.5.5 ----- * Log response when using requests library * Provide method for calling action in an environment * Bump hacking to 0.9.x series 0.5.4 ----- * Fix some calls after switch to 'requests' 0.5.3 ----- * Convert muranoclient to 'requests' * Fix exception names and CLI args * Adds toggle public functionality * Add LICENSE to muranoclient * Update package-import command * Add package-create command * Updated from global requirements * Allow deletion of multiple environments * Infrastructure update * Add ability to retrieve supplier logo for package * Correct muranoclient service\_get function * Enable to set updating type for "Update package" * Extend package-show output with detailed information 0.5.2 ----- * Improve logging in CLI client * Extend CLI functionality * Fix issue with python 3.3 gate job * Paginate packages list using 'next\_marker' attribute * Run hacking in a right way * fixed several pep8 issues * Fixed Keystone endpoint query * Enable unpickling base.Resource objects 0.5.1 ----- * for pkg list, make include\_disabled default False * Return package object in packages.create method 0.5.0 ----- * Remove version from setup.cfg * Added HTTP proxy support * Fix the issue with stats API call * Update requirements.txt due to changes in global-requirements.txt * InstanceStatistics updated for corresponding API change * Parse YAML in muranoclient with the loader of the given class * Refactor packages filter and list methods * Interface to get instance statistics added * Support package update, toggle\_active and filter methods * Initial support of working with package definitions * Updated openstack-common * Calls to the v1 API are prefixed with the appropriate version prefix * Add Stats List method * Fixed issue with requirements * Update README with actual info * Update requirements to stable/havana * Update version in setup.cfg * Support passing networking info during environment create, not update * Support getting and updating network\_info of environment * Cherry-picked changes from release-0.3 * Support building wheels (PEP-427) * Update requirements to match havana's ones * Resolve issues with package setup.py * Cherry-pick following change-ids from release-0.2 * Fix error from previous commit * Fix service creation failure * Fix http.py to initial * Resolved bug 746 * Add SSL support to muranoclient * Modified client to support lastStatus fetching * Add CentOS setup shell script * setup.sh add * list deployments in client now properly returns a list, not dictionary * Fixed a couple of bugs in path resolution routine * Changes in client to work with deployment logs fetching * Added support for new API v0.2 * fixed python setup workflow * Added tests for new services * Another changes within Service::Delete * Correct Service::Delete params * Murano Python Client support for IIS/ASP.NET web farms * Fix API according to New Sessions Handling design * python-muranoclient to support ASP.NET apps deployment API * Added .gitreview file * glazierclient -> muranoclient in tests * KEERO-320 - Fix all occurrences of old names (keero, glazier) in python client * Removed all projects except Glazier Api Client * Removed all projects except Glazier Api Client * Small fixes for unit tests * Fixed issue with length of new name * Fixed small issue * Fixed issue with names * Fixed issue with names * Fixed issue with names * Fixed issue with names * Fixed issue with names * Fixed issue with names * Fixed issue with names * Fixed small issue with new names * Fixed issue with horizon component installation * Fixed issue with horizon component installation * Fixed issue with horizon component installation * Finished converting API Specification * Fixed small issue with setup.py * Fixed issue with renaming of the tabula component * Fixed issue with renaming of the tabula component * Added part of API Specification * Tabula renamed to dashboard * Tabula renamed to dashboard * Finished documentation for API * Finished documentation for API * Renamed Portas to API * Renamed Portas to API * Added tox for webUI tests * Added license to documentation of Portas Client * Small fix * Fixed api interface names to environments * Fixed api interface names to environments * Renamed and licensed python-glazierclient * Fixed api interface names to environments * Fixed api interface names to environments * Renamed documentation project * Fixed licenses for tabula and tests. Fixed name of tabula project * Fixed licenses for tabula and tests. Fixed name of tabula project * Fixed a few small issues * Pass all RabbitMQ settings from conductor to Agent * Initialization of tox for conductor and portas. Add new webUI automated tests * Licenses added * A lot of changes were made * Issue with figures * Main Documentation Project * Fixed ignore file for python-portasclient * Documentation for UI * Documentation for Python PortasClient * Forgot man pages * Documentation for Portas Project * Send token when deleting environment * Fixed https://mirantis.jira.com/browse/KEERO-227 * Fixed issue with sessions * Fixed issue with sessions * Experiments * Experiments * Experiments * Experiments * Experiments * Add logging to WebUI * Add initial files for unit tests * Fixed issues with sessions Added logging * Added unit tests for client. Coverage 66% * Rename RabbitMQ username field Removed use\_ssl option from settings * Fix running install\_venv.py * Updated python-portasclient * PEP8 compliance * Fixed unit tests * Added ability to add services to environment * bug fix * bug fix * Fixed index bug * Finalize UI * Finalize UI * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Fix PEP8 errors * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Fix issue with statuses * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Experiments * Fixed issue with sessions * Naming conventions: use name instead of id for instance names * Heat auto-discovery, keero-linux-keys -> keero-keys * Experiments with UI * typo * Scoped tokens * Experiments with UI * Experiments with UI * Experiments with UI * Experiments with UI * Experiments with UI * Experiments with UI * Experiments with UI * Fix issue for result of deleted environments * Fix merge issue * Merged * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fix name of the variable * Fixed small issue * Send token when deleting environment * Removed unneeded binding * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Added tabs for services * forgotten file * Fixed issue with activeDirectory deletion * Add support for reports filtering * Added tabs for services * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Fixed small issue * Added dropdown list with list of services * Added initial version of tabs for services * Added initial version of tabs for services * Added initial version of tabs for services * Fixed issues with templates * Fixed issue with installation * Fixed issue with installation * PEP-8 * Fixed issue with incorrect import * logging and bug-fixes * fixed VM initialization script * Evironment/Units deletion, bug-fixes * Fixed UI issues * Remove service deletion button * Finished installable UI * Removed old code * Naming conventions changed * Updated OpenStack Common (Fixed issue with notifier package) * Use Heat REST API instead of command-line * Added support for setup.py Added localization and documentation skeletons PEP8 Fixes, optimized imports * #KEERO-222 Remove random part of unit name * Send Env to Conductor for deletion * Hot fix for WebUI tests * Fixed WebUI tests. Added new tests * #KEERO-220 Send X-Auth-Token to Conductor * Added initial unit tests for RestAPI service * Fixed all pep8 * Fixed automated tests for web UI * Fixed automated tests for WebUI. Added class Page with simple objects * Code to start\stop keero components using devstack functions * Fixed: changed the run mode for install venv script * Added deployment script for automated tests * All Cloudbase-Init plugins disabled except UserDataPlugin * Userdata script updated to support computer renaming functionality * Entry point script renamed 2d27f4f5054f34982ed67da2bf4b35c8ac1558d3 * Issues #195 and #199 * README and guide for conductor * Fix and unit test for issue: https://mirantis.jira.com/browse/KEERO-219 * Added unit tests for REST API client. Fixed pep8 * New devstack scripts added * Old devstack scripts removed * Write-Host replaced by Write-Log * Fixed typo * Sync * Sync * Sync * Cloned horizon and added our dashboard * Removed obsolete code Removed projects: [windc, windcclient] * Added tests for REST API. Fixed issues with Web UI * Added tests for REST API. Fixed issues with Web UI * Merged iteration3 branch to master. Fixed pep8 * Fixed small issues with UI * Added lst fixes for demo * Fix another issue with environments list * Fix another issue with services * Fix issue with getting list of environments * Added progress bars for services. Fixed few issues for demo * Fix issue with ack on results * Add part of service id to unit name * Add ability to get status for Environments and Sessions * Added password-secure checks for UI, fix usability issues for demo * ExecutionPlanGenerator DSL processor * Updated workflow elements to reflect new name changes and to fix typos * ExecutionPlanGenerator DSL processor * Updated workflow elements to reflect new name changes and to fix typos * Another Issue with sessions * Added progress bar to Web UI. Fixed pep8 errors * Change behaviour of viewing services * Issue with session * No ack is needed when auto\_ack set to True * Resolved issue with reports from orchestration engine * FIx issue with fields created & updated * Fixed issue with logging * Added deploy button for data centers in UI. Fixed templates for services * Queues should be durable * Add debug logging for controllers * Sync before tag * Fixed issue with empty services list * Added new API for Web UI * typos * Fixed length of names * Fixed instance namings * Added WebServer and AD * Workflows, ExecutionPlanGenerator, Reporting, UserData, conductor improvements * Removed obsolete file * Bug with Session * Added Session operations * Added Environments CRUD operations * Issue with deleting Environment * Removed obsolete files * Added initial version for python-portasclient * Issue with port for RabbitMQ * Function updated to return IPv4 addresses only * Typo * Explicit import of module DnsServer added * Function to install IIS added * Code to return DNS listening IPs from DC added * WebServer API Added WebServer API Small refactoring * Fix issues with queues * Added units name generation * Write results from orchestration engine * Active Directory API * Remove obsolete service table * Enable session deployment * Reports from orchestration engine Added ability to store and expose reports from orchestration engine * Cleaned up API * Added support for reading messages from RabbitMQ asynchronously * Typo * userdata.py fixed * Finished Task KEERO-111. Added base UI tests * Hot fix: Fixed pep8 for Dashboard * Finished Task: KEERO-117. Added new UI wizard for Create Services Action * Userdata plugin with minimal MIME support added * User data sample file added * Extra functions moved to NotCoreFunctions.ps1 file in order to remove them in the future * Functions to work with Base64 strings added * Functions to work with Zip files added * Modified files from cloudbase-init added * Fixed pep8. Fixed deployment script * Added support for session checking * Most part of Session API * Removed obsolete code * Added new Session model and migration Fixed issues with previous models * Initial conductor implementation * Added deployment script and automated tests * Small PEP8 fixes * Fixed small issues with parameters. It is required fix * Added remove method for environments Also slightly updated routes table * Finished environments api * Only environments from same tenant as users should be shown * Remove unnecessary blocks of code * When new DC is adding tenant\_id is added as param * Fix issues with context * Moved utils.py from WindDC * Small changes to .gitignore Removed global .gitignore Added .gitignore to WindowsAgent project * Update added files * Added support for keystone-auth * Updated initial version of portas-api * Initial version of portas-api * Simple function to update agent config added * Simple function for working with templates added * Function to retrieve meta data opject from config drive added * localrc updated * Files to automate devstack installation added * Fixed small issues with WebUI * asd * test.commit * test.commit * Log functions updated * Stop-Execution modified * Removed obsole line * Removed obsolete file Added .gitignore file * Added reference JSON for Active Directory * Fixed urls for dashboard * Fixed Web UI for demo * Files removed * Windows PowerShell module added * Unattended installation files added * Execution plan files added * windc iteration2 * Added WebUI for correct configuration of new service AD * Resolved issue with datacenter id * Resolved issue with datacenter id * Fixed many small issues * Fixed typo * Fixed KEERO-89 * Fixed issue with data centers * Added services functions to client. Need to be tested * [KEERO-83] Windows Agent: Ability to reboot machine after execution plan is executed * [KEERO-83] Windows Agent: Typo fixes + sample values in config * [KEERO-83] Windows Agent initial implementation * Added operations for chef. They might be remove if we decide to not use chef * Fixed small issues * Fixed KEERO-85 * Fixed issue with virtual environment SQLAlchemy library * Added library libsqlite3-dev to virtual environment for windc client * Added new functional to dashboard, fixed small issues * Added windc API client, sync repo with dev box * Added new files * Updated design. Removed extra code * 1. Added support of CloudFormation templates. Made a simple interface to build template. Stan can work here to redesign template.py 2. Added calls of drivers. Now heat is called from cmd instead of client. Should be rewritten. 3. ActiveDirectory makes a static template. Need to rewrite this with working with actual parameters * Added additional fields for Domain Controller * Added simple form for configuration Domen Controllers and IIS Servers * Fixed small problems with links and titles on pages * Fixed small problems with links and titles on pages * Added initial project for horizon dashboard * 1. Added builders support. Each builder is a class dynamically loaded from ./windc/core/builders folder. The class name should be the same as module file name. 2. Updated core/api.py to support datacenter and service creation with extra parameters which are not defined by model explicitly. 3. Added event based approach for the windows environment change. Now when user submits a request to API the core updates database and initiates a new event which defined scope (datacenter, service, VM) and action (add, modify, delete). This event and data will be iterated over all registered builders. Each builder can use this event and data to plan some modification * 1. Fixed issue with main file start ./bin/windc-api 2. Added router to Route /datacenters/ and /services/ URLs 3. Added stubs for windc/core/api. 4. Fixed start-up process for service ------------------------------------------------- Now it is working service which will reply for curl http://localhost:8181/tenant\_id/datacenters/ curl http://localhost:8181/tenant\_id/datacenters/dc\_id/services curl http://localhost:8181/tenant\_id/datacenters/dc\_id/services/service\_id * Initial version of the Windows DataCenter project. It is openstak-skeleton based * Unattended files added * Initial empty repository ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/HACKING.rst0000664000175000017500000000017000000000000017357 0ustar00zuulzuul00000000000000Style Commandments ================== Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/LICENSE0000664000175000017500000002363700000000000016603 0ustar00zuulzuul00000000000000 Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.6220894 python-muranoclient-2.8.0/PKG-INFO0000664000175000017500000000750000000000000016662 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: python-muranoclient Version: 2.8.0 Summary: python-muranoclient Home-page: https://docs.openstack.org/python-muranoclient/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: Apache License, Version 2.0 Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-muranoclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on Murano ====== .. image:: https://img.shields.io/pypi/v/python-muranoclient.svg :target: https://pypi.org/project/python-muranoclient/ :alt: Latest Version Murano Project introduces an application catalog, which allows application developers and cloud administrators to publish various cloud-ready applications in a browsable categorised catalog, which may be used by the cloud users (including the inexperienced ones) to pick-up the needed applications and services and composes the reliable environments out of them in a "push-the-button" manner. * `PyPi`_ - package installation * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.org/project/python-muranoclient .. _Launchpad project: https://launchpad.net/python-muranoclient .. _Blueprints: https://blueprints.launchpad.net/python-muranoclient .. _Bugs: https://bugs.launchpad.net/python-muranoclient .. _Source: https://opendev.org/openstack/python-muranoclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/murano-specs/ .. _Release Notes: https://docs.openstack.org/releasenotes/python-muranoclient Python Muranoclient ------------------- python-muranoclient is a client library for Murano built on the Murano API. It provides a Python API (the ``muranoclient`` module) and a command-line tool (``murano``). Project Resources ----------------- Project status, bugs, and blueprints are tracked on Launchpad: * Client bug tracker * https://launchpad.net/python-muranoclient * Murano bug tracker * https://launchpad.net/murano Developer documentation can be found here: https://docs.openstack.org/murano/latest/ Additional resources are linked from the project wiki page: https://wiki.openstack.org/wiki/Murano License ------- Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 Platform: UNKNOWN Classifier: Development Status :: 4 - Beta Classifier: Environment :: Console Classifier: Environment :: OpenStack Classifier: Intended Audience :: Developers Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.6 Classifier: Programming Language :: Python :: 3.7 Classifier: Programming Language :: Python :: 3.8 Requires-Python: >=3.6 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/README.rst0000664000175000017500000000433200000000000017254 0ustar00zuulzuul00000000000000======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-muranoclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html .. Change things from this point on Murano ====== .. image:: https://img.shields.io/pypi/v/python-muranoclient.svg :target: https://pypi.org/project/python-muranoclient/ :alt: Latest Version Murano Project introduces an application catalog, which allows application developers and cloud administrators to publish various cloud-ready applications in a browsable categorised catalog, which may be used by the cloud users (including the inexperienced ones) to pick-up the needed applications and services and composes the reliable environments out of them in a "push-the-button" manner. * `PyPi`_ - package installation * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.org/project/python-muranoclient .. _Launchpad project: https://launchpad.net/python-muranoclient .. _Blueprints: https://blueprints.launchpad.net/python-muranoclient .. _Bugs: https://bugs.launchpad.net/python-muranoclient .. _Source: https://opendev.org/openstack/python-muranoclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/murano-specs/ .. _Release Notes: https://docs.openstack.org/releasenotes/python-muranoclient Python Muranoclient ------------------- python-muranoclient is a client library for Murano built on the Murano API. It provides a Python API (the ``muranoclient`` module) and a command-line tool (``murano``). Project Resources ----------------- Project status, bugs, and blueprints are tracked on Launchpad: * Client bug tracker * https://launchpad.net/python-muranoclient * Murano bug tracker * https://launchpad.net/murano Developer documentation can be found here: https://docs.openstack.org/murano/latest/ Additional resources are linked from the project wiki page: https://wiki.openstack.org/wiki/Murano License ------- Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1709301724.586079 python-muranoclient-2.8.0/doc/0000775000175000017500000000000000000000000016330 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/doc/requirements.txt0000664000175000017500000000045600000000000021621 0ustar00zuulzuul00000000000000# The order of packages is significant, because pip processes them in the order # of appearance. Changing the order has an impact on the overall integration # process, which may cause wedges in the gate later. sphinx>=2.0.0,!=2.1.0 # BSD openstackdocstheme>=2.2.1 # Apache-2.0 reno>=3.1.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1709301724.586079 python-muranoclient-2.8.0/doc/source/0000775000175000017500000000000000000000000017630 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1709301724.586079 python-muranoclient-2.8.0/doc/source/cli/0000775000175000017500000000000000000000000020377 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/doc/source/cli/index.rst0000664000175000017500000000263600000000000022247 0ustar00zuulzuul00000000000000================= Murano API Client ================= In order to use the python api directly, you must first obtain an auth token and identify which endpoint you wish to speak to. Once you have done so, you can use the API like so:: >>> from muranoclient import Client >>> murano = Client('1', endpoint=MURANO_URL, token=OS_AUTH_TOKEN) ... Command-line Tool ================= In order to use the CLI, you must provide your OpenStack username, password, tenant, and auth endpoint. Use the corresponding configuration options (:option:``--os-username``, :option:``--os-password``, :option:``--os-tenant-id``, and :option:``--os-auth-url``) or set them in environment variables:: export OS_USERNAME=user export OS_PASSWORD=pass export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b export OS_AUTH_URL=http://auth.example.com:5000/v2.0 The command line tool will attempt to reauthenticate using your provided credentials for every request. You can override this behavior by manually supplying an auth token using :option:``--os-image-url`` and :option:``--os-auth-token``. You can alternatively set these environment variables:: export MURANO_URL=http://murano.example.org:8082/ export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 Once you've configured your authentication parameters, you can run :command:`murano help` to see a complete listing of available commands. .. toctree:: murano ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/doc/source/cli/murano.rst0000664000175000017500000006745500000000000022453 0ustar00zuulzuul00000000000000======================================================== Application Catalog service (murano) command-line client ======================================================== The murano client is the command-line interface (CLI) for the Application Catalog service (murano) API and its extensions. This chapter documents :command:`murano` version ``0.13.0``. For help on a specific :command:`murano` command, enter: .. code-block:: console $ murano help COMMAND .. _murano_command_usage: murano usage ~~~~~~~~~~~~ .. code-block:: console usage: murano [--version] [-d] [-v] [--cert-file OS_CERT] [--key-file OS_KEY] [--ca-file OS_CACERT] [--api-timeout API_TIMEOUT] [--os-tenant-id OS_TENANT_ID] [--os-tenant-name OS_TENANT_NAME] [--os-region-name OS_REGION_NAME] [--os-auth-token OS_AUTH_TOKEN] [--os-no-client-auth] [--murano-url MURANO_URL] [--glance-url GLANCE_URL] [--glare-url GLARE_URL] [--murano-api-version MURANO_API_VERSION] [--os-service-type OS_SERVICE_TYPE] [--os-endpoint-type OS_ENDPOINT_TYPE] [--include-password] [--murano-repo-url MURANO_REPO_URL] [--murano-packages-service {murano,glance,glare}] [--insecure] [--os-cacert ] [--os-cert ] [--os-key ] [--timeout ] [--os-auth-url OS_AUTH_URL] [--os-domain-id OS_DOMAIN_ID] [--os-domain-name OS_DOMAIN_NAME] [--os-project-id OS_PROJECT_ID] [--os-project-name OS_PROJECT_NAME] [--os-project-domain-id OS_PROJECT_DOMAIN_ID] [--os-project-domain-name OS_PROJECT_DOMAIN_NAME] [--os-trust-id OS_TRUST_ID] [--os-user-id OS_USER_ID] [--os-username OS_USERNAME] [--os-user-domain-id OS_USER_DOMAIN_ID] [--os-user-domain-name OS_USER_DOMAIN_NAME] [--os-password OS_PASSWORD] ... **Subcommands:** ``app-show`` List applications, added to specified environment. ``bundle-import`` Import a bundle. ``bundle-save`` Save a bundle. ``category-create`` Create a category. ``category-delete`` Delete a category. ``category-list`` List all available categories. ``category-show`` Display category details. ``class-schema`` Display class schema ``deployment-list`` List deployments for an environment or multiple environments. ``env-template-add-app`` Add application to the environment template. ``env-template-clone`` Create a new template, cloned from template. ``env-template-create`` Create an environment template. ``env-template-create-env`` Create a new environment from template. ``env-template-del-app`` Delete application from the environment template. ``env-template-delete`` Delete an environment template. ``env-template-list`` List the environments templates. ``env-template-show`` Display environment template details. ``env-template-update`` Update an environment template. ``environment-action-call`` Call action \`ACTION\` in environment \`ID\`. ``environment-action-get-result`` Get result of \`TASK\` in environment \`ID\`. ``environment-apps-edit`` Edit environment's object model. ``environment-create`` Create an environment. ``environment-delete`` Delete an environment. ``environment-deploy`` Start deployment of a murano environment session. ``environment-list`` List the environments. ``environment-model-edit`` Edit an environment's object model. ``environment-model-show`` Display an environment's object model. ``environment-rename`` Rename an environment. ``environment-session-create`` Creates a new configuration session for environment ID. ``environment-show`` Display environment details. ``package-create`` Create an application package. ``package-delete`` Delete a package. ``package-download`` Download a package to a filename or stdout. ``package-import`` Import a package. ``package-list`` List available packages. ``package-save`` Save a package. ``package-show`` Display details for a package. ``package-update`` Update an existing package. ``static-action-call`` Call static method \`METHOD\` of the class \`CLASS\` with \`ARGUMENTS\`. ``bash-completion`` Prints all of the commands and options to stdout. ``help`` Display help about this program or one of its subcommands. .. _murano_command_options: murano optional arguments ~~~~~~~~~~~~~~~~~~~~~~~~~ ``--version`` Show program's version number and exit. ``-d, --debug`` Defaults to ``env[MURANOCLIENT_DEBUG]``. ``-v, --verbose`` Print more verbose output. ``--cert-file OS_CERT`` **DEPRECATED!** Use --os-cert. ``--key-file OS_KEY`` **DEPRECATED!** Use --os-key. ``--ca-file OS_CACERT`` **DEPRECATED!** Use --os-cacert. ``--api-timeout API_TIMEOUT`` Number of seconds to wait for an API response, defaults to system socket timeout. ``--os-tenant-id OS_TENANT_ID`` Defaults to ``env[OS_TENANT_ID]``. ``--os-tenant-name OS_TENANT_NAME`` Defaults to ``env[OS_TENANT_NAME]``. ``--os-region-name OS_REGION_NAME`` Defaults to ``env[OS_REGION_NAME]``. ``--os-auth-token OS_AUTH_TOKEN`` Defaults to ``env[OS_AUTH_TOKEN]``. ``--os-no-client-auth`` Do not contact keystone for a token. Defaults to ``env[OS_NO_CLIENT_AUTH]``. ``--murano-url MURANO_URL`` Defaults to ``env[MURANO_URL]``. ``--glance-url GLANCE_URL`` Defaults to ``env[GLANCE_URL]``. ``--glare-url GLARE_URL`` Defaults to ``env[GLARE_URL]``. ``--murano-api-version MURANO_API_VERSION`` Defaults to ``env[MURANO_API_VERSION]`` or 1. ``--os-service-type OS_SERVICE_TYPE`` Defaults to ``env[OS_SERVICE_TYPE]``. ``--os-endpoint-type OS_ENDPOINT_TYPE`` Defaults to ``env[OS_ENDPOINT_TYPE]``. ``--include-password`` Send os-username and os-password to murano. ``--murano-repo-url MURANO_REPO_URL`` Defaults to ``env[MURANO_REPO_URL]`` or http://apps.openstack.org/api/v1/murano_repo/liberty/ ``--murano-packages-service {murano,glance,glare}`` Specifies if murano-api ("murano") or Glance Artifact Repository ("glare") should be used to store murano packages. Defaults to ``env[MURANO_PACKAGES_SERVICE]`` or to "murano" ``--insecure`` Explicitly allow client to perform "insecure" TLS (https) requests. The server's certificate will not be verified against any certificate authorities. This option should be used with caution. ``--os-cacert `` Specify a CA bundle file to use in verifying a TLS (https) server certificate. Defaults to ``env[OS_CACERT]``. ``--os-cert `` Defaults to ``env[OS_CERT]``. ``--os-key `` Defaults to ``env[OS_KEY]``. ``--timeout `` Set request timeout (in seconds). ``--os-auth-url OS_AUTH_URL`` Authentication URL ``--os-domain-id OS_DOMAIN_ID`` Domain ID to scope to ``--os-domain-name OS_DOMAIN_NAME`` Domain name to scope to ``--os-project-id OS_PROJECT_ID`` Project ID to scope to ``--os-project-name OS_PROJECT_NAME`` Project name to scope to ``--os-project-domain-id OS_PROJECT_DOMAIN_ID`` Domain ID containing project ``--os-project-domain-name OS_PROJECT_DOMAIN_NAME`` Domain name containing project ``--os-trust-id OS_TRUST_ID`` Trust ID ``--os-user-id OS_USER_ID`` User ID ``--os-username OS_USERNAME, --os-user-name OS_USERNAME, --os-user_name OS_USERNAME`` Username ``--os-user-domain-id OS_USER_DOMAIN_ID`` User's domain id ``--os-user-domain-name OS_USER_DOMAIN_NAME`` User's domain name ``--os-password OS_PASSWORD`` User's password .. _murano_app-show: murano app-show --------------- .. code-block:: console usage: murano app-show [-p ] List applications, added to specified environment. **Positional arguments:** ```` Environment ID to show applications from. **Optional arguments:** ``-p , --path `` Level of detalization to show. Leave empty to browse all applications in the environment. .. _murano_bundle-import: murano bundle-import -------------------- .. code-block:: console usage: murano bundle-import [--is-public] [--exists-action {a,s,u}] [ ...] Import a bundle. \`FILE\` can be either a path to a zip file, URL, or name from repo. If \`FILE\` is a local file, treat names of packages in a bundle as file names, relative to location of the bundle file. Requirements are first searched in the same directory. **Positional arguments:** ```` Bundle URL, bundle name, or path to the bundle file. **Optional arguments:** ``--is-public`` Make packages available to users from other tenants. ``--exists-action {a,s,u}`` Default action when a package already exists. .. _murano_bundle-save: murano bundle-save ------------------ .. code-block:: console usage: murano bundle-save [-p ] [--no-images] Save a bundle. This will download a bundle of packages with all dependencies to specified path. If path doesn't exist it will be created. **Positional arguments:** ```` Bundle URL, bundle name, or path to the bundle file. **Optional arguments:** ``-p , --path `` Path to the directory to store packages. If not set will use current directory. ``--no-images`` If set will skip images downloading. .. _murano_category-create: murano category-create ---------------------- .. code-block:: console usage: murano category-create Create a category. **Positional arguments:** ```` Category name. .. _murano_category-delete: murano category-delete ---------------------- .. code-block:: console usage: murano category-delete [ ...] Delete a category. **Positional arguments:** ```` ID of a category(ies) to delete. .. _murano_category-list: murano category-list -------------------- .. code-block:: console usage: murano category-list List all available categories. .. _murano_category-show: murano category-show -------------------- .. code-block:: console usage: murano category-show Display category details. **Positional arguments:** ```` ID of a category(s) to show. .. _murano_class-schema: murano class-schema ------------------- .. code-block:: console usage: murano class-schema [--package-name PACKAGE_NAME] [--class-version CLASS_VERSION] [ [ ...]] Display class schema **Positional arguments:** ```` Class FQN ```` Method name **Optional arguments:** ``--package-name PACKAGE_NAME`` FQN of the package where the class is located ``--class-version CLASS_VERSION`` Class version or version range (version spec) .. _murano_deployment-list: murano deployment-list ---------------------- .. code-block:: console usage: murano deployment-list [--all-environments] [] List deployments for an environment or multiple environments. **Positional arguments:** ```` Environment ID for which to list deployments. **Optional arguments:** ``--all-environments`` Lists all deployments for all environments in user's tenant. .. _murano_env-template-add-app: murano env-template-add-app --------------------------- .. code-block:: console usage: murano env-template-add-app Add application to the environment template. **Positional arguments:** ```` Environment template ID. ```` Path to the template. .. _murano_env-template-clone: murano env-template-clone ------------------------- .. code-block:: console usage: murano env-template-clone Create a new template, cloned from template. **Positional arguments:** ```` Environment template ID. ```` New environment template name. .. _murano_env-template-create: murano env-template-create -------------------------- .. code-block:: console usage: murano env-template-create [--is-public] Create an environment template. **Positional arguments:** ```` Environment template name. **Optional arguments:** ``--is-public`` Make the template available for users from other tenants. .. _murano_env-template-create-env: murano env-template-create-env ------------------------------ .. code-block:: console usage: murano env-template-create-env [--region ] Create a new environment from template. **Positional arguments:** ```` Environment template ID. ```` New environment name. **Optional arguments:** ``--region `` Name of the target OpenStack region. .. _murano_env-template-del-app: murano env-template-del-app --------------------------- .. code-block:: console usage: murano env-template-del-app Delete application from the environment template. **Positional arguments:** ```` Environment template ID. ```` Application ID. .. _murano_env-template-delete: murano env-template-delete -------------------------- .. code-block:: console usage: murano env-template-delete [ ...] Delete an environment template. **Positional arguments:** ```` ID of environment(s) template to delete. .. _murano_env-template-list: murano env-template-list ------------------------ .. code-block:: console usage: murano env-template-list List the environments templates. .. _murano_env-template-show: murano env-template-show ------------------------ .. code-block:: console usage: murano env-template-show Display environment template details. **Positional arguments:** ```` Environment template ID. .. _murano_env-template-update: murano env-template-update -------------------------- .. code-block:: console usage: murano env-template-update Update an environment template. **Positional arguments:** ```` Environment template ID. ```` Environment template name. .. _murano_environment-action-call: murano environment-action-call ------------------------------ .. code-block:: console usage: murano environment-action-call --action-id [--arguments [ [ ...]]] id Call action \`ACTION\` in environment \`ID\`. Returns id of an asynchronous task, that executes the action. Actions can only be called on a \`deployed\` environment. To view actions available in a given environment use \`environment-show\` command. **Positional arguments:** ``id`` ID of Environment to call action against. **Optional arguments:** ``--action-id `` ID of action to run. ``--arguments [ [ ...]]`` Action arguments. .. _murano_environment-action-get-result: murano environment-action-get-result ------------------------------------ .. code-block:: console usage: murano environment-action-get-result --task-id Get result of \`TASK\` in environment \`ID\`. **Positional arguments:** ```` ID of Environment where task is being executed. **Optional arguments:** ``--task-id `` ID of action to run. .. _murano_environment-apps-edit: murano environment-apps-edit ---------------------------- .. code-block:: console usage: murano environment-apps-edit --session-id [FILE] Edit environment's object model. \`FILE\` is path to a file, that contains jsonpatch, that describes changes to be made to environment's object-model. [ { "op": "add", "path": "/-", "value": { ... your-app object model here ... } }, { "op": "replace", "path": "/0/?/name", "value": "new_name" }, ] NOTE: Values '===id1===', '===id2===', etc. in the resulting object-model will be substituted with uuids. For more info on jsonpatch see RFC 6902 **Positional arguments:** ```` ID of Environment to edit. ``FILE`` File to read jsonpatch from (defaults to stdin). **Optional arguments:** ``--session-id `` Id of a config session. .. _murano_environment-create: murano environment-create ------------------------- .. code-block:: console usage: murano environment-create [--join-net-id ] [--join-subnet-id ] [--region ] Create an environment. **Positional arguments:** ```` Environment name. **Optional arguments:** ``--join-net-id `` Network id to join. ``--join-subnet-id `` Subnetwork id to join. ``--region `` Name of the target OpenStack region. .. _murano_environment-delete: murano environment-delete ------------------------- .. code-block:: console usage: murano environment-delete [--abandon] [ ...] Delete an environment. **Positional arguments:** ```` Id or name of environment(s) to delete. **Optional arguments:** ``--abandon`` If set will abandon environment without deleting any of its resources. .. _murano_environment-deploy: murano environment-deploy ------------------------- .. code-block:: console usage: murano environment-deploy --session-id Start deployment of a murano environment session. **Positional arguments:** ```` ID of Environment to deploy. **Optional arguments:** ``--session-id `` ID of configuration session to deploy. .. _murano_environment-list: murano environment-list ----------------------- .. code-block:: console usage: murano environment-list [--all-tenants] [--tenant ] List the environments. **Optional arguments:** ``--all-tenants`` Allows to list environments from all tenants (admin only). ``--tenant `` Allows to list environments for a given tenant (admin only). .. _murano_environment-model-edit: murano environment-model-edit ----------------------------- .. code-block:: console usage: murano environment-model-edit --session-id [] Edit an environment's object model. **Positional arguments:** ```` ID of Environment to edit. ```` File to read JSON-patch from (defaults to stdin). **Optional arguments:** ``--session-id `` Id of a config session. .. _murano_environment-model-show: murano environment-model-show ----------------------------- .. code-block:: console usage: murano environment-model-show [--path ] [--session-id ] Display an environment's object model. **Positional arguments:** ```` ID of Environment to show. **Optional arguments:** ``--path `` Path to Environment model section. Defaults to '/'. ``--session-id `` Id of a config session. .. _murano_environment-rename: murano environment-rename ------------------------- .. code-block:: console usage: murano environment-rename Rename an environment. **Positional arguments:** ```` Environment ID or name. ```` A name to which the environment will be renamed. .. _murano_environment-session-create: murano environment-session-create --------------------------------- .. code-block:: console usage: murano environment-session-create Creates a new configuration session for environment ID. **Positional arguments:** ```` ID of Environment to add session to. .. _murano_environment-show: murano environment-show ----------------------- .. code-block:: console usage: murano environment-show [--session-id ] [--only-apps] Display environment details. **Positional arguments:** ```` Environment ID or name. **Optional arguments:** ``--session-id `` Id of a config session. ``--only-apps`` Only print apps of the environment (useful for automation). .. _murano_package-create: murano package-create --------------------- .. code-block:: console usage: murano package-create [-t ] [-c ] [-r ] [-n ] [-f ] [-a ] [--tags [ [ ...]]] [-d ] [-o ] [-u ] [--type TYPE] [-l ] Create an application package. **Optional arguments:** ``-t , --template `` Path to the Heat template to import as an Application Definition. ``-c , --classes-dir `` Path to the directory containing application classes. ``-r , --resources-dir `` Path to the directory containing application resources. ``-n , --name `` Display name of the Application in Catalog. ``-f , --full-name `` Fully-qualified name of the Application in Catalog. ``-a , --author `` Name of the publisher. ``--tags [ [ ...]]`` A list of keywords connected to the application. ``-d , --description `` Detailed description for the Application in Catalog. ``-o , --output `` The name of the output file archive to save locally. ``-u , --ui `` Dynamic UI form definition. ``--type TYPE`` Package type. Possible values: Application or Library. ``-l , --logo `` Path to the package logo. .. _murano_package-delete: murano package-delete --------------------- .. code-block:: console usage: murano package-delete [ ...] Delete a package. **Positional arguments:** ```` Package ID to delete. .. _murano_package-download: murano package-download ----------------------- .. code-block:: console usage: murano package-download [file] Download a package to a filename or stdout. **Positional arguments:** ```` Package ID to download. ``file`` Filename to save package to. If it is not specified and there is no stdout redirection the package won't be saved. .. _murano_package-import: murano package-import --------------------- .. code-block:: console usage: murano package-import [-c [ [ ...]]] [--is-public] [--package-version PACKAGE_VERSION] [--exists-action {a,s,u}] [--dep-exists-action {a,s,u}] [ ...] Import a package. \`FILE\` can be either a path to a zip file, url or a FQPN. You can use \`--\` to separate \`FILE\`s from other arguments. Categories have to be separated with a space and have to be already present in murano. **Positional arguments:** ```` URL of the murano zip package, FQPN, path to zip package or path to directory with package. **Optional arguments:** ``-c [ [ ...]], --categories [ [ ...]]`` Category list to attach. ``--is-public`` Make the package available for users from other tenants. ``--package-version PACKAGE_VERSION`` Version of the package to use from repository (ignored when importing with multiple packages). ``--exists-action {a,s,u}`` Default action when a package already exists: (s)kip, (u)pdate, (a)bort. ``--dep-exists-action {a,s,u}`` Default action when a dependency package already exists: (s)kip, (u)pdate, (a)bort. .. _murano_package-list: murano package-list ------------------- .. code-block:: console usage: murano package-list [--limit LIMIT] [--marker MARKER] [--include-disabled] [--owned] [--search ] [--name ] [--fqn ] [--type ] [--category ] [--class_name ] [--tag ] List available packages. **Optional arguments:** ``--limit LIMIT`` Show limited number of packages ``--marker MARKER`` Show packages starting from package with id excluding it ``--include-disabled`` ``--owned`` ``--search `` Show packages, that match search keys fuzzily ``--name `` Show packages, whose name match parameter exactly ``--fqn `` Show packages, whose fully qualified name match parameter exactly ``--type `` Show packages, whose type match parameter exactly ``--category `` Show packages, whose categories include parameter ``--class_name `` Show packages, whose class name match parameter exactly ``--tag `` Show packages, whose tags include parameter .. _murano_package-save: murano package-save ------------------- .. code-block:: console usage: murano package-save [-p ] [--package-version PACKAGE_VERSION] [--no-images] [ ...] Save a package. This will download package(s) with all dependencies to specified path. If path doesn't exist it will be created. **Positional arguments:** ```` Package URL or name. **Optional arguments:** ``-p , --path `` Path to the directory to store package. If not set will use current directory. ``--package-version PACKAGE_VERSION`` Version of the package to use from repository (ignored when saving with multiple packages). ``--no-images`` If set will skip images downloading. .. _murano_package-show: murano package-show ------------------- .. code-block:: console usage: murano package-show Display details for a package. **Positional arguments:** ```` Package ID to show. .. _murano_package-update: murano package-update --------------------- .. code-block:: console usage: murano package-update [--is-public {true|false}] [--enabled {true|false}] [--name NAME] [--description DESCRIPTION] [--tags [ [ ...]]] Update an existing package. **Positional arguments:** ```` Package ID to update. **Optional arguments:** ``--is-public {true|false}`` Make package available to users from other tenants. ``--enabled {true|false}`` Make package active and available for deployments. ``--name NAME`` New name for the package. ``--description DESCRIPTION`` New package description. ``--tags [ [ ...]]`` A list of keywords connected to the application. .. _murano_static-action-call: murano static-action-call ------------------------- .. code-block:: console usage: murano static-action-call [--arguments [ [ ...]]] [--package-name ] [--class-version CLASS_VERSION] Call static method \`METHOD\` of the class \`CLASS\` with \`ARGUMENTS\`. Returns the result of the method execution. \`PACKAGE\` and \`CLASS_VERSION\` can be specified optionally to find class in a particular package and to look for the specific version of a class respectively. **Positional arguments:** ```` FQN of the class with static method ```` Static method to run **Optional arguments:** ``--arguments [ [ ...]]`` Method arguments. No arguments by default ``--package-name `` Optional FQN of the package to look for the class in ``--class-version CLASS_VERSION`` Optional version of the class, otherwise version =0 is used ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/doc/source/conf.py0000664000175000017500000000541300000000000021132 0ustar00zuulzuul00000000000000# -*- coding: utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os # -- General configuration ---------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = ['sphinx.ext.autodoc', 'openstackdocstheme',] # 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 # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. copyright = u'OpenStack Foundation' exclude_trees = ['api'] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. html_theme = 'openstackdocs' # Add any paths that contain custom themes here, relative to this directory. #html_theme_path = ['_theme'] #html_theme_path = [openstackdocstheme.get_html_theme_path()] # openstackdocstheme options openstackdocs_repo_name = 'openstack/python-muranoclient' openstackdocs_bug_project = 'python-muranoclient' openstackdocs_bug_tag = '' # Output file base name for HTML help builder. htmlhelp_basename = 'python-muranoclientdoc' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass # [howto/manual]). latex_documents = [ ( 'index', 'python-muranoclient.tex', u'python-muranoclient Documentation', u'OpenStack Foundation', 'manual' ), ] # Example configuration for intersphinx: refer to the Python standard library. intersphinx_mapping = {'http://docs.python.org/': None} ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1709301724.586079 python-muranoclient-2.8.0/doc/source/contributor/0000775000175000017500000000000000000000000022202 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/doc/source/contributor/contributing.rst0000664000175000017500000000361000000000000025443 0ustar00zuulzuul00000000000000============================ So You Want to Contribute... ============================ For general information on contributing to OpenStack, please check out the `contributor guide `_ to get started. It covers all the basics that are common to all OpenStack projects: the accounts you need, the basics of interacting with our Gerrit review system, how we communicate as a community, etc. Below will cover the more project specific information you need to get started with python-muranoclient. Communication ~~~~~~~~~~~~~ * IRC channel #murano at OFTC * Mailing list (prefix subjects with ``[murano]`` for faster responses) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss Contacting the Core Team ~~~~~~~~~~~~~~~~~~~~~~~~ Please refer the `python-muranoclient Core Team `_ contacts. New Feature Planning ~~~~~~~~~~~~~~~~~~~~ python-muranoclient features are tracked on `Launchpad `_. Task Tracking ~~~~~~~~~~~~~ We track our tasks in `Launchpad `_. If you're looking for some smaller, easier work item to pick up and get started on, search for the 'low-hanging-fruit' tag. Reporting a Bug ~~~~~~~~~~~~~~~ You found an issue and want to make sure we are aware of it? You can do so on `Launchpad `_. Getting Your Patch Merged ~~~~~~~~~~~~~~~~~~~~~~~~~ All changes proposed to the python-muranoclient project require one or two +2 votes from python-muranoclient core reviewers before one of the core reviewers can approve patch by giving ``Workflow +1`` vote. Project Team Lead Duties ~~~~~~~~~~~~~~~~~~~~~~~~ All common PTL duties are enumerated in the `PTL guide `_. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/doc/source/index.rst0000664000175000017500000000103300000000000021466 0ustar00zuulzuul00000000000000================================= python-muranoclient documentation ================================= This is a client for the OpenStack Application Catalog API. There's a Python API (the :mod:`muranoclient` module) and a :doc:`command-line script ` (installed as :program:`murano`). .. toctree:: :maxdepth: 2 cli/index For Contributors ================ * If you are a new contributor to python-muranoclient please refer: :doc:`contributor/contributing` .. toctree:: :hidden: contributor/contributing ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1709301724.586079 python-muranoclient-2.8.0/muranoclient/0000775000175000017500000000000000000000000020263 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/__init__.py0000664000175000017500000000164300000000000022400 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import pbr.version _ROOT = os.path.abspath(os.path.dirname(__file__)) def get_resource(path): return os.path.join(_ROOT, 'data', path) version_info = pbr.version.VersionInfo('python-muranoclient') try: __version__ = version_info.version_string() except AttributeError: __version__ = None ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5900803 python-muranoclient-2.8.0/muranoclient/apiclient/0000775000175000017500000000000000000000000022233 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/apiclient/__init__.py0000664000175000017500000000000000000000000024332 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/apiclient/auth.py0000664000175000017500000001544100000000000023553 0ustar00zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # Copyright 2013 Spanish National Research Council. # 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. # E0202: An attribute inherited from %s hide this method # pylint: disable=E0202 import abc import argparse import os from stevedore import extension from muranoclient.apiclient import exceptions _discovered_plugins = {} def discover_auth_systems(): """Discover the available auth-systems. This won't take into account the old style auth-systems. """ global _discovered_plugins _discovered_plugins = {} def add_plugin(ext): _discovered_plugins[ext.name] = ext.plugin ep_namespace = "muranoclient.apiclient.auth" mgr = extension.ExtensionManager(ep_namespace) mgr.map(add_plugin) def load_auth_system_opts(parser): """Load options needed by the available auth-systems into a parser. This function will try to populate the parser with options from the available plugins. """ group = parser.add_argument_group("Common auth options") BaseAuthPlugin.add_common_opts(group) for name, auth_plugin in _discovered_plugins.items(): group = parser.add_argument_group( "Auth-system '%s' options" % name, conflict_handler="resolve") auth_plugin.add_opts(group) def load_plugin(auth_system): try: plugin_class = _discovered_plugins[auth_system] except KeyError: raise exceptions.AuthSystemNotFound(auth_system) return plugin_class(auth_system=auth_system) def load_plugin_from_args(args): """Load required plugin and populate it with options. Try to guess auth system if it is not specified. Systems are tried in alphabetical order. :type args: argparse.Namespace :raises: AuthPluginOptionsMissing """ auth_system = args.os_auth_system if auth_system: plugin = load_plugin(auth_system) plugin.parse_opts(args) plugin.sufficient_options() return plugin for plugin_auth_system in sorted(iter(_discovered_plugins.keys())): plugin_class = _discovered_plugins[plugin_auth_system] plugin = plugin_class() plugin.parse_opts(args) try: plugin.sufficient_options() except exceptions.AuthPluginOptionsMissing: continue return plugin raise exceptions.AuthPluginOptionsMissing(["auth_system"]) class BaseAuthPlugin(object, metaclass=abc.ABCMeta): """Base class for authentication plugins. An authentication plugin needs to override at least the authenticate method to be a valid plugin. """ auth_system = None opt_names = [] common_opt_names = [ "auth_system", "username", "password", "tenant_name", "token", "auth_url", ] def __init__(self, auth_system=None, **kwargs): self.auth_system = auth_system or self.auth_system self.opts = dict((name, kwargs.get(name)) for name in self.opt_names) @staticmethod def _parser_add_opt(parser, opt): """Add an option to parser in two variants. :param opt: option name (with underscores) """ dashed_opt = opt.replace("_", "-") env_var = "OS_%s" % opt.upper() arg_default = os.environ.get(env_var, "") arg_help = "Defaults to env[%s]." % env_var parser.add_argument( "--os-%s" % dashed_opt, metavar="<%s>" % dashed_opt, default=arg_default, help=arg_help) parser.add_argument( "--os_%s" % opt, metavar="<%s>" % dashed_opt, help=argparse.SUPPRESS) @classmethod def add_opts(cls, parser): """Populate the parser with the options for this plugin.""" for opt in cls.opt_names: # use `BaseAuthPlugin.common_opt_names` since it is never # changed in child classes if opt not in BaseAuthPlugin.common_opt_names: cls._parser_add_opt(parser, opt) @classmethod def add_common_opts(cls, parser): """Add options that are common for several plugins.""" for opt in cls.common_opt_names: cls._parser_add_opt(parser, opt) @staticmethod def get_opt(opt_name, args): """Return option name and value. :param opt_name: name of the option, e.g., "username" :param args: parsed arguments """ return (opt_name, getattr(args, "os_%s" % opt_name, None)) def parse_opts(self, args): """Parse the actual auth-system options if any. This method is expected to populate the attribute `self.opts` with a dict containing the options and values needed to make authentication. """ self.opts.update(dict(self.get_opt(opt_name, args) for opt_name in self.opt_names)) def authenticate(self, http_client): """Authenticate using plugin defined method. The method usually analyses `self.opts` and performs a request to authentication server. :param http_client: client object that needs authentication :type http_client: HTTPClient :raises: AuthorizationFailure """ self.sufficient_options() self._do_authenticate(http_client) @abc.abstractmethod def _do_authenticate(self, http_client): """Protected method for authentication.""" def sufficient_options(self): """Check if all required options are present. :raises: AuthPluginOptionsMissing """ missing = [opt for opt in self.opt_names if not self.opts.get(opt)] if missing: raise exceptions.AuthPluginOptionsMissing(missing) @abc.abstractmethod def token_and_endpoint(self, endpoint_type, service_type): """Return token and endpoint. :param service_type: Service type of the endpoint :type service_type: string :param endpoint_type: Type of endpoint. Possible values: public or publicURL, internal or internalURL, admin or adminURL :type endpoint_type: string :returns: tuple of token and endpoint strings :raises: EndpointException """ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/apiclient/base.py0000664000175000017500000004077300000000000023532 0ustar00zuulzuul00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 OpenStack Foundation # Copyright 2012 Grid Dynamics # Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Base utilities to build API operation managers and objects on top of. """ # E1102: %s is not callable # pylint: disable=E1102 import abc import copy from oslo_utils import strutils from oslo_utils import uuidutils import parse from muranoclient.apiclient import exceptions from muranoclient.i18n import _ def getid(obj): """Return id if argument is a Resource. Abstracts the common pattern of allowing both an object or an object's ID (UUID) as a parameter when dealing with relationships. """ try: if obj.uuid: return obj.uuid except AttributeError: pass try: return obj.id except AttributeError: return obj # TODO(aababilov): call run_hooks() in HookableMixin's child classes class HookableMixin(object): """Mixin so classes can register and run hooks.""" _hooks_map = {} @classmethod def add_hook(cls, hook_type, hook_func): """Add a new hook of specified type. :param cls: class that registers hooks :param hook_type: hook type, e.g., '__pre_parse_args__' :param hook_func: hook function """ if hook_type not in cls._hooks_map: cls._hooks_map[hook_type] = [] cls._hooks_map[hook_type].append(hook_func) @classmethod def run_hooks(cls, hook_type, *args, **kwargs): """Run all hooks of specified type. :param cls: class that registers hooks :param hook_type: hook type, e.g., '__pre_parse_args__' :param args: args to be passed to every hook function :param kwargs: kwargs to be passed to every hook function """ hook_funcs = cls._hooks_map.get(hook_type) or [] for hook_func in hook_funcs: hook_func(*args, **kwargs) class BaseManager(HookableMixin): """Basic manager type providing common operations. Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. """ resource_class = None def __init__(self, client): """Initializes BaseManager with `client`. :param client: instance of BaseClient descendant for HTTP requests """ super(BaseManager, self).__init__() self.client = client def _list(self, url, response_key, obj_class=None, json=None): """List the collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, e.g., 'servers' :param obj_class: class for constructing the returned objects (self.resource_class will be used by default) :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) """ if json: body = self.client.post(url, json=json).json() else: body = self.client.get(url).json() if obj_class is None: obj_class = self.resource_class data = body[response_key] # NOTE(ja): keystone returns values as list as {'values': [ ... ]} # unlike other services which just return the list... try: data = data['values'] except (KeyError, TypeError): pass return [obj_class(self, res, loaded=True) for res in data if res] def _get(self, url, response_key): """Get an object from collection. :param url: a partial URL, e.g., '/servers' :param response_key: the key to be looked up in response dictionary, e.g., 'server' """ body = self.client.get(url).json() return self.resource_class(self, body[response_key], loaded=True) def _head(self, url): """Retrieve request headers for an object. :param url: a partial URL, e.g., '/servers' """ resp = self.client.head(url) return resp.status_code == 204 def _post(self, url, json, response_key, return_raw=False): """Create an object. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'servers' :param return_raw: flag to force returning raw JSON instead of Python object of self.resource_class """ body = self.client.post(url, json=json).json() if return_raw: return body[response_key] return self.resource_class(self, body[response_key]) def _put(self, url, json=None, response_key=None): """Update an object with PUT method. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'servers' """ resp = self.client.put(url, json=json) # PUT requests may not return a body if resp.content: body = resp.json() if response_key is not None: return self.resource_class(self, body[response_key]) else: return self.resource_class(self, body) def _patch(self, url, json=None, response_key=None): """Update an object with PATCH method. :param url: a partial URL, e.g., '/servers' :param json: data that will be encoded as JSON and passed in POST request (GET will be sent by default) :param response_key: the key to be looked up in response dictionary, e.g., 'servers' """ body = self.client.patch(url, json=json).json() if response_key is not None: return self.resource_class(self, body[response_key]) else: return self.resource_class(self, body) def _delete(self, url): """Delete an object. :param url: a partial URL, e.g., '/servers/my-server' """ return self.client.delete(url) class ManagerWithFind(BaseManager, metaclass=abc.ABCMeta): """Manager with additional `find()`/`findall()` methods.""" @abc.abstractmethod def list(self): pass def find(self, **kwargs): """Find a single item with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ matches = self.findall(**kwargs) num_matches = len(matches) if num_matches == 0: msg = _("No %(name)s matching %(args)s.") % { 'name': self.resource_class.__name__, 'args': kwargs } raise exceptions.NotFound(msg) elif num_matches > 1: raise exceptions.NoUniqueMatch() else: return matches[0] def findall(self, **kwargs): """Find all items with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ found = [] searches = kwargs.items() for obj in self.list(): try: if all(getattr(obj, attr) == value for (attr, value) in searches): found.append(obj) except AttributeError: continue return found class CrudManager(BaseManager): """Base manager class for manipulating entities. Children of this class are expected to define a `collection_key` and `key`. - `collection_key`: Usually a plural noun by convention (e.g. `entities`); used to refer collections in both URL's (e.g. `/v3/entities`) and JSON objects containing a list of member resources (e.g. `{'entities': [{}, {}, {}]}`). - `key`: Usually a singular noun by convention (e.g. `entity`); used to refer to an individual member of the collection. """ collection_key = None key = None def build_url(self, base_url=None, **kwargs): """Builds a resource URL for the given kwargs. Given an example collection where `collection_key = 'entities'` and `key = 'entity'`, the following URL's could be generated. By default, the URL will represent a collection of entities, e.g.:: /entities If kwargs contains an `entity_id`, then the URL will represent a specific member, e.g.:: /entities/{entity_id} :param base_url: if provided, the generated URL will be appended to it """ url = base_url if base_url is not None else '' url += '/%s' % self.collection_key # do we have a specific entity? entity_id = kwargs.get('%s_id' % self.key) if entity_id is not None: url += '/%s' % entity_id return url def _filter_kwargs(self, kwargs): """Drop null values and handle ids.""" for key, ref in kwargs.copy().items(): if ref is None: kwargs.pop(key) else: if isinstance(ref, Resource): kwargs.pop(key) kwargs['%s_id' % key] = getid(ref) return kwargs def create(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._post( self.build_url(**kwargs), {self.key: kwargs}, self.key) def get(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._get( self.build_url(**kwargs), self.key) def head(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._head(self.build_url(**kwargs)) def list(self, base_url=None, **kwargs): """List the collection. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) return self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', }, self.collection_key) def put(self, base_url=None, **kwargs): """Update an element. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) return self._put(self.build_url(base_url=base_url, **kwargs)) def update(self, **kwargs): kwargs = self._filter_kwargs(kwargs) params = kwargs.copy() params.pop('%s_id' % self.key) return self._patch( self.build_url(**kwargs), {self.key: params}, self.key) def delete(self, **kwargs): kwargs = self._filter_kwargs(kwargs) return self._delete( self.build_url(**kwargs)) def find(self, base_url=None, **kwargs): """Find a single item with attributes matching ``**kwargs``. :param base_url: if provided, the generated URL will be appended to it """ kwargs = self._filter_kwargs(kwargs) rl = self._list( '%(base_url)s%(query)s' % { 'base_url': self.build_url(base_url=base_url, **kwargs), 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', }, self.collection_key) num = len(rl) if num == 0: msg = _("No %(name)s matching %(args)s.") % { 'name': self.resource_class.__name__, 'args': kwargs } raise exceptions.NotFound(404, msg) elif num > 1: raise exceptions.NoUniqueMatch else: return rl[0] class Extension(HookableMixin): """Extension descriptor.""" SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') manager_class = None def __init__(self, name, module): super(Extension, self).__init__() self.name = name self.module = module self._parse_extension_module() def _parse_extension_module(self): self.manager_class = None for attr_name, attr_value in self.module.__dict__.items(): if attr_name in self.SUPPORTED_HOOKS: self.add_hook(attr_name, attr_value) else: try: if issubclass(attr_value, BaseManager): self.manager_class = attr_value except TypeError: pass def __repr__(self): return "" % self.name class Resource(object): """Base class for OpenStack resources (tenant, user, etc.). This is pretty much just a bag for attributes. """ HUMAN_ID = False NAME_ATTR = 'name' def __init__(self, manager, info, loaded=False): """Populate and bind to a manager. :param manager: BaseManager object :param info: dictionary representing resource attributes :param loaded: prevent lazy-loading if set to True """ self.manager = manager self._info = info self._add_details(info) self._loaded = loaded self._init_completion_cache() def _init_completion_cache(self): cache_write = getattr(self.manager, 'write_to_completion_cache', None) if not cache_write: return # NOTE(sirp): ensure `id` is already present because if it isn't we'll # enter an infinite loop of __getattr__ -> get -> __init__ -> # __getattr__ -> ... if 'id' in self.__dict__ and uuidutils.is_uuid_like(self.id): cache_write('uuid', self.id) if self.human_id: cache_write('human_id', self.human_id) def __repr__(self): reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and k != 'manager') info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) return "<%s %s>" % (self.__class__.__name__, info) @property def human_id(self): """Human-readable ID which can be used for bash completion.""" if self.HUMAN_ID: name = getattr(self, self.NAME_ATTR, None) if name is not None: return strutils.to_slug(name) return None def _add_details(self, info): for (k, v) in info.items(): try: setattr(self, k, v) self._info[k] = v except AttributeError: # In this case we already defined the attribute on the class pass def __getattr__(self, k): if k not in self.__dict__: # NOTE(bcwaldon): disallow lazy-loading if already loaded once if not self.is_loaded(): self.get() return self.__getattr__(k) raise AttributeError(k) else: return self.__dict__[k] def get(self): """Support for lazy loading details. Some clients, such as novaclient have the option to lazy load the details, details which can be loaded with this function. """ # set_loaded() first ... so if we have to bail, we know we tried. self.set_loaded(True) if not hasattr(self.manager, 'get'): return new = self.manager.get(self.id) if new: self._add_details(new._info) def __eq__(self, other): if not isinstance(other, Resource): return NotImplemented # two resources of different types are not equal if not isinstance(other, self.__class__): return False return self._info == other._info def __ne__(self, other): return not self.__eq__(other) def is_loaded(self): return self._loaded def set_loaded(self, val): self._loaded = val def to_dict(self): return copy.deepcopy(self._info) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/apiclient/client.py0000664000175000017500000003100100000000000024056 0ustar00zuulzuul00000000000000# Copyright 2010 Jacob Kaplan-Moss # Copyright 2011 OpenStack Foundation # Copyright 2011 Piston Cloud Computing, Inc. # Copyright 2013 Alessio Ababilov # Copyright 2013 Grid Dynamics # Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ OpenStack Client interface. Handles the REST calls and responses. """ # E0202: An attribute inherited from %s hide this method # pylint: disable=E0202 try: import simplejson as json except ImportError: import json import time from oslo_log import log as logging from oslo_utils import importutils import requests from muranoclient.apiclient import exceptions from muranoclient.i18n import _ _logger = logging.getLogger(__name__) class HTTPClient(object): """This client handles sending HTTP requests to OpenStack servers. Features: - share authentication information between several clients to different services (e.g., for compute and image clients); - reissue authentication request for expired tokens; - encode/decode JSON bodies; - raise exceptions on HTTP errors; - pluggable authentication; - store authentication information in a keyring; - store time spent for requests; - register clients for particular services, so one can use `http_client.identity` or `http_client.compute`; - log requests and responses in a format that is easy to copy-and-paste into terminal and send the same request with curl. """ user_agent = "muranoclient.apiclient" def __init__(self, auth_plugin, region_name=None, endpoint_type="publicURL", original_ip=None, verify=True, cert=None, timeout=None, timings=False, keyring_saver=None, debug=False, user_agent=None, http=None): self.auth_plugin = auth_plugin self.endpoint_type = endpoint_type self.region_name = region_name self.original_ip = original_ip self.timeout = timeout self.verify = verify self.cert = cert self.keyring_saver = keyring_saver self.debug = debug self.user_agent = user_agent or self.user_agent self.times = [] # [("item", starttime, endtime), ...] self.timings = timings # requests within the same session can reuse TCP connections from pool self.http = http or requests.Session() self.cached_token = None def _http_log_req(self, url, method, kwargs): if not self.debug: return string_parts = [ "curl -i", "-X '%s'" % method, "'%s'" % url, ] for element in kwargs['headers']: header = "-H '%s: %s'" % (element, kwargs['headers'][element]) string_parts.append(header) _logger.debug("REQ: %s" % " ".join(string_parts)) if 'data' in kwargs: _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) def _http_log_resp(self, resp): if not self.debug: return _logger.debug( "RESP: [%s] %s\n", resp.status_code, resp.headers) if resp._content_consumed: _logger.debug( "RESP BODY: %s\n", resp.text) def serialize(self, kwargs): if kwargs.get('json') is not None: kwargs['headers']['Content-Type'] = 'application/json' kwargs['data'] = json.dumps(kwargs.pop('json')) def get_timings(self): return self.times def reset_timings(self): self.times = [] def request(self, url, method, **kwargs): """Send an http request with the specified characteristics. Wrapper around `requests.Session.request` to handle tasks such as setting headers, JSON encoding/decoding, and error handling. :param method: method of HTTP request :param url: URL of HTTP request :param kwargs: any other parameter that can be passed to requests.Session.request (such as `headers`) or `json` that will be encoded as JSON and used as `data` argument """ kwargs.setdefault("headers", kwargs.get("headers", {})) kwargs["headers"]["User-Agent"] = self.user_agent if self.original_ip: kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( self.original_ip, self.user_agent) if self.timeout is not None: kwargs.setdefault("timeout", self.timeout) kwargs.setdefault("verify", self.verify) if self.cert is not None: kwargs.setdefault("cert", self.cert) self.serialize(kwargs) self._http_log_req(url, method, kwargs) if self.timings: start_time = time.time() resp = self.http.request(url, method, **kwargs) if self.timings: self.times.append(("%s %s" % (url, method), start_time, time.time())) self._http_log_resp(resp) if resp.status_code >= 400: _logger.debug( "Request returned failure status: %s", resp.status_code) raise exceptions.from_response(resp, url, method) return resp @staticmethod def concat_url(endpoint, url): """Concatenate endpoint and final URL. E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to "http://keystone/v2.0/tokens". :param endpoint: the base URL :param url: the final URL """ return "%s/%s" % (endpoint.rstrip("/"), url.strip("/")) def client_request(self, client, url, method, **kwargs): """Send an http request using `client`'s endpoint and specified `url`. If request was rejected as unauthorized (possibly because the token is expired), issue one authorization attempt and send the request once again. :param client: instance of BaseClient descendant :param method: method of HTTP request :param url: URL of HTTP request :param kwargs: any other parameter that can be passed to `HTTPClient.request` """ filter_args = { "endpoint_type": client.endpoint_type or self.endpoint_type, "service_type": client.service_type, } token, endpoint = (self.cached_token, client.cached_endpoint) just_authenticated = False if not (token and endpoint): try: token, endpoint = self.auth_plugin.token_and_endpoint( **filter_args) except exceptions.EndpointException: pass if not (token and endpoint): self.authenticate() just_authenticated = True token, endpoint = self.auth_plugin.token_and_endpoint( **filter_args) if not (token and endpoint): raise exceptions.AuthorizationFailure( _("Cannot find endpoint or token for request")) old_token_endpoint = (token, endpoint) kwargs.setdefault("headers", {})["X-Auth-Token"] = token self.cached_token = token client.cached_endpoint = endpoint # Perform the request once. If we get Unauthorized, then it # might be because the auth token expired, so try to # re-authenticate and try again. If it still fails, bail. try: return self.request( method, self.concat_url(endpoint, url), **kwargs) except exceptions.Unauthorized as unauth_ex: if just_authenticated: raise self.cached_token = None client.cached_endpoint = None self.authenticate() try: token, endpoint = self.auth_plugin.token_and_endpoint( **filter_args) except exceptions.EndpointException: raise unauth_ex if (not (token and endpoint) or old_token_endpoint == (token, endpoint)): raise unauth_ex self.cached_token = token client.cached_endpoint = endpoint kwargs["headers"]["X-Auth-Token"] = token return self.request( method, self.concat_url(endpoint, url), **kwargs) def add_client(self, base_client_instance): """Add a new instance of :class:`BaseClient` descendant. `self` will store a reference to `base_client_instance`. Example: >>> def test_clients(): ... from keystoneclient.auth import keystone ... from openstack.common.apiclient import client ... auth = keystone.KeystoneAuthPlugin( ... username="user", password="pass", tenant_name="tenant", ... auth_url="http://auth:5000/v2.0") ... openstack_client = client.HTTPClient(auth) ... # create nova client ... from novaclient.v1_1 import client ... client.Client(openstack_client) ... # create keystone client ... from keystoneclient.v2_0 import client ... client.Client(openstack_client) ... # use them ... openstack_client.identity.tenants.list() ... openstack_client.compute.servers.list() """ service_type = base_client_instance.service_type if service_type and not hasattr(self, service_type): setattr(self, service_type, base_client_instance) def authenticate(self): self.auth_plugin.authenticate(self) # Store the authentication results in the keyring for later requests if self.keyring_saver: self.keyring_saver.save(self) class BaseClient(object): """Top-level object to access the OpenStack API. This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient` will handle a bunch of issues such as authentication. """ service_type = None endpoint_type = None # "publicURL" will be used cached_endpoint = None def __init__(self, http_client, extensions=None): self.http_client = http_client http_client.add_client(self) # Add in any extensions... if extensions: for extension in extensions: if extension.manager_class: setattr(self, extension.name, extension.manager_class(self)) def client_request(self, url, method, **kwargs): return self.http_client.client_request( self, url, method, **kwargs) def head(self, url, **kwargs): return self.client_request(url, "HEAD", **kwargs) def get(self, url, **kwargs): return self.client_request(url, "GET", **kwargs) def post(self, url, **kwargs): return self.client_request(url, "POST", **kwargs) def put(self, url, **kwargs): return self.client_request(url, "PUT", **kwargs) def delete(self, url, **kwargs): return self.client_request(url, "DELETE", **kwargs) def patch(self, url, **kwargs): return self.client_request(url, "PATCH", **kwargs) @staticmethod def get_class(api_name, version, version_map): """Returns the client class for the requested API version :param api_name: the name of the API, e.g. 'compute', 'image', etc :param version: the requested API version :param version_map: a dict of client classes keyed by version :rtype: a client class for the requested API version """ try: client_path = version_map[str(version)] except (KeyError, ValueError): msg = _("Invalid %(api_name)s client version '%(version)s'. " "Must be one of: %(version_map)s") % { 'api_name': api_name, 'version': version, 'version_map': ', '.join(version_map.keys())} raise exceptions.UnsupportedVersion(msg) return importutils.import_class(client_path) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/apiclient/exceptions.py0000664000175000017500000003024600000000000024773 0ustar00zuulzuul00000000000000# Copyright 2011 Nebula, Inc. # Copyright 2013 Alessio Ababilov # Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Exception definitions. """ import inspect import sys from muranoclient.i18n import _ class ClientException(Exception): """The base exception class for all exceptions this library raises.""" pass class MissingArgs(ClientException): """Supplied arguments are not sufficient for calling a function.""" def __init__(self, missing): self.missing = missing msg = _("Missing arguments: %s") % ", ".join(missing) super(MissingArgs, self).__init__(msg) class ValidationError(ClientException): """Error in validation on API client side.""" pass class UnsupportedVersion(ClientException): """User is trying to use an unsupported version of the API.""" pass class CommandError(ClientException): """Error in CLI tool.""" pass class AuthorizationFailure(ClientException): """Cannot authorize API client.""" pass class ConnectionRefused(ClientException): """Cannot connect to API service.""" pass class AuthPluginOptionsMissing(AuthorizationFailure): """Auth plugin misses some options.""" def __init__(self, opt_names): super(AuthPluginOptionsMissing, self).__init__( _("Authentication failed. Missing options: %s") % ", ".join(opt_names)) self.opt_names = opt_names class AuthSystemNotFound(AuthorizationFailure): """User has specified an AuthSystem that is not installed.""" def __init__(self, auth_system): super(AuthSystemNotFound, self).__init__( _("AuthSystemNotFound: %s") % repr(auth_system)) self.auth_system = auth_system class NoUniqueMatch(ClientException): """Multiple entities found instead of one.""" pass class EndpointException(ClientException): """Something is rotten in Service Catalog.""" pass class EndpointNotFound(EndpointException): """Could not find requested endpoint in Service Catalog.""" pass class AmbiguousEndpoints(EndpointException): """Found more than one matching endpoint in Service Catalog.""" def __init__(self, endpoints=None): super(AmbiguousEndpoints, self).__init__( _("AmbiguousEndpoints: %s") % repr(endpoints)) self.endpoints = endpoints class HttpError(ClientException): """The base exception class for all HTTP exceptions.""" http_status = 0 message = _("HTTP Error") def __init__(self, message=None, details=None, response=None, request_id=None, url=None, method=None, http_status=None): self.http_status = http_status or self.http_status self.message = message or self.message self.details = details self.request_id = request_id self.response = response self.url = url self.method = method formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) if request_id: formatted_string += " (Request-ID: %s)" % request_id super(HttpError, self).__init__(formatted_string) class HTTPRedirection(HttpError): """HTTP Redirection.""" message = _("HTTP Redirection") class HTTPClientError(HttpError): """Client-side HTTP error. Exception for cases in which the client seems to have erred. """ message = _("HTTP Client Error") class HttpServerError(HttpError): """Server-side HTTP error. Exception for cases in which the server is aware that it has erred or is incapable of performing the request. """ message = _("HTTP Server Error") class MultipleChoices(HTTPRedirection): """HTTP 300 - Multiple Choices. Indicates multiple options for the resource that the client may follow. """ http_status = 300 message = _("Multiple Choices") class BadRequest(HTTPClientError): """HTTP 400 - Bad Request. The request cannot be fulfilled due to bad syntax. """ http_status = 400 message = _("Bad Request") class Unauthorized(HTTPClientError): """HTTP 401 - Unauthorized. Similar to 403 Forbidden, but specifically for use when authentication is required and has failed or has not yet been provided. """ http_status = 401 message = _("Unauthorized") class PaymentRequired(HTTPClientError): """HTTP 402 - Payment Required. Reserved for future use. """ http_status = 402 message = _("Payment Required") class Forbidden(HTTPClientError): """HTTP 403 - Forbidden. The request was a valid request, but the server is refusing to respond to it. """ http_status = 403 message = _("Forbidden") class NotFound(HTTPClientError): """HTTP 404 - Not Found. The requested resource could not be found but may be available again in the future. """ http_status = 404 message = _("Not Found") class MethodNotAllowed(HTTPClientError): """HTTP 405 - Method Not Allowed. A request was made of a resource using a request method not supported by that resource. """ http_status = 405 message = _("Method Not Allowed") class NotAcceptable(HTTPClientError): """HTTP 406 - Not Acceptable. The requested resource is only capable of generating content not acceptable according to the Accept headers sent in the request. """ http_status = 406 message = _("Not Acceptable") class ProxyAuthenticationRequired(HTTPClientError): """HTTP 407 - Proxy Authentication Required. The client must first authenticate itself with the proxy. """ http_status = 407 message = _("Proxy Authentication Required") class RequestTimeout(HTTPClientError): """HTTP 408 - Request Timeout. The server timed out waiting for the request. """ http_status = 408 message = _("Request Timeout") class Conflict(HTTPClientError): """HTTP 409 - Conflict. Indicates that the request could not be processed because of conflict in the request, such as an edit conflict. """ http_status = 409 message = _("Conflict") class Gone(HTTPClientError): """HTTP 410 - Gone. Indicates that the resource requested is no longer available and will not be available again. """ http_status = 410 message = _("Gone") class LengthRequired(HTTPClientError): """HTTP 411 - Length Required. The request did not specify the length of its content, which is required by the requested resource. """ http_status = 411 message = _("Length Required") class PreconditionFailed(HTTPClientError): """HTTP 412 - Precondition Failed. The server does not meet one of the preconditions that the requester put on the request. """ http_status = 412 message = _("Precondition Failed") class RequestEntityTooLarge(HTTPClientError): """HTTP 413 - Request Entity Too Large. The request is larger than the server is willing or able to process. """ http_status = 413 message = _("Request Entity Too Large") def __init__(self, *args, **kwargs): try: self.retry_after = int(kwargs.pop('retry_after')) except (KeyError, ValueError): self.retry_after = 0 super(RequestEntityTooLarge, self).__init__(*args, **kwargs) class RequestUriTooLong(HTTPClientError): """HTTP 414 - Request-URI Too Long. The URI provided was too long for the server to process. """ http_status = 414 message = _("Request-URI Too Long") class UnsupportedMediaType(HTTPClientError): """HTTP 415 - Unsupported Media Type. The request entity has a media type which the server or resource does not support. """ http_status = 415 message = _("Unsupported Media Type") class RequestedRangeNotSatisfiable(HTTPClientError): """HTTP 416 - Requested Range Not Satisfiable. The client has asked for a portion of the file, but the server cannot supply that portion. """ http_status = 416 message = _("Requested Range Not Satisfiable") class ExpectationFailed(HTTPClientError): """HTTP 417 - Expectation Failed. The server cannot meet the requirements of the Expect request-header field. """ http_status = 417 message = _("Expectation Failed") class UnprocessableEntity(HTTPClientError): """HTTP 422 - Unprocessable Entity. The request was well-formed but was unable to be followed due to semantic errors. """ http_status = 422 message = _("Unprocessable Entity") class InternalServerError(HttpServerError): """HTTP 500 - Internal Server Error. A generic error message, given when no more specific message is suitable. """ http_status = 500 message = _("Internal Server Error") # NotImplemented is a python keyword. class HttpNotImplemented(HttpServerError): """HTTP 501 - Not Implemented. The server either does not recognize the request method, or it lacks the ability to fulfill the request. """ http_status = 501 message = _("Not Implemented") class BadGateway(HttpServerError): """HTTP 502 - Bad Gateway. The server was acting as a gateway or proxy and received an invalid response from the upstream server. """ http_status = 502 message = _("Bad Gateway") class ServiceUnavailable(HttpServerError): """HTTP 503 - Service Unavailable. The server is currently unavailable. """ http_status = 503 message = _("Service Unavailable") class GatewayTimeout(HttpServerError): """HTTP 504 - Gateway Timeout. The server was acting as a gateway or proxy and did not receive a timely response from the upstream server. """ http_status = 504 message = _("Gateway Timeout") class HttpVersionNotSupported(HttpServerError): """HTTP 505 - HttpVersion Not Supported. The server does not support the HTTP protocol version used in the request. """ http_status = 505 message = _("HTTP Version Not Supported") # _code_map contains all the classes that have http_status attribute. _code_map = dict( (getattr(obj, 'http_status', None), obj) for name, obj in vars(sys.modules[__name__]).items() if inspect.isclass(obj) and getattr(obj, 'http_status', False) ) def from_response(response, url, method): """Returns an instance of :class:`HttpError` or subclass based on response. :param response: instance of `requests.Response` class :param method: HTTP method used for request :param url: URL used for request """ req_id = response.headers.get("x-openstack-request-id") # NOTE(hdd): true for older versions of nova and cinder if not req_id: req_id = response.headers.get("x-compute-request-id") kwargs = { "http_status": response.status_code, "response": response, "method": method, "url": url, "request_id": req_id, } if "retry-after" in response.headers: kwargs["retry_after"] = response.headers["retry-after"] content_type = response.headers.get("Content-Type", "") if content_type.startswith("application/json"): try: body = response.json() except ValueError: pass else: if isinstance(body, dict): error = list(body.values())[0] kwargs["message"] = error.get("message") kwargs["details"] = error.get("details") elif content_type.startswith("text/"): kwargs["details"] = response.text try: cls = _code_map[response.status_code] except KeyError: if 500 <= response.status_code < 600: cls = HttpServerError elif 400 <= response.status_code < 500: cls = HTTPClientError else: cls = HttpError return cls(**kwargs) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/apiclient/fake_client.py0000664000175000017500000001344000000000000025053 0ustar00zuulzuul00000000000000# Copyright 2013 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ A fake server that "responds" to API methods with pre-canned responses. All of these responses come from the spec, so if for some reason the spec's wrong the tests might raise AssertionError. I've indicated in comments the places where actual behavior differs from the spec. """ # W0102: Dangerous default value %s as argument # pylint: disable=W0102 import json import parse import requests from muranoclient.apiclient import client def assert_has_keys(dct, required=None, optional=None): if required is None: required = [] if optional is None: optional = [] for k in required: try: assert k in dct except AssertionError: extra_keys = set(dct.keys()).difference(set(required + optional)) raise AssertionError("found unexpected keys: %s" % list(extra_keys)) class TestResponse(requests.Response): """Wrap requests.Response and provide a convenient initialization.""" def __init__(self, data): super(TestResponse, self).__init__() self._content_consumed = True if isinstance(data, dict): self.status_code = data.get('status_code', 200) # Fake the text attribute to streamline Response creation text = data.get('text', "") if isinstance(text, (dict, list)): self._content = json.dumps(text) default_headers = { "Content-Type": "application/json", } else: self._content = text default_headers = {} if isinstance(self._content, str): self._content = self._content.encode('utf-8', 'strict') self.headers = data.get('headers') or default_headers else: self.status_code = data def __eq__(self, other): return (self.status_code == other.status_code and self.headers == other.headers and self._content == other._content) def __ne__(self, other): return not self.__eq__(other) class FakeHTTPClient(client.HTTPClient): def __init__(self, *args, **kwargs): self.callstack = [] self.fixtures = kwargs.pop("fixtures", None) or {} if not args and "auth_plugin" not in kwargs: args = (None, ) super(FakeHTTPClient, self).__init__(*args, **kwargs) def assert_called(self, url, method, body=None, pos=-1): """Assert than an API method was just called.""" expected = (url, method) called = self.callstack[pos][0:2] assert self.callstack, \ "Expected %s %s but no calls were made." % expected assert expected == called, 'Expected %s %s; got %s %s' % \ (expected + called) if body is not None: if self.callstack[pos][3] != body: raise AssertionError('%r != %r' % (self.callstack[pos][3], body)) def assert_called_anytime(self, url, method, body=None): """Assert than an API method was called anytime in the test.""" expected = (url, method) assert self.callstack, \ "Expected %s %s but no calls were made." % expected found = False entry = None for entry in self.callstack: if expected == entry[0:2]: found = True break assert found, 'Expected %s %s; got %s' % \ (url, method, self.callstack) if body is not None: assert entry[3] == body, "%s != %s" % (entry[3], body) self.callstack = [] def clear_callstack(self): self.callstack = [] def authenticate(self): pass def client_request(self, client, url, method, **kwargs): # Check that certain things are called correctly if method in ["GET", "DELETE"]: assert "json" not in kwargs # Note the call self.callstack.append( (url, method, kwargs.get("headers") or {}, kwargs.get("json") or kwargs.get("data"))) try: fixture = self.fixtures[url][method] except KeyError: pass else: return TestResponse({"headers": fixture[0], "text": fixture[1]}) # Call the method args = parse.parse_qsl(parse.urlparse(url)[4]) kwargs.update(args) munged_url = url.rsplit('?', 1)[0] munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') munged_url = munged_url.replace('-', '_') callback = "%s_%s" % (method.lower(), munged_url) if not hasattr(self, callback): raise AssertionError('Called unknown API method: %s %s, ' 'expected fakes method name: %s' % (url, method, callback)) resp = getattr(self, callback)(**kwargs) if len(resp) == 3: status, headers, body = resp else: status, body = resp headers = {} return TestResponse({ "status_code": status, "text": body, "headers": headers, }) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/client.py0000664000175000017500000000161300000000000022114 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_utils import importutils def Client(version, *args, **kwargs): module = importutils.import_versioned_module('muranoclient', version, 'client') client_class = getattr(module, 'Client') return client_class(*args, **kwargs) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5900803 python-muranoclient-2.8.0/muranoclient/common/0000775000175000017500000000000000000000000021553 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/common/__init__.py0000664000175000017500000000000000000000000023652 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/common/base.py0000664000175000017500000001603600000000000023045 0ustar00zuulzuul00000000000000# 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 abc import copy from muranoclient.apiclient import exceptions def getid(obj): """Get obj's id or object itself if no id Abstracts the common pattern of allowing both an object or an object's ID (UUID) as a parameter when dealing with relationships. """ try: return obj.id except AttributeError: return obj class Manager(object): """Interacts with type of API Managers interact with a particular type of API (servers, flavors, images, etc.) and provide CRUD operations for them. """ resource_class = None def __init__(self, api): self.api = api def _list(self, url, response_key=None, obj_class=None, data=None, headers=None): if headers is None: headers = {} resp, body = self.api.json_request(url, 'GET', headers=headers) if obj_class is None: obj_class = self.resource_class if response_key: if response_key not in body: body[response_key] = [] data = body[response_key] else: data = body return [obj_class(self, res, loaded=True) for res in data if res] def _delete(self, url, headers=None): if headers is None: headers = {} self.api.request(url, 'DELETE', headers=headers) def _update(self, url, data, response_key=None, return_raw=False, headers=None, method='PUT', content_type='application/json'): if headers is None: headers = {} resp, body = self.api.json_request(url, method, content_type=content_type, data=data, headers=headers) # PUT or PATCH requests may not return a body if body: if return_raw: if response_key: return body[response_key] return body if response_key: return self.resource_class(self, body[response_key]) return self.resource_class(self, body) def _create(self, url, data=None, response_key=None, return_raw=False, headers=None): if headers is None: headers = {} if data: resp, body = self.api.json_request(url, 'POST', data=data, headers=headers) else: resp, body = self.api.json_request(url, 'POST', headers=headers) if return_raw: if response_key: return body[response_key] return body if response_key: return self.resource_class(self, body[response_key]) return self.resource_class(self, body) def _get(self, url, response_key=None, return_raw=False, headers=None): if headers is None: headers = {} resp, body = self.api.json_request(url, 'GET', headers=headers) if return_raw: if response_key: return body[response_key] return body if response_key: return self.resource_class(self, body[response_key]) return self.resource_class(self, body) class ManagerWithFind(Manager, metaclass=abc.ABCMeta): """Manager with additional `find()`/`findall()` methods.""" @abc.abstractmethod def list(self): pass def find(self, **kwargs): """Find a single item with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ rl = self.findall(**kwargs) num = len(rl) if num == 0: msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) raise exceptions.NotFound(msg) elif num > 1: raise exceptions.NoUniqueMatch else: return self.get(rl[0].id) def findall(self, **kwargs): """Find all items with attributes matching ``**kwargs``. This isn't very efficient: it loads the entire list then filters on the Python side. """ found = [] searches = kwargs.items() for obj in self.list(): try: if all(getattr(obj, attr) == value for (attr, value) in searches): found.append(obj) except AttributeError: continue return found class Resource(object): """Represents an instance of an object A resource represents a particular instance of an object (tenant, user, etc). This is pretty much just a bag for attributes. :param manager: Manager object :param info: dictionary representing resource attributes :param loaded: prevent lazy-loading if set to True """ def __init__(self, manager, info, loaded=False): self.manager = manager self._info = info self._add_details(info) self._loaded = loaded def _add_details(self, info): for k, v in info.items(): setattr(self, k, v) def __setstate__(self, d): for k, v in d.items(): setattr(self, k, v) def __getattr__(self, k): if k not in self.__dict__: # NOTE(bcwaldon): disallow lazy-loading if already loaded once if not self.is_loaded(): self.get() return self.__getattr__(k) raise AttributeError(k) else: return self.__dict__[k] def __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 get(self): # set_loaded() first ... so if we have to bail, we know we tried. self.set_loaded(True) if not hasattr(self.manager, 'get'): return new = self.manager.get(self.id) if new: self._add_details(new._info) def __eq__(self, other): if not isinstance(other, self.__class__): return False return self._info == other._info def __ne__(self, other): return not self.__eq__(other) def is_loaded(self): return self._loaded def set_loaded(self, val): self._loaded = val def to_dict(self): return copy.deepcopy(self._info) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/common/exceptions.py0000664000175000017500000001156600000000000024317 0ustar00zuulzuul00000000000000# 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 re import sys # TODO(sjmc7): This module is likely redundant because it's replaced # by openstack.common.apiclient; should be removed class BaseException(Exception): """An error occurred.""" def __init__(self, message=None): self.message = message def __str__(self): return self.message or self.__class__.__doc__ class InvalidEndpoint(BaseException): """The provided endpoint is invalid.""" class CommunicationError(BaseException): """Unable to communicate with server.""" class ClientException(Exception): """DEPRECATED!""" class HTTPException(ClientException): """Base exception for all HTTP-derived exceptions.""" code = 'N/A' def __init__(self, details=None): self.details = details or self.__class__.__name__ def __str__(self): return "%s (HTTP %s)" % (self.details, self.code) class HTTPMultipleChoices(HTTPException): code = 300 def __str__(self): self.details = ("Requested version of Application Catalog API is not " "available.") return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code, self.details) class BadRequest(HTTPException): """DEPRECATED!""" code = 400 class HTTPBadRequest(BadRequest): pass class Unauthorized(HTTPException): """DEPRECATED!""" code = 401 class HTTPUnauthorized(Unauthorized): pass class Forbidden(HTTPException): """DEPRECATED!""" code = 403 class HTTPForbidden(Forbidden): pass class NotFound(HTTPException): """DEPRECATED!""" code = 404 class HTTPNotFound(NotFound): pass class HTTPMethodNotAllowed(HTTPException): code = 405 class Conflict(HTTPException): """DEPRECATED!""" code = 409 class HTTPConflict(Conflict): pass class OverLimit(HTTPException): """DEPRECATED!""" code = 413 class HTTPOverLimit(OverLimit): pass class HTTPInternalServerError(HTTPException): code = 500 class HTTPNotImplemented(HTTPException): code = 501 class HTTPBadGateway(HTTPException): code = 502 class ServiceUnavailable(HTTPException): """DEPRECATED!""" code = 503 class HTTPServiceUnavailable(ServiceUnavailable): pass # NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception # classes _code_map = {} for obj_name in dir(sys.modules[__name__]): if obj_name.startswith('HTTP'): obj = getattr(sys.modules[__name__], obj_name) _code_map[obj.code] = obj def from_response(response): """Return an instance of an HTTPException based on httplib response.""" cls = _code_map.get(response.status_code, HTTPException) body = response.content if body and response.headers['content-type'].\ lower().startswith("application/json"): # Iterate over the nested objects and retrieve the "message" attribute. messages = [obj.get('message') for obj in response.json().values()] # Join all of the messages together nicely and filter out any objects # that don't have a "message" attr. details = '\n'.join(i for i in messages if i is not None) return cls(details=details) elif body and \ response.headers['content-type'].lower().startswith("text/html"): # Split the lines, strip whitespace and inline HTML from the response. details = [re.sub(r'<.+?>', '', i.strip()) for i in response.text.splitlines()] details = [i for i in details if i] # Remove duplicates from the list. details_seen = set() details_temp = [] for i in details: if i not in details_seen: details_temp.append(i) details_seen.add(i) # Return joined string separated by colons. details = ': '.join(details_temp) return cls(details=details) elif body: details = body.replace('\n\n', '\n') return cls(details=details) return cls() def from_code(code): cls = _code_map.get(code, HTTPException) return cls() class NoTokenLookupException(Exception): """DEPRECATED!""" pass class EndpointNotFound(Exception): """DEPRECATED!""" pass class SSLConfigurationError(BaseException): pass class SSLCertificateError(BaseException): pass ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/common/http.py0000664000175000017500000003261100000000000023107 0ustar00zuulzuul00000000000000# 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 hashlib import os import socket import keystoneclient.adapter as keystone_adapter from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import encodeutils import requests import urllib from muranoclient.common import exceptions as exc LOG = logging.getLogger(__name__) USER_AGENT = 'python-muranoclient' CHUNKSIZE = 1024 * 64 # 64kB def get_system_ca_file(): """Return path to system default CA file.""" # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, # Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca ca_path = ['/etc/ssl/certs/ca-certificates.crt', '/etc/pki/tls/certs/ca-bundle.crt', '/etc/ssl/ca-bundle.pem', '/etc/ssl/cert.pem', '/System/Library/OpenSSL/certs/cacert.pem', requests.certs.where()] for ca in ca_path: LOG.debug("Looking for ca file %s", ca) if os.path.exists(ca): LOG.debug("Using ca file %s", ca) return ca LOG.warning("System ca file could not be found.") class HTTPClient(object): def __init__(self, endpoint, **kwargs): self.endpoint = endpoint self.auth_url = kwargs.get('auth_url') self.auth_token = kwargs.get('token') self.username = kwargs.get('username') self.password = kwargs.get('password') self.region_name = kwargs.get('region_name') self.include_pass = kwargs.get('include_pass') self.endpoint_url = endpoint self.cert_file = kwargs.get('cert_file') self.key_file = kwargs.get('key_file') self.timeout = kwargs.get('timeout') self.ssl_connection_params = { 'cacert': kwargs.get('cacert'), 'cert_file': kwargs.get('cert_file'), 'key_file': kwargs.get('key_file'), 'insecure': kwargs.get('insecure'), } self.verify_cert = None if urllib.parse.urlparse(endpoint).scheme == "https": if kwargs.get('insecure'): self.verify_cert = False else: self.verify_cert = kwargs.get('cacert', get_system_ca_file()) def _safe_header(self, name, value): if name in ['X-Auth-Token', 'X-Subject-Token']: # because in python3 byte string handling is ... ug v = value.encode('utf-8') h = hashlib.sha1(v) d = h.hexdigest() return encodeutils.safe_decode(name), "{SHA1}%s" % d else: return (encodeutils.safe_decode(name), encodeutils.safe_decode(value)) def log_curl_request(self, url, method, kwargs): curl = ['curl -i -X %s' % method] for (key, value) in kwargs['headers'].items(): header = '-H \'%s: %s\'' % self._safe_header(key, value) curl.append(header) conn_params_fmt = [ ('key_file', '--key %s'), ('cert_file', '--cert %s'), ('cacert', '--cacert %s'), ] for (key, fmt) in conn_params_fmt: value = self.ssl_connection_params.get(key) if value: curl.append(fmt % value) if self.ssl_connection_params.get('insecure'): curl.append('-k') if 'data' in kwargs: curl.append('-d \'%s\'' % kwargs['data']) curl.append('%s%s' % (self.endpoint, url)) LOG.debug(' '.join(curl)) @staticmethod def log_http_response(resp): status = (resp.raw.version / 10.0, resp.status_code, resp.reason) dump = ['\nHTTP/%.1f %s %s' % status] dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()]) dump.append('') if resp.content: content = resp.content if isinstance(content, bytes): try: content = encodeutils.safe_decode(resp.content) except UnicodeDecodeError: pass else: dump.extend([content, '']) LOG.debug('\n'.join(dump)) def request(self, url, method, log=True, **kwargs): """Send an http request with the specified characteristics. Wrapper around requests.request to handle tasks such as setting headers and error handling. """ _set_data(kwargs) # 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.auth_token: kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) else: kwargs['headers'].update(self.credentials_headers()) if self.auth_url: kwargs['headers'].setdefault('X-Auth-Url', self.auth_url) if self.region_name: kwargs['headers'].setdefault('X-Region-Name', self.region_name) self.log_curl_request(url, method, kwargs) if self.cert_file and self.key_file: kwargs['cert'] = (self.cert_file, self.key_file) if self.verify_cert is not None: kwargs['verify'] = self.verify_cert if self.timeout is not None: kwargs['timeout'] = float(self.timeout) # Allow the option not to follow redirects follow_redirects = kwargs.pop('follow_redirects', True) # Since requests does not follow the RFC when doing redirection to sent # back the same method on a redirect we are simply bypassing it. For # example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says # that we should follow that URL with the same method as before, # requests doesn't follow that and send a GET instead for the method. # Hopefully this could be fixed as they say in a comment in a future # point version i.e.: 3.x # See issue: https://github.com/kennethreitz/requests/issues/1704 allow_redirects = False try: resp = requests.request( method, self.endpoint_url + url, allow_redirects=allow_redirects, **kwargs) except socket.gaierror as e: message = ("Error finding address for %(url)s: %(e)s" % {'url': self.endpoint_url + url, 'e': e}) raise exc.InvalidEndpoint(message=message) except (socket.error, socket.timeout, requests.exceptions.ConnectionError) as e: endpoint = self.endpoint message = ("Error communicating with %(endpoint)s %(e)s" % {'endpoint': endpoint, 'e': e}) raise exc.CommunicationError(message=message) if log: self.log_http_response(resp) if 'X-Auth-Key' not in kwargs['headers'] and \ (resp.status_code == 401 or (resp.status_code == 500 and "(HTTP 401)" in resp.content)): raise exc.HTTPUnauthorized("Authentication failed. Please try" " again.\n%s" % resp.content) elif 400 <= resp.status_code < 600: raise exc.from_response(resp) elif resp.status_code in (301, 302, 305): # Redirected. Reissue the request to the new location, # unless caller specified follow_redirects=False if follow_redirects: location = resp.headers.get('location') path = self.strip_endpoint(location) resp = self.request(path, method, **kwargs) elif resp.status_code == 300: raise exc.from_response(resp) return resp def strip_endpoint(self, location): if location is None: message = "Location not returned with 302" raise exc.InvalidEndpoint(message=message) elif location.startswith(self.endpoint): return location[len(self.endpoint):] else: message = "Prohibited endpoint redirect %s" % location raise exc.InvalidEndpoint(message=message) def credentials_headers(self): creds = {} if self.username: creds['X-Auth-User'] = self.username if self.password: creds['X-Auth-Key'] = self.password return creds def json_request(self, url, method, content_type='application/json', **kwargs): kwargs.setdefault('headers', {}) kwargs['headers'].setdefault('Content-Type', content_type) # Don't set Accept because we aren't always dealing in JSON _set_data(kwargs) if 'data' in kwargs: kwargs['data'] = jsonutils.dumps(kwargs['data']) resp = self.request(url, method, **kwargs) body = resp.content if body and 'application/json' in resp.headers['content-type']: try: body = resp.json() except ValueError: LOG.error('Could not decode response body as JSON') else: body = None return resp, body def json_patch_request(self, url, method='PATCH', **kwargs): content_type = 'application/murano-packages-json-patch' return self.json_request( url, method, content_type=content_type, **kwargs) def head(self, url, **kwargs): return self.json_request(url, "HEAD", **kwargs) def get(self, url, **kwargs): return self.json_request(url, "GET", **kwargs) def post(self, url, **kwargs): return self.json_request(url, "POST", **kwargs) def put(self, url, **kwargs): return self.json_request(url, "PUT", **kwargs) def delete(self, url, **kwargs): return self.request(url, "DELETE", **kwargs) def patch(self, url, **kwargs): return self.json_request(url, "PATCH", **kwargs) class SessionClient(keystone_adapter.Adapter): """Murano specific keystoneclient Adapter. Murano can't use keystoneclient LegacyJsonAdapter, because murano has the check for right content-type for "update" operation which is 'application/murano-packages-json-patch'. So, we need to create our own adapter. """ def request(self, url, method, **kwargs): raise_exc = kwargs.pop('raise_exc', True) _set_data(kwargs) resp = super(SessionClient, self).request(url, method, raise_exc=False, **kwargs) if raise_exc and resp.status_code >= 400: LOG.trace("Error communicating with {url}: {exc}" .format(url=url, exc=exc.from_response(resp))) raise exc.from_response(resp) return resp def json_request(self, url, method, **kwargs): headers = kwargs.setdefault('headers', {}) headers['Content-Type'] = kwargs.pop('content_type', 'application/json') _set_data(kwargs) if 'data' in kwargs: kwargs['data'] = jsonutils.dumps(kwargs['data']) # NOTE(starodubcevna): We need to prove that json field is empty, # or it will be modified by keystone adapter. kwargs['json'] = None resp = self.request(url, method, **kwargs) body = resp.text if body: try: body = jsonutils.loads(body) except ValueError: pass return resp, body def json_patch_request(self, url, method='PATCH', **kwargs): content_type = 'application/murano-packages-json-patch' return self.json_request( url, method, content_type=content_type, **kwargs) def _construct_http_client(*args, **kwargs): session = kwargs.pop('session', None) auth = kwargs.pop('auth', None) endpoint = next(iter(args), None) if session: service_type = kwargs.pop('service_type', None) endpoint_type = kwargs.pop('endpoint_type', None) region_name = kwargs.pop('region_name', None) service_name = kwargs.pop('service_name', None) parameters = { 'endpoint_override': endpoint, 'session': session, 'auth': auth, 'interface': endpoint_type, 'service_type': service_type, 'region_name': region_name, 'service_name': service_name, 'user_agent': 'python-muranoclient', } parameters.update(kwargs) return SessionClient(**parameters) else: return HTTPClient(*args, **kwargs) def _set_data(kwargs): if 'body' in kwargs: if 'data' in kwargs: raise ValueError("Can't provide both 'data' and " "'body' to a request") LOG.warning("Use of 'body' is deprecated; use 'data' instead") kwargs['data'] = kwargs.pop('body') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/common/utils.py0000664000175000017500000006725300000000000023302 0ustar00zuulzuul00000000000000# 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 collections from io import BytesIO import json from muranopkgcheck import manager as check_manager from muranopkgcheck import pkg_loader as check_pkg_loader from muranopkgcheck import validators as check_validators import os import re import shutil import sys import tempfile import textwrap import uuid import warnings import zipfile from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import encodeutils from oslo_utils import uuidutils import prettytable import requests import urllib import yaml import yaql from muranoclient.common import exceptions from muranoclient.i18n import _ try: import yaql.language # noqa from muranoclient.common.yaqlexpression import YaqlExpression except ImportError: # no yaql.language means legacy yaql from muranoclient.common.yaqlexpression_legacy import YaqlExpression LOG = logging.getLogger(__name__) # Decorator for cli-args def arg(*args, **kwargs): def _decorator(func): # Because of the semantics of decorator composition if we just append # to the options list positional options will appear to be backwards. func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) return func return _decorator def json_formatter(js): return jsonutils.dumps(js, indent=2) def text_wrap_formatter(d): return '\n'.join(textwrap.wrap(d or '', 55)) def pretty_choice_list(l): return ', '.join("'%s'" % i for i in l) def print_list(objs, fields, field_labels, formatters=None, sortby=0): if formatters is None: formatters = {} pt = prettytable.PrettyTable([f for f in field_labels], caching=False) pt.align = 'l' for o in objs: row = [] for field in fields: if field in formatters: row.append(formatters[field](o)) else: data = getattr(o, field, None) or '' row.append(data) pt.add_row(row) result = encodeutils.safe_encode(pt.get_string()) result = result.decode() print(result) def print_dict(d, formatters=None): if formatters is None: formatters = {} pt = prettytable.PrettyTable(['Property', 'Value'], caching=False) pt.align = 'l' for field in d.keys(): if field in formatters: pt.add_row([field, formatters[field](d[field])]) else: pt.add_row([field, d[field]]) result = encodeutils.safe_encode(pt.get_string(sortby='Property')) result = result.decode() print(result) def find_resource(manager, name_or_id, *args, **kwargs): """Helper for the _find_* methods.""" # first try to get entity as integer id try: if isinstance(name_or_id, int) or name_or_id.isdigit(): return manager.get(int(name_or_id), *args, **kwargs) except exceptions.NotFound: pass # now try to get entity as uuid try: uuid.UUID(str(name_or_id)) return manager.get(name_or_id, *args, **kwargs) except (ValueError, exceptions.NotFound): pass # finally try to find entity by name try: return manager.find(name=name_or_id) except exceptions.NotFound: msg = "No %s with a name or ID of '%s' exists." % \ (manager.resource_class.__name__.lower(), name_or_id) raise exceptions.CommandError(msg) def string_to_bool(arg): return arg.strip().lower() in ('t', 'true', 'yes', '1') def env(*vars, **kwargs): """Search for the first defined of possibly many env vars Returns the first environment variable defined in vars, or returns the default defined in kwargs. """ for v in vars: value = os.environ.get(v, None) if value: return value return kwargs.get('default', '') def exit(msg=''): if msg: print(encodeutils.safe_encode(msg), file=sys.stderr) sys.exit(1) def getsockopt(self, *args, **kwargs): """Allows us to monkey patch eventlet's GreenSocket A function which allows us to monkey patch eventlet's GreenSocket, adding a required 'getsockopt' method. TODO: (mclaren) we can remove this once the eventlet fix (https://bitbucket.org/eventlet/eventlet/commits/609f230) lands in mainstream packages. NOTE: Already in 0.13, but we can't be sure that all clients that use python-muranoclient also use newest eventlet """ return self.fd.getsockopt(*args, **kwargs) def exception_to_str(exc): try: error = str(exc) except UnicodeError: error = ("Caught '%(exception)s' exception." % {"exception": exc.__class__.__name__}) return encodeutils.safe_encode(error, errors='ignore') class NoCloseProxy(object): """A proxy object, that does nothing on close.""" def __init__(self, obj): self.obj = obj def close(self): return def __getattr__(self, name): return getattr(self.obj, name) class File(object): def __init__(self, name, binary=True): self.name = name self.binary = binary def open(self): mode = 'rb' if self.binary else 'r' if hasattr(self.name, 'read'): # NOTE(kzaitsev) We do not want to close a file object # passed to File wrapper. The caller should be responsible # for closing it return NoCloseProxy(self.name) else: if os.path.isfile(self.name): return open(self.name, mode) if os.path.isdir(self.name): tmp = tempfile.NamedTemporaryFile() archive = zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) for root, dirs, files in os.walk(self.name): for _file in files: destination = os.path.relpath( os.path.join(root, _file), os.path.join(self.name)) archive.write(os.path.join(root, _file), destination) tmp.flush() return open(tmp.name, mode) url = urllib.parse.urlparse(self.name) if url.scheme in ('http', 'https'): resp = requests.get(self.name, stream=True) if not resp.ok: raise ValueError("Got non-ok status({0}) " "while connecting to {1}".format( resp.status_code, self.name)) temp_file = tempfile.NamedTemporaryFile(mode='w+b') for chunk in resp.iter_content(1024 * 1024): temp_file.write(chunk) temp_file.flush() return open(temp_file.name, mode) raise ValueError("Can't open {0}".format(self.name)) def to_url(filename, base_url, version='', path='/', extension=''): if urllib.parse.urlparse(filename).scheme in ('http', 'https'): return filename if not base_url: raise ValueError("No base_url for repository supplied") if '/' in filename or filename in ('.', '..'): raise ValueError("Invalid filename path supplied: {0}".format( filename)) version = '.' + version if version else '' return urllib.parse.urljoin(base_url, path + filename + version + extension) class FileWrapperMixin(object): def __init__(self, file_wrapper): self.file_wrapper = file_wrapper try: self._file = self.file_wrapper.open() except Exception: # NOTE(kzaitsev): We need to have _file available at __del__ time. self._file = None raise def file(self): self._file.seek(0) return self._file def close(self): if self._file and not self._file.closed: self._file.close() def save(self, dst, binary=True): file_name = self.file_wrapper.name if urllib.parse.urlparse(file_name).scheme: file_name = file_name.split('/')[-1] dst = os.path.join(dst, file_name) mode = 'wb' if binary else 'w' with open(dst, mode) as dst_file: self._file.seek(0) shutil.copyfileobj(self._file, dst_file) def __del__(self): self.close() class Package(FileWrapperMixin): """Represents murano package contents.""" @staticmethod def from_file(file_obj): if not isinstance(file_obj, File): file_obj = File(file_obj) pkg = Package(file_obj) errs = pkg.validate() if errs: raise exceptions.HTTPBadRequest(details=errs) return pkg @staticmethod def fromFile(file_obj): warnings.warn("Use from_file function", DeprecationWarning) return Package.from_file(file_obj) @staticmethod def from_location(name, base_url='', version='', url='', path=None): """Open file using one of three possible options If path is supplied search for name file in the path, otherwise if url is supplied - open that url and finally search murano repository for the package. """ if path: pkg_name = os.path.join(path, name) file_name = None for f in [pkg_name, pkg_name + '.zip']: if os.path.exists(f): file_name = f if file_name: return Package.from_file(file_name) LOG.error("Couldn't find file for package {0}, tried {1}".format( name, [pkg_name, pkg_name + '.zip'])) if url: return Package.from_file(url) return Package.from_file(to_url( name, base_url=base_url, version=version, path='apps/', extension='.zip') ) def validate(self): m = check_manager.Manager(self._file, loader=check_pkg_loader.ZipLoader) errors = m.validate( validators=[check_validators.manifest.ManifestValidator], only_errors=True) if errors: fmt = check_manager.PlainTextFormatter().format return 'Invalid Murano package\n{}\n'.format(fmt(errors)) @property def contents(self): """Contents of a package.""" if not hasattr(self, '_contents'): try: self._file.seek(0) self._zip_obj = zipfile.ZipFile( BytesIO(self._file.read())) except Exception as e: LOG.error("Error {0} occurred," " while parsing the package".format(e)) raise return self._zip_obj @property def manifest(self): """Parsed manifest file of a package.""" if not hasattr(self, '_manifest'): try: self._manifest = yaml.safe_load( self.contents.open('manifest.yaml')) except Exception as e: LOG.error("Error {0} occurred, while extracting " "manifest from package".format(e)) raise return self._manifest def images(self): """Returns a list of required image specifications.""" if 'images.lst' not in self.contents.namelist(): return [] try: return yaml.safe_load( self.contents.open('images.lst')).get('Images', []) except Exception: return [] @property def resolvers(self): if not hasattr(self, '_resolvers'): self.classes return self._resolvers @property def classes(self): if not hasattr(self, '_classes'): self._classes = {} self._resolvers = {} for class_name, class_file in ( self.manifest.get('Classes', {}).items()): filename = "Classes/%s" % class_file if filename not in self.contents.namelist(): continue klass_list = yaml.load_all(self.contents.open(filename), DummyYaqlYamlLoader) if not klass_list: raise ValueError('No classes defined in file') resolver = None for klass in klass_list: ns = klass.get('Namespaces') if ns: resolver = NamespaceResolver(ns) name = klass.get('Name') if name and resolver: name = resolver.resolve_name(name) if name == class_name: self._classes[class_name] = klass self._resolvers[class_name] = resolver break return self._classes @property def ui(self): if not hasattr(self, '_ui'): if 'UI/ui.yaml' in self.contents.namelist(): self._ui = self.contents.open('UI/ui.yaml') else: self._ui = None return self._ui @property def logo(self): if not hasattr(self, '_logo'): if 'logo.png' in self.contents.namelist(): self._logo = self.contents.open('logo.png') else: self._logo = None return self._logo def _get_package_order(self, packages_graph): """Sorts packages according to dependencies between them Murano allows cyclic dependencies. It is impossible to do topological sort for graph with cycles, so at first graph condensation should be built. For condensation building Kosaraju's algorithm is used. Packages in strongly connected components can be situated in random order to each other. """ def topological_sort(graph, start_node): order = [] not_seen = set(graph) def dfs(node): not_seen.discard(node) for dep_node in graph[node]: if dep_node in not_seen: dfs(dep_node) order.append(node) dfs(start_node) return order def transpose_graph(graph): transposed = collections.defaultdict(list) for node, deps in graph.items(): for dep in deps: transposed[dep].append(node) return transposed order = topological_sort(packages_graph, self.manifest['FullName']) order.reverse() transposed = transpose_graph(packages_graph) def top_sort_by_components(graph, component_order): result = [] seen = set() def dfs(node): seen.add(node) result.append(node) for dep_node in graph[node]: if dep_node not in seen: dfs(dep_node) for item in component_order: if item not in seen: dfs(item) return reversed(result) return top_sort_by_components(transposed, order) def requirements(self, base_url, path=None, dep_dict=None): """Scans Require section of manifests of all the dependencies. Returns a dict with FQPNs as keys and respective Package objects as values, ordered by topological sort. :param base_url: url of packages location :param path: local path of packages location :param dep_dict: unused. Left for backward compatibility """ unordered_requirements = {} requirements_graph = collections.defaultdict(list) dep_queue = collections.deque([(self.manifest['FullName'], self)]) while dep_queue: dep_name, dep_file = dep_queue.popleft() unordered_requirements[dep_name] = dep_file direct_deps = Package._get_direct_deps(dep_file, base_url, path) for name, file in direct_deps: if name not in unordered_requirements: dep_queue.append((name, file)) requirements_graph[dep_name] = [dep[0] for dep in direct_deps] ordered_reqs_names = self._get_package_order(requirements_graph) ordered_reqs_dict = collections.OrderedDict() for name in ordered_reqs_names: ordered_reqs_dict[name] = unordered_requirements[name] return ordered_reqs_dict @staticmethod def _get_direct_deps(package, base_url, path): result = [] if 'Require' in package.manifest: for dep_name, ver in package.manifest['Require'].items(): try: req_file = Package.from_location( dep_name, version=ver, path=path, base_url=base_url, ) except Exception as e: LOG.error("Error {0} occurred while parsing package {1}, " "required by {2} package".format( e, dep_name, package.manifest['FullName'])) continue result.append((req_file.manifest['FullName'], req_file)) return result def save_image_local(image_spec, base_url, dst): dst = os.path.join(dst, image_spec['Name']) download_url = to_url( image_spec.get("Url", image_spec['Name']), base_url=base_url, path='images/' ) with open(dst, "w") as image_file: response = requests.get(download_url, stream=True) total_length = response.headers.get('content-length') if total_length is None: image_file.write(response.content) else: dl = 0 total_length = int(total_length) for chunk in response.iter_content(1024 * 1024): dl += len(chunk) image_file.write(chunk) done = int(50 * dl / total_length) sys.stdout.write("\r[{0}{1}]". format('=' * done, ' ' * (50 - done))) sys.stdout.flush() sys.stdout.write("\n") image_file.flush() def ensure_images(glance_client, image_specs, base_url, local_path=None, is_package_public=False): """Ensure that images are available Ensure that images from image_specs are available in glance. If not attempts: instructs glance to download the images and sets murano-specific metadata for it. """ def _image_valid(image, keys): for key in keys: if key not in image: LOG.warning("Image specification invalid: " "No {0} key in image ").format(key) return False return True keys = ['Name', 'DiskFormat', 'ContainerFormat', ] installed_images = [] for image_spec in image_specs: if not _image_valid(image_spec, keys): continue filters = { 'name': image_spec["Name"], 'disk_format': image_spec["DiskFormat"], 'container_format': image_spec["ContainerFormat"], } images = glance_client.images.list(filters=filters) try: img = next(images).to_dict() except StopIteration: img = None update_metadata = False if img: LOG.info("Found desired image {0}, id {1}".format( img['name'], img['id'])) # check for murano meta-data if 'murano_image_info' in img.get('properties', {}): LOG.info("Image {0} already has murano meta-data".format( image_spec['Name'])) else: update_metadata = True else: LOG.info("Desired image {0} not found attempting " "to download".format(image_spec['Name'])) update_metadata = True img_file = None if local_path: img_file = os.path.join(local_path, image_spec['Name']) if img_file and not os.path.exists(img_file): LOG.error("Image file {0} does not exist." .format(img_file)) if img_file and os.path.exists(img_file): img = glance_client.images.create( name=image_spec['Name'], container_format=image_spec['ContainerFormat'], disk_format=image_spec['DiskFormat'], data=open(img_file, 'rb'), ) img = img.to_dict() else: download_url = to_url( image_spec.get("Url", image_spec['Name']), base_url=base_url, path='images/', ) LOG.info("Instructing glance to download image {0}".format( image_spec['Name'])) img = glance_client.images.create( name=image_spec["Name"], container_format=image_spec['ContainerFormat'], disk_format=image_spec['DiskFormat'], copy_from=download_url) img = img.to_dict() if is_package_public: try: glance_client.images.update(img['id'], is_public=True) LOG.debug('Success update for image {0}'.format(img['id'])) except Exception as e: LOG.exception(_("Error {0} occurred while setting " "image {1} public").format(e, img['id'])) installed_images.append(img) if update_metadata and 'Meta' in image_spec: LOG.info("Updating image {0} metadata".format( image_spec['Name'])) murano_image_info = jsonutils.dumps(image_spec['Meta']) glance_client.images.update( img['id'], properties={'murano_image_info': murano_image_info}) return installed_images class Bundle(FileWrapperMixin): """Represents murano bundle contents.""" @staticmethod def from_file(file_obj): if not isinstance(file_obj, File): file_obj = File(file_obj, binary=False) return Bundle(file_obj) @staticmethod def fromFile(file_obj): warnings.warn("Use from_file function", DeprecationWarning) return Bundle.from_file(file_obj) def package_specs(self): """Get a generator yielding package specifications Returns a generator yielding package specifications i.e. dicts with 'Name' and 'Version' fields """ self._file.seek(0) bundle = None try: # NOTE(kzaitsev) jsonutils throws a type error here # see bug 1515231 bundle = json.load(self._file) except ValueError: pass if bundle is None: try: bundle = yaml.safe_load(self._file) except yaml.error.YAMLError: pass if bundle is None or 'Packages' not in bundle: raise ValueError("Can't parse bundle contents") for package in bundle['Packages']: if 'Name' not in package: continue yield package def packages(self, base_url='', path=None): """Get a generator yielding Package objects Returns a generator, yielding Package objects for each package found in the bundle. """ for package in self.package_specs(): try: pkg_obj = Package.from_location( package['Name'], version=package.get('Version'), url=package.get('Url'), path=path, base_url=base_url, ) except Exception as e: LOG.error("Error {0} occurred while obtaining " "package {1}".format(e, package['Name'])) continue yield pkg_obj class DummyYaqlYamlLoader(yaml.SafeLoader): """Constructor that treats !yaql as string.""" pass DummyYaqlYamlLoader.add_constructor( u'!yaql', DummyYaqlYamlLoader.yaml_constructors[u'tag:yaml.org,2002:str']) class YaqlYamlLoader(yaml.SafeLoader): pass # workaround for PyYAML bug: http://pyyaml.org/ticket/221 resolvers = {} for k, v in yaml.SafeLoader.yaml_implicit_resolvers.items(): resolvers[k] = v[:] YaqlYamlLoader.yaml_implicit_resolvers = resolvers def yaql_constructor(loader, node): value = loader.construct_scalar(node) return YaqlExpression(value) YaqlYamlLoader.add_constructor(u'!yaql', yaql_constructor) YaqlYamlLoader.add_implicit_resolver(u'!yaql', YaqlExpression, None) def traverse_and_replace(obj, pattern=re.compile(r'^===id(\d+)===$'), replacements=None): """Helper function that traverses object model and substitutes ids. Recursively checks values of objects found in `obj` against `pattern`, and replaces strings that match pattern with uuidutils.generate_uuid(). Keeps track of any replacements already made, i.e. ===id1=== would always be the same, across `obj`. Uses 1st group, found in the `pattern` regexp as unique identifier of a replacement """ if replacements is None: replacements = collections.defaultdict( lambda: uuidutils.generate_uuid(dashed=False)) def _maybe_replace(obj, key, value): """Check and replace value against pattern""" if isinstance(value, str): m = pattern.search(value) if m: if m.group(1) not in replacements: replacements[m.group(1)] = uuidutils.generate_uuid( dashed=False) obj[key] = replacements[m.group(1)] if isinstance(obj, list): for key, value in enumerate(obj): if isinstance(value, (list, dict)): traverse_and_replace(value, pattern, replacements) else: _maybe_replace(obj, key, value) elif isinstance(obj, dict): for key, value in obj.items(): if isinstance(value, (list, dict)): traverse_and_replace(value, pattern, replacements) else: _maybe_replace(obj, key, value) else: _maybe_replace(obj, key, value) class NamespaceResolver(object): """Copied from main murano repo original at murano/dsl/namespace_resolver.py """ def __init__(self, namespaces): self._namespaces = namespaces self._namespaces[''] = '' def resolve_name(self, name, relative=None): if name is None: raise ValueError() if name and name.startswith(':'): return name[1:] if ':' in name: parts = name.split(':') if len(parts) != 2 or not parts[1]: raise NameError('Incorrectly formatted name ' + name) if parts[0] not in self._namespaces: raise KeyError('Unknown namespace prefix ' + parts[0]) return '.'.join((self._namespaces[parts[0]], parts[1])) if not relative and '=' in self._namespaces and '.' not in name: return '.'.join((self._namespaces['='], name)) if relative and '.' not in name: return '.'.join((relative, name)) return name ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/common/yaqlexpression.py0000664000175000017500000000334200000000000025215 0ustar00zuulzuul00000000000000# All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re import yaql from yaql.language import exceptions as yaql_exc def _set_up_yaql(): legacy_engine_options = { 'yaql.limitIterators': 10000, 'yaql.memoryQuota': 1000000 } return yaql.YaqlFactory().create(options=legacy_engine_options) YAQL = _set_up_yaql() class YaqlExpression(object): def __init__(self, expression): self._expression = str(expression) self._parsed_expression = YAQL(self._expression) def expression(self): return self._expression def __repr__(self): return 'YAQL(%s)' % self._expression def __str__(self): return self._expression @staticmethod def match(expr): if not isinstance(expr, str): return False if re.match(r'^[\s\w\d.:]*$', expr): return False try: YAQL(expr) return True except yaql_exc.YaqlGrammarException: return False except yaql_exc.YaqlLexicalException: return False def evaluate(self, data=yaql.utils.NO_VALUE, context=None): return self._parsed_expression.evaluate(data=data, context=context) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/common/yaqlexpression_legacy.py0000664000175000017500000000273400000000000026545 0ustar00zuulzuul00000000000000# All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import re import yaql class YaqlExpression(object): def __init__(self, expression): self._expression = str(expression) self._parsed_expression = yaql.parse(self._expression) def expression(self): return self._expression def __repr__(self): return 'YAQL(%s)' % self._expression def __str__(self): return self._expression @staticmethod def match(expr): if not isinstance(expr, str): return False if re.match(r'^[\s\w\d.:]*$', expr): return False try: yaql.parse(expr) return True except yaql.exceptions.YaqlGrammarException: return False except yaql.exceptions.YaqlLexicalException: return False def evaluate(self, data=None, context=None): return self._parsed_expression.evaluate(data=data, context=context) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5900803 python-muranoclient-2.8.0/muranoclient/data/0000775000175000017500000000000000000000000021174 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/data/heat_logo.png0000664000175000017500000020440000000000000023643 0ustar00zuulzuul00000000000000PNG  IHDRrrgF^sBITOtEXtSoftwareShutterc IDATxis#GG ,uO]kkc;3=GX5*uDRH03p[ucD/ fu:!*"233RRJݮU1/wwBd3PD43U%"f^ Up8Zc_s"BDV3co /o}3c6xADmO;>m߻#U53CD"23i ~w={ׯs]v5YC\}A)}_dYァ> inEUgAaI~fd>芳L`e sELC-d Vzr/EziGskBHP\Z9wEK6zn5xYyJ;Kcfn oP⽏16^ G-Eoj9"JNE6H-zc+ou7@)嚭鬏) c[p']^!Biv)ov V$aiG喟-|\ ++Yy5g1SJcaZ]dylCK>z-]ץT5F+m 6iPBXC\YSli,<4PtxudxkψXۅ2eYT5tۥ.naG[&c߾ ι;M8wZUUD߷UDUG&>|>[y;~Zia&4HMazRyS:ZTe&Bpmsm^>_! Ta-= 7F[G` VǓ^o_XlZ@7nW`rLvSZ֒8 PVq p_|LX}1 Uט b/#,[D֭9O䘽s5\Ji+0mwkԛ{߂F[7nmӗvsuHDMْ->+ҽ.[â6[B|1V])[ιDv4ڂhk\K'}E`~ni-Ao%jۇ|D & 4jڇ Wx fSDאqe$foiȿE[?n}+ldPDRWT`j+4o,ul&_ڊ-AD !UKe"k(ԾrFB_syCAX6-Bh}ڼ{+]-blzŎ@>SrmZ0I} K),*2D案_r߮m/l-}DploVvyhEۻ]um/gJ)-um*Cy24eQ#tb9e%v:bno}VYP[Vʹ15|67XS?ya]5Dޫ!  QW]ܶϑ?W ^x3fhk ސ"JRk=rSUm}s}Wdu$.HD) ҚW2fhMZsYFU?x! {tsŴ:$0[+\ {U[Hhy9M[o[p'[brykͰVUjfUԲ(NG?_85O  rX) V>g/!:8[wZsYѝ`嶾}]xjڲ8Ve-*fcV@UMӘSy|>>8eB9ljZ bRj7 :VA0sVsVnm5V߶ZASEշ</L3m"֜S:|_܏ ba3ITjR@C! ^h] *3}{&|O{۶m5k[Ok$ADsn-ȶ.asj*RLK 5ɕYJDϞ@ @ւV7=u.vZ`9}J~k+Mn[ SZ|)eQ+ESUum6|Gme4MPQS1"IS4q6kEn7l5sź`f hgڂh^{FE=-,RJND.. V~n/>V{ѣ1V>a JZRe洸eЪ2r 쨋; ϵ5HM'ZLF=k1u Vn5YlK?֌îhQIRMDKKqH nq@r1 H,!`l>H<،T5^S+7XkkzX.fVXQ0m?)UR4gK R:t >nPؓ91 _r>*ҷR?Ces, -WcMIiVj? >os  ֦L` ""Ҍytq֔%[wٳ5?mB. jjU+3q)*r+2;/QǾV؞aTU TUk,M4Oz:2TTEÀ>:΃  f.e[||MOއv{+[bwdN#`LL@U WIs2|XO8dj CI >·ܹ4P a o{aŗv{loCwؗN./YiiP\$dyeeLcP葉DH =L=7<\\ Ke. Vn뼓VvZVnWV0#5 5kYd9R˰~8 agEtr%XW[Q;v{qλu6|jly;^ֱAo+? o+֪|a ?ZQLQL"j-&EQƉѦQ\!:uKFPDm"086RK!~$A!he]4r;Y`VVZv;ZE#ZBRrs]&Y&GsJ11B9h?~8,{?H͵f!z=AGDd]co.*³1B]9;ގttIi?4emq9iJL;wC z;dPS9s\m}O2'Ur*$Iw+f+$Ŷ-SkE6~l[Op[)ں 6RJNAibLDmK%&ɞ8R)օԹڅn_C([pUPI%! K,5ϸLs@K_!R1q`s7XAV6]DBiah^Ms=7me#"#ddJZTT甋2OKkzן_g͐s=>q?c޻H1Ypta}^#v{"YbRRLM3mZ"-K%ԭ9](%.[:A?wNoۙrXVa6*´2U.97b%k)c,qqgfvh)']N4YjeՇtw@A&gfvjRհK ZTU%XB7jTkmn"ROWUUZ!Qm/1~w?vrmdj%Uۏ>˹yR9zxq*Oe)UD@{zwCT 2333TeqCUKZ98^|cʋٵ",[wZ5ZGm^lxwnAЏ>.ssK ;j ٙ(X)sg'?O0NxL{9&j.3ڬZߙ QKT&ߐNOJ3s.Gg>z?`"3{+K)1cn _oMY*Lu<ӑ{|,J,FG/w4sثUS9AByeT$2%1R0-f,j֒Qx@sឿۺǔR$¿koQ)0 !6+>l|4U%YҶxVjU544'{z'{HOnVv>ҾN8ي*21sNS”ZKMTLP! 3پzV/޷(z{ZUVY; Y^YVC0&FdE6SߛU"z(C FFg1dieOGO~ .@gbuu lRsľ3-!:11/Tj1&e;gS.V']VV\z?G}>sޞ6mmgHUUeYP9~QU&T~ӱs7Ӝ)oÑ̇]꼯nfUz=He JE$~_r<7 K"ܟžWu-^&Z<˲4I~۪~!Blmۦ%+۱sf6+o bV e{^z^jbڲ[ EJjjjjՒ4OGF&Ox<#sý}b=Ǟ}DD13;gL:W!q KhVI a>%sk-wܯm-gvQWUmN[O+c1NlFHC!J5U<xN'{zbƘenw8tGA"@D`f bC%ђ*EsҴԴH*3P5D5ϨK%eߚPaƋ_I}q. oig@ɹm”dZ@9t[Zk V۶۱d a3y5[UYHjӀTJyO(?8˻]!wa7;3ڼ;#uIKR!'Es1πuYK}%yq](`5͠bJ~m 2[!ζioZ[\;mbkn /PEȵ.e|OGx|,yN9֡<<,}8pǾ# 3Iw5[ҟ&%}۪_V8ー؎ֻkauT+si-^زhVA Dƅv}w>vsTSDrH!8bfHjSXJRZAӉ `h``7n}XDˆ-{E[MYs%`-BPoY+"&xs{j&zb{]L1oHEN#"kONž  %cwx ?=3^G 1!a k%)2g Br-shP3"bqav5<}p5Iu(!&@$ HD`e+^ܟҜHfj*,11RkB٭EExzh4}3L| ]/:/ K)ifKKyz)N;4ђTԢ]_vyḰ))V%-&lH՘DbjBj PsLqrˌ<2K] EՓ``؜}͎msPzḫM~0_ O (+slEM]?M@$|-]m.BaT IDATbZj-BP,fyw@]8P׻؀Ξ.DvU5cޫ8 yPCY2. R\Yk*%UU# "y44 ¹H:ڷE^` Z][ s}}1敷8ZŶ;i `(Z"IKǑ'8$bĴJK hs] D&D##3`gJL1T F]0,br^橦e #`&d`*A$dVpM9_Un7kuz%b(׿-wQ+ZgmqbfW kIK2m<)x9tA({z 7r+&C1Ud!$50q1خ+ "DS)I])ރq02 @:?Fҳ=0j.ĺk<Ǐ#v|M@ ^ez\$ V[a6]jIiu>Ƽ 2L#O" eߕqwBO!oEوL >fLN`}!U @yW;Yj ̤䒗y$'wxԟ;9 >tC/C옉=e-ЁĘC'`(!v s@h&ЈмQR gtơ:/'-dU=,c&gFY$"J)"0)ȚڄgGյZD6j _n}m4qo^Sޚ䕵^Ҝ_tz TH^r-Y#G> Wj;xx(tqÞw}yYֆWD&_)sA) s jEdh)IN5Vc5j9dhd&6-b;VL__'{7^ao/M<~3k*Uњke9<4>>H*Y^z_v)\q.l B !0{ދ:GmS)ًuR;Y&I$#6_W*Z>@[a"Z?= jsnWu1x֏\Tb:TӨɏGq扑ԡv~ Cƽ|Y= õ0GFC@yoF`HȀ,JUN>hGs]Y ,i6jL׎m}qd [TkXV^A녏EȪhÔRJNKZ2`>xCxlfE- ;7܇0xhg\cvN9D%6vbЌ)CVŏs>#'"D9R3k%g`g;%t>PJ\s KRƉ%<_vvjjK,"͗듗}~r-\OYCTRSNlˌiiÉsu\u~(}_.;wjFp UR&0m~#[$>w G޶o6-Jȉ'LUYj4t,izz 9;~n!t};@Rֱ@ y:"2S2UQ3n9R+], R)>e;vޑwQ̪C!RSiiP|npÝIZp,DK/ ^ZDLTDJ9/Lʹ2%dgkk4W +[c)5wzs!!(( jMYO|zrc=b-Iknw]~pw!:l_١SB9B`S Tg}zݎFwgMsK)}g=ypm{^PXZQkTcfir jي @VXZ#/#G:|x|\B1ۇ00yDj Gu߉azw BN xSmػn]9쀩&Xn{{BM]TrɦYh@LZ˒Bir.22sZYe7;^ Cm2V,cN4_倿>@-aOoc-]ww1vEpv`d0F8xf*~10}luj.Z bZ2߭-ʵr.,D續umhi>p^0nyn'}ݻuq߇q`fdߜlCfc'w7:7BC8&:8ቖ)ϧ̒ŴuW0PVyVwb*UKii)iyiӄ E!nc]{ak7gsק ;<}hqؑ j`0 |`8O0#E22ʲh&T.~ S[|[Ii5cUTma:xǓ?ܸ$&짇lw_\a}`fj`9/xn_Mt5ylMD h.J`kp ,Fv;xset4ղh&" gsu+AP$#2j햷Jc榺7sJ"ʥ|%t,$.0bEӔ`<ӓaZDA]8+G*"n MM;Cc=G.wf#D"GD06%θgBu|ˌKW*}o9Z)?>AO<\[L̨AݹoTAOJz{%EY4'_jڪJYj$`q:@O4N(du2t5F:ڬz C&CRCSQON w"`ީ  wGP ?aq\d=ŧ'z|qR0eM}n 9El3h;mb  ټ3{ໞ{Oj\` D@Lm*\G8i N9-'@D$P HUiuش֟1XB׶ZFYDJ*i(H蟎4(Uevw'wnui(0Ra/I{b1!VŅu}e*PSTA!:D=#MS̙9ϧ42K);4]| 퓇m۞gw[hs T\2t@OSgՑ.~/wej't;!r%FlQU7@Fd3;r>T%Fza.AMu'+U̳ u@K ~adHDw]2= {~.Ҕ\yK!mZsGPG:R)9ss)11JlffHڼo~cmlv ^Nk1V-Ys40z,̞\hlEbYS)BbL)aw[͵q$SO33dVf\0rU;v: B%&`-AtMt9tp?ol!$}EY򙣜yty3 Osӣ^qtk7<8_C)VM>nҷVX6^_8!b &@F47'prr039uOUbYӢ!͘Z Xýu$9ا=ZW*è[|y%}9L|-!8g9n59u&c^iBtY˧$]֑2UKH[$"$fw'~sĬ ! &rFjn@3mAH"[-04,B(;y}ԃS֔ A'Dܳ _Y?+nƮdJh(nUm|ov[~5^oЊǛdי.}8 ve@hMC`D0'rb`w$70w@Wc NlH[DH_f&,#7kJ9%(IUH6z2 TN"wG7YNof,EQK0 &DEVq ?ЗLlsF ʸi9fEb Ff0|Os +غh SZ!9 MݘߥEفwIsGgm'Ήm\DbИ@R%u>˴p;_Tspxav{)z:4fts'_g <6OoZRpߥzakQxXl=,9S(縇#}?;~?|ϭC6S ba;,;a:b{9\_쾏|3Y(݁Pkd= +Md  IDATTU]wmUkcFon;ՎDחc[.KX6K Q"`q~`#46bDԑc{̶GYt$Qc}Ljh$Mb#eFGn8vfcWstz3 9NS hCՎR/G~M%Y,ˌ—W[.I-%EUn6o˘>!^VXS!93Sw`gaDPs 16 &1Cx 䄂0[(bc,}$]_]Z>nx4^%31"0 x =% ~z:ɪhgvP~}8nljLrr6+6[HQDTkۯڎ⣻;d ƮaYU‡)\);S(&X) V*9Nfemie^(*#[l.<5"OP +⁈@hXqwwUs^pF;&]2ǿ߶&ˋ-CDHr&ioovڏՎ۬rO0e”DPQ)~<2$̓C̼eeϟ71߱\җanF\Y^ǩ C6_#F_Fʜ2F! ?U= +2u"d0G;nZwxvͥc'Sa,9vBOk}o?o?7C/_#.#opg$$c֘-g\fND,4w8JJё191>8 ZveǭeӜG(G.O4yʿh;NٵumEbfGطY/.lļŸEPOk߯z-}wMD!-yEj ?in "!J=$b!` 1FVeQNmRt&gfZ"^ѱC[ks3{V@OXW$${"zT+{߾сp Kl}[c!02ZuJkw aחٛ?/DtJ2P"1òPN5=狠4GtI^c_>]!Y.ڏYwGzeb>I''XQ#vnߐZ. oXP_Th D43D)|Ǽ/*:(x7m^[j\Ԃ``f <\`Pv|d3~C1h9E3=PN-.#wu{Gl7mSu:7,2nvC5 m\ ek^^RH,Q$ 84>zQv8:*7/O>5FY$ ('V*_ S$,"1b!%y`mΉc^is9i1%07.-NRkQiԽ}oJ~ 0sUS9C[z`+\ψҦitep\bBLOh3fs4kk c$ Ĕh4E1Q XN_z<BHJ1v%O ܪtn1Y=F|yD5{nzLm=?"SOX}8 ݦ1[V5){:X'AEKIKy|W (gn>G٭rv{ n#n_֙y %90gxřYD88PT|0tC4$[ױle׋+CrǷ=W.{/שt\sF+dVki>^F Vlt+;[ho0:EėKKy&˅J1DFAThήVԎv {hÙSmKn9͔ /W)?pDD!aʍx~}oWi.ǺB^yYòRp,f>Zmco{J&k2E-mWDV䗢^$( BYj9eMۦb)9Lz ٜ.q6`B'/n+|>Km " xw2@w(ܱksjd[z[ѷM")$$b>$9]ͦ~p/|zщh/ 乬6^^²JH_=wx q9s^=)q.r#B {(%)c)I,m]-"b?~ cWhU{Qu`Yӻ m秵V~{TœػnK)rŽp9X8U_6^z̘_$)!"HHULmvu6[ޱ* @\7]6kHk HdfD'_d+[qs,8!0 1zŖbtJKnh$2eX"m0 [؏J.JVWmfwg@suwpuW _Z,N""`$;K̘+.۬8{ҨoU{˖8ΐ= ﳯ)B;v.;]:A$B@SB7sDDB@7.6?Rff>:jRع5xse_yÐ%.)!$@DBD HD :lGZvHm",.[ 2s8,1rX>>SHC(W2$D&yuF01T?Gq# Bu-)m ^@Tjg=:@WtC7BgFg<ё9imF)ڪBuo73 H\`,y qIqaM0!8%68wכ؁Dº˦9zHRC,B?IT?a"2K A"I@ d!esfo j1Q'N>p"Kʐ"nu(Q,ƀq"\ \ ("Wx.O0>١W=ABmeK5Aa:슠QuV~QoMbP1% g {{2.#Qye³ b!i'ƽnNu Li%~[뫵6S@f1F_>p[ʳ2}t@d0ps70D>E3[` :98@:ەm~s_|T}_ m2~Bq]&e't ֝G9yk?h#2g qe8T7QМN%_808&r3$'C !Ĭ$tTធ_.A>]Ovdz.. /u}-//K_ 6z ;}c۷9{ǘmp$F @ Oɕ':.ӹ& q>3uchSGoCA+qx*h9:eĤ)B\9."9CTw9Pv8}idYC@L1bJ"J@S/2![h9Mgwhjֹ56sCĸrrD>ebJx1Z]U݌ m'_P{nesڥ7;M/_]HbyʺƼpH$锿536{xkPzSUcĔ|͖)pʧL@ؒ1C!P mXP}h6I1eu'zY#ҿI*eVvf%g xr+0?ܵ?_iCpz6՝[]84v'"%e[\2^9 ƘubPxT:v8 9ٖg3Os%:1~)?( +!Cr 9Q|LS:N˘BX- %yŶE@UtÍ~Q(f+lz  $^xf+T3G~.̺;~)g{R8~P)4i =Ǿc]SΎ20icx/*#Jx`[\R[Hr12#?d 0ǥ*?E$D b!̜5GL,)q)қoK[Ȗݕj]z-w9p쳖1zש9}oT'':lfUCFȗmnKɩŖ  ;KLhv~9fPoTK(NISS]ZJ#% ƅb$ gRp:HB0sl9ZNȄr]o` %dΫUK +@ c~eO^Wmή:Ny1{n0?aW'?>0hVwU߮[sOևxmLkI8~(ns6Gz`9bmP*vAYe,Y *q9Y%@W·ńiє{pMy*"ѽ9),ȫs_V}}I&rxʱVЪc6i:tA:iOn/T6USGִ5+wn;oT .歅i|DI2 09jYaTnEZJzre.y!$ %2 ! E?2x3Ր0%Gv,gBrC0즓 ,d]6[yy!SamIdfb:"s aN%|-gj:#!}nsz3 nFV1ڧtګjRm*1D\ciBE":`O1l}8s4u)#كXXZܨL%2G2u˦)QzjuסT /$73[_#cfju6*έqΥB\ː!SHG '^O_ul4.֤H)hL#D bfIXb@480#1͜GJ=%Y:P'21625da7k;m+\+cm]yg;0á<_<* U7tPZ,E;n`역iR\c\RHI# ?Oe9C[֤7ijRI'"N".q rn- Yè=!8!p iy3]Dgq5k{/>kC5A (dZ6Hy\^ܖ!,xT] 7-bՏꙒ~fj:7 [ZQvc9|˪c!CX$1-!SYԍ9ҵ6[8;!mQV ;)D )E81?/}{# &_^zQGR ˬu2/m{yyga8pzkS|`ɣ?3'zA G`TnmiCᴁ>5eB1K\)&r>r 5 Suؘ:;N}Hػ0"FN*8d, _2~@DbD1$jb=ڌ>ڬ;)$żq^5]ulާ*E#nl|~H= 'zH@9sGvsѭ Zq/:;&m:cpZ%.3+cl5yW IDAT{0ƨ)ƀ!S"A( q"" $"9C9 sm=KްW}s Cf^f^4/3^v,j^fo\rw0{N=aWqAClN=]JR(W*HK5[̘2s ؙ\O$k}sZV}"bZ8X rI8 nAN8U"3א9ՠOC⣢}#KvIh9 bKֹuoz6Ou3|bV~) 4 6GzP-X Ƒ Q%x#KF @t.9[ fѸU s`}YgL# b "IpV3WDX0$at[#&lJmԦPI":Su 8AǍ[^wf6Qӳ_A;apitxk ]jg +l-R "q%1l1lOSeUgmsozPq~Xk49'N+HHH!Z f@<.vMGX?̧z>&sz보s@`"&bB$"t8BB|8~> S4.۪!LdSm\csvi`g)fiFA`K\ƭsm::mė'ᆲ s4ukSJ:h4) S!k2ρ3Uw3ӧ8 ILV (@GԂs嘈řC%Y^f^,/nxX:*u`ڏ>ZT`& bZ5X7 ɥQ5n^uT7s5g|஧(>ۘGX0m%GW $D?*GUtGw5y<'ot@srpPw5j2{18dr yi!hH ޵IncI L?7=Ȯj;;W(d-tDxxGfENaf  +橫*uc ;Z[;cj4q4E1xppE)-Ҹi# :a.\B Ϻl-Pc=<|2Hy[ݐDVԧڦڴN<=qaO+ezFx'7.ְ,&AiLYKp)X:$rkپa (,Eu:Ӽw-8hO. Ks{kBH>Ͼ.,e2VzuD[ɝ>/V5V&\^A潏e.GEfb4BRHBFL'NDY֊Egw )i2e\!ZEvLؿO TIBsB\:CND KseQzfZL/z|=g_{DKrdiR|j j8H;Sk־.f->`o)ͼ@z絕u˺r |k.^Ԫ|7tJ'7m\Y,\.eD)tyZPf-eR.ʲ5z"("=ew6c3FwX ,%u U+X5!t祔b79]C)6v%AD\RRKӮҹ<|B[&i "HN1b"T]ZMɺw H;ݭe\F\6c&L.IYTE02LYEdd"GX3NnRh*@hG2y׎:glAs&S"12ɊVjVէڔ;čn}DʤEsyI=H:Ú{~7o7.1/(l]7_6C\পrZP*EdI1#Fc?zF8R(S)#w6oS 0q="*ZHKz-9)UM76g3r#73,beB:XV*OsmE}ݽj?X)W-<!)=̚y3_mr%'tW`4c{L$N\w:͙$Dhmp772CND)U 0YC|I}Jw:᫊¤2 PsvY+:]iXWCLǕ%IQw) 3 羪u-#,ˣ8XZ~ZRҼxRXu]>je\Ѭw:4_zx2T}yTe2r$yD$-;y8B5SPU&):`w'4P( IQgZDwf:E )AD0 ecR+O{"b4%3 b&>PLK]Ft=\OsOՂZ[o)("A;O{ʛv5lfaW^eYYf-^ԴZ$k!-Qfr+ Ndp˰-{G&Eu4c3p koEtp ts$0TDZCeR93;znjmd-RK Ju& ajf9fs;bq:K|D{Y[ӻK[/-n-ATRQR ~nnCCl &U]7MY!o1ϡdRj( N5Xztz=oKSDNR$ïQ!x3z~E?a҈15n ݽI\+M՚( 7lVNK7g|"$锈d"p6(uʦ&ܘ3"߅DEf ղѨ<{,aZ0k%QZdJD\)fd=څ†8Q8%7M2 ]>eXCu6Gk.d̄(O%"%KAQVҹ{_e-LXZj$9d̶`$KS2X俵Jʯ]; l(J\j)Y˙mѯp\JJ PZI :1;2JZBKZQpxƲm3fOMrO:q<-QfdfEXZsqW2\ "e5n@TD[BOZanP${FMܥ7 &"EPR&0Qrrn W q7ILN0cߑ"q[N $!$3D])7dR<Kflsh 3TihqcRH$q‡ʖi2?|L{ƣ}%(*\p@r)$&3)2(,'9I8LIB@ \@`k-rtevϊ 0(#Ҝznc >VyXDH5X\DpQ"t0xK, tܔNNĘnt?*GM) |+􀻌#̨,Pތ7&2ܴp-ˆcS%ޝ/緙yf t,,ĽdJ0FPa")@*01 1LC9>|?cF;GHDEB8!$Hq3CL[XC2@yFKWAȷf G71,`BTCKG#| A#v~ !")>")=zwQpNa3,)) )Cia-FD<WizeP Ӗ!  {kv 0m0+9C]aBz(#o{peAzrxwFJQ<`VqN#C|l %IY**-W0SwwG6Œ>kx I2~@ff%T33")"o)t o.ߚ l Q:Q֨x`Sg:!8G4"'E8p5KF` Um JF\zLhrn\@l`J&.m;2y`;:7XHMP$(:31%@H =R Dݺ㕑OI~Wm<o!]ÛV6yEN[2)3+b؄poJ((~}n ^*˽h2ڼwxA/,bP*B 6cE6,qNJG \b..s 6%R# P*B Xr%| ?1 Pp$K5{L•v dSJ]]fpkeQvR 7^Dc9r*'S#"uL IDAT|%*ͨy!* *U)%PT7$yXi^|8gT *R8hy"O0l6 <8G yYz/&<ǠX 'IF18@ۻ(WMb; [$X %]Gw1x1ܥOy$2dWn\ R$V17Þ[Fh3x'RfQ Ǐt[úe8BL.ZGbx@g]ƋRa􀕯)B7d]x }-|bM\soGV8I,@T!Lt%nbp #˃|Y2<`.K´^T]dJ!-Y,K, 0&0gU2RT1h5ļ$ Qok~plAL@:0mZp *l|aJ2KE]RGodF-܇'æ}·5ol5'~LM[2ĵ,*ܫXQ2G7ޭ_]кc1E֊R04,% Oz2x_/7k{gR["&۰d#2<\hؾnd1(Z,l&Sa=h|||r/Wզj  #IVkrCpN0VfbDzXX˾r޵um&QsNs:V.6H(ADpDo2y1jR s'pq8Fح@]囱wTH.YJZQfgewXg3qc3 3bV }6|ξi{xXn&JﴮC2(DךRX CGʇnVYW].|Ÿ9 Tϵk "Vb[0m>G|H&#p?(LČ)|*[f:9>N:`ET&Co.jZo3Z֣s8W `d@_}1AtJ0of-zֹ5 n`yu fe(wk|W6R4R] ʬe'RRD "7]?o%NNI;xx9'En)o/o7_}t RLŵD7nSqxGbS޾\t:gLO}UN[3JZ #),lvYr:HsH 1ԉu:z_B7?똒eI`vr-$tE}g3"3ƫ“T5)-3 T'+." c7KTKzo[0+?[N#Ԉj֬kIoX;=0Q*0pHs'o -r>K?rLzz^ET.3Ke)i3僧* z_rWȵbҒHB@tiYgxʇ_ySC5).礚)ݪ4˾x-#"~]coJ"`H2;z:d]iY7,1UŹP)؈VLۙWYt*G>]hmRq89=M,2 VNZPJR3i/XJ36Kh-~'#m0R ]TRKd&Y ֆֹ-6_ͽ'yFdĵ ⑂ps[LOa-v>녗+ԕS'~ʌ}ȧc9Wx%2vr>O64e>42 %~r-M}xw3l̈́BZHB1h7a:բs+X )"Q|lVPp%EF"<ÜDzpdekVkjv{2f)FM$ -%"@d,.Na*#CWbi:yfȷ@^K" Z풶rETDP-9Q"NDdVi.t |e7]DNЙHFW#gʟx~m-$4TP&e.YO8O*.Sbz^ȭۺ* R7d3KP KAX$"k)Z ;,$;ZT0/yx׃Q;{[&h2= HIՋ9` IRnǶmIjqy-ryjW.??u$GD) \΅< .?>,?|TcNZ窥BY{n%1myf2rzU )XuJ!-Tg|h?wE'\tT35ڲu|ZnERPjjZ{<4;Kų(Jo{Xgj;"Ї/Ak^vKB\>(תQH^tiƴKL[Fg[p~-c="E)ylѴeWtQTף F[jUTTiRyaN|@gb&nr@P\U$'J}Z'%dND~9R{&e__-f㽹i?<1Խy Ch-5OG^-O8^*x]2q"(Jl}K[ʧ})KdH;yzSN{/;|꾔\3HVȒH(\X'ѹDey j_γ/YݖX0oI̼e$! "(r݅V糳gf\^/^zX9tfbq^:73JB#F뒗3Ϻ8tByB̻jP E[i]qx'_46|a:Pu>:W(AVߞn537&sj:Ӽi:~~'ǣ ^.t)SoK%( b84d0NS}u7)~Dܜ[ӫ.'mKoK:IR6-Cۛ W}vEn,v>z#3xQ4M#Dyw^OoӉAgÇuXd=ϘR,|-spѲyoiצʻ=yΞS'=rҖY2le˳ QZr6OtPP 9r[:Ys[X+Ձ@bJLˈۂuRKf(~xi*&  rzݽ~*?RdK~/vm7I_EY$Rpx<Xn,ѰHUx:2lӞWW/;帶ږIRHƘVZ]֩sg2Hxr^t]Жn֛6ֈp#uOpT{_ۺrI.rOd]*uҘHFYϧx,?BKoP*c/şL^뤪<3[|C>hp$9E `g{~Akuz?,t\ΧXq]~L)Rt #c_DT+%ThQR2t4];wK_[nH,׽eI-2(#¢u֖u9-SY "ثU=-5DuU?=gӇ?N??SuZXgSCGл-MLY^[YK@7rQ8?JmR0% 1 &NIѨEˤl>mLk'β&@AiLѩLC'"}BD!@G65%/<|Tg_8}4OC#Ӱ"x*Y*?:M;>LXp6""Z|}ჯ é3)OC2Y'eE2p2-p>D5A\r=ףr:ӱy:k[EzMS797S1>ج3T7aCpxQAd#Ĉtt>]P(qwD\tiM7{G;i:tjucvՆ (: #LTA-l3̻g߷| OLD9U^0y9kF[rr"6lZwPPFXm+8^pNO,:TSdAyKnBq=`{i7XPeD`2:ɼ! /ODz:]p:N9_tzj22%4Y VQ4^JkAPE=P 9wRI &\-g+n.rOӺr*}<paܕDѧNH9z_zYӌ)3oAUO$V*quw=DZH Gy aF=~q 1W5UM&@ ASLH"pS tWң%#fEiH"Cݮo(e};<:zI&$)vЎk mWն\aE8T!,vvCf3d(KC %dV,&8&>"sf|u_10w֓9Pd&N;v/}Wu_ UXO\kWGUcA#@1b!GU✲$< 1!1VaZzw VlGQ ^SS'=ִY:lZ[JA`8ڶbw0 s-d2vf͌&3?=Uc!)8ݩ~Jq㉸=o Pޢ0sVEH4 R Ħ Y\?P? &F3 הˆ̬3! 3:3Dq^"v~0*8?&g qa@` j"R~C/onna.]eC8D*.XݫMKaVrdR|V[_CD3 uz@""`fBfF5&YYE!{@qou4;:r;D0u۸튮_jC&0St-1ë]ޖ КƖ38tMdg.ƺ!:8 ^;hϷmVPȼ*ќ04B#34@04SU":r8hו:6vk۽WQ+: @QHb:,J`԰DΗy9ͦ0=M[85N Fr*`?y@Mz$#W6I IáOQ;趘/jpMHܶ'XsKՙڀ@ m>*c0>I#ۿ`m~;R4 ":VS.]Ư:^_͍@*ޚYQn5:UL0Ja 94 = @l*`%ʹ0)XeSCE)t@R ;ҡq: @U JC][ nn+|qoUd).j!PC RV] w6x"gLg6YMob3Q7|?`v0{~D*NVANv$WWj$y΅bH=hQ0'(8  Lopl_ğaAQkfE3He(NWUnv/50̦" "7}eCl n֓,xzNӥf\UU[34?@O V~K❥w7Qb+@@M ?:CE_P:]Ml˭Jzc={ѣhCEL| IDAT<D31 k$:.SP<:%Y 66utV[W+&Ҟ=jUCtVrAO]&gSk4=硝0R$p|?i9NH'L9o5# t@.M[I!aqFEr\{la_r 9(CV=9fbC$D#"DG ?&*d1 23C$C#3CK/%IQ}٬_+Y>G3&q2aZ dz6<$uX%]{Em dDv|Ir1 W'BtAZ`A϶κe )O]HGGn5Sqq:!o)[1;cC2D0PU%羓ax uu onpwCSհo[0}[<rtVs{Hgd~.m~'qvgU4T;YޝApIIm$4$f$4$Y\BT`&C֢hRZ ٣V8q1~*0?w YӢilW^ݦwq 7k\\G h>U`6EX&Ntq&dZ//|zb였m{uzmf0 z ZqE0xI m(n=RAJRPIɔrg1; LGyz((qw@5=wyuXݾެ _=Q.x02lKNjCB/qjg,dꦊG"$3#uT{hܞ V~Fq,K#{B%h 0jr`&M9S@]oI2] -XM\!0#⽕]GV XK)lkY[/ M>2C7zF~.dr|Ӧԍ̛Y8!8@0ڽb~i`7zp?sQO1Sv`5vqz(cal[]E4 yH^8k(dquQWeoޘq82+u^[|pObx(,.m6n ۵\-vO`{L:YzMpVE=WVQ"Phc&SNrX͵.oVכpke&f&C( \ K] ;[LZ:{ՙNn6os?2D1H!ShFx{;zL1wf!јԿ-93P24DTcH#Jd*\p-p(9,@!|7x>ug~9dJRRC7̆2XhcA5CEff3A "*Dw8Uu)V臞A)?J>lusVEV[zy[ L-6@٬hFF@ La;x=: {i2ɬH2MYT،""5 #H*3P4L& Ґ]t]ݬfe owA٫9153dgٹTä)Iy(Mt6Lge:OnzVMθTuB3{<3F`?:қD|xn6HsEtܴB@qS1,tbJp5TD+^q|/9tz^Ґ$ђ$Kx)bUGd1pC DDfVDEw2tl}+p+}rJȢ]_aF< mr>m=IyntIjg.>T3KZoTCe0rUabע >u_ +HD`l6pa !r\&1F.q%Pd :uhѹ.um*I0 2OlXĈˊDdf]+fIEDJN}W[~vqruū&WA-& q\/pǺ.2Ld^7XMy±fx|?gvჄoH@H#GՉ @fՅĦ^ɫ!~%sʸP!@Sif %(j! 숑L__?$"2N)z%:&?&lV)9L.2 e_t7t4O0]t"LǦ B`"Bq$=o+LC%<ءvZu Fx!0 !"'.U}ebn [x~ςi`]尗Rh\ 9U%Rf q~?e)I!u-5VzܭnETCEX,Ά45N4Yo"TH) c">\}gpn60C2$ 3p %t9qAK)SrYqjʛA= ̂]v]S2(v hOV;`Bw`!#"""? ۝t'7n}+Ͽ\Ny0>/gӏԴiT' 7[Roz|9H ?26=ȃStъGNЉ{"3"28 T̈Dd]fT;#@@*fy)51^%acǕWj/7kh"l]_^;<2mw U`逩w]YJ0жۍͶ0ΰni1NB;c_xx&kXww`'{ V~hT YEr"LTd!:C#02+cDo-n6t2“Ȉ% TR!ZB@qW!4q>lVy>ًvF f:Z< n09vzuYJ'ٯ].4?[:HXbU0x}EqT -?b#ݫW₄0MO2C۶\,i6:ϖ7-7o[ y\ "N3C|`z0Sp('XE TDTd:nnV޹ŮcuC18!TmN{|'`qRW苧\G|.hҫ\}ӹz\=2B1)۱!\OE,ƩJw4 fLxzO+y5`!84GdUhE`WVTՀR")nHY餉I3 k7XŰw6> LЩ eOHg9\Ggwwe@ &ssU5;My"G؎~uaOCdJb\J)d 3&0Y#h::bݠ{#;M @a%oZLAd9'X39X0`f]׭Vz>tݤHUU(0<6}CgQnGc5 )c6  㒪PsЛT-!l2REChE%BRrU7$SWO];\d=-A.N֒Q<Г+d̈4 9?rÐ9\@9`~x E16וfk,:V]`Df$ zR䑃Um)%r U1 5$D3'ӇZ8U>DՇʇQqn\v⫖{SM <8ѠZ?}Ff9!b˗W}ߗ,9D|9u`D0 93=*bz:ϵ_Uf&`ffj{4NrsMjDz4D "c3Jɒ+m·c 64"2qhCo[!PCU!1331׽;z7*磚ݙc">{vO"DO^MUc N+Op;jTaÛ:X1Ө" ƅȯe^zLD@\`B9g3EC`f}dݭ_k^ H9~fc1#iTĜ+4;#vrweBs‘d3sNƘj-vAHDUU#&OGp.tyjo;1^~;whr߈^bY~7!89WU!m`zg:O”dxx}XT%D$hG (12)g|~oi7xVފȬ";SEcy |@D@aLq{X7G+JQHf#֫ȳm; ɟNG"}bw\} [wh4xO#1y@DfeFD+FLDlfj`?("Y"1Ԫ S/_>yrX,J)!1o33շ/} O+?sX旋S^;?h0C"D`CdqTߌV?~𰺌DfBDfpٟV PFY{>ƽ*"H̎]9jv0ER0+ʼ4>E!D|5B-ת_/QaMLTf7]PSSD39_(g|Ha+(ƈȇðC. FpfV{Lйu^DTTQDFQ98 [P;WgaV?ċw& W/5DŽ@yͱ w_'ڝDךsAI1炈y)gaH8 ȉ;<{wŲW V~u)k*_=07|$r.ߦ~(9g~uG~htý{DD7O+؎^^4Pfbalx??v/q)U8Ccgޡ 7(+*O{ YJ ">J|~yh4u,uk˻!8yB:6ÿCxo/]x˽׼ =?Khf^H;? V~u> ^^M,T`|q7;!$ӕoV/|Ao''G֧~_{qtۛ'jm/z \7$o>EMx' V~fykq62^PmL#_0cC[7l_='=#={gZFuyٽɧޟ,L 70߯.3֓>:c =xq2~%53$}tA8Z Ott%HHNUSJppۭLDTALMqﻮ33T&+T TETHDDD Dz8 W4iz}{;v} fR"2j L `X9%],z]UTs.8~.sco~i V 7.9g`󁛦u%,`x)")!vR@.XUU]͚󳳪JQD5].h9eEPqT(hTعc*Ebz? )uCrHy}UUU ZBpl>l6JH 8`i& KI<1}}3$fw!}JECN@0cc|ԁЬҨ1TDT2h{9K]M_^ sfÐá*D(NzCY}v̪{?L˅jɣ[?a V a UQQd!/~lT!9';? 6])qGG}TW{o@De3}3ܐ8 YC;dIEZ=rʐ7>wD̪ݗ\⣧=hEz@w(9h)Iau]!`0noz"޵V ":lK$WG/>Y3$)5-wZF߾x9(@}fs8{=C1BO*2WWf߬n>Xvbq 0<\m}ţgYCpHR ɘq_JQUBd3<;$3,9䒆u P^K"̜p{{u:t}BdS"u53 M3G~g=S6T IDAT@KaR^珪BiV V~+9R~'W7UL& !dDD vCR1sm\>z PUL1CN9Ñ&_?S.Ť%"FdD4$t ;/E3u7h6NvQORhgl^xɣ๎A]} V/<)>TsDF"! |׳{hW HjͿ~t"b=3C66%"2{vիo} rw!VHNDg&6*,Cd <S7_={:k*efc;2b$A I]~vE;=ջC 0*" #&PFCL{_~w񓤥ƦdK&SV}^HaҠe LhPS|{UH4b'>+?o4ltiQC.=~C*K< fcޓTU$Cs!W>ԏ|RͷϾK{`uhd2`` `(nkD,"E}{v>lrL,(!;Q+"H^TL뛮7mU{b3@AЬ1FQ t>NvS6%tv{@3Tb.*)%31)Αv~Lgg߾:1*?Օ75FRʘc)1 s}ʒ #v(njѳH)V}]cU})j!֏m|x^2z?DM@U9DDv+э1C.*eH1"bn]'˳˗?Mba0s!3dD,ێ Sfճo>nϒ!E( #j51Q6Eq2X}{l\jލ45B23")f:w UOÐ}Lf)DUEr!=cT+`rfsK7_XL&.fUpL&Ef!LKV3mJy@$353e(!3Cq.ufRTu r?VOʏ+cʽ=Qa4/7?C Sb%0XNnެoKfHjQk#UӿW] CFD眪:JQO8FW7ۗqi]צ%GӰߗzls{/`*~'g d=8UUŗ߼Tdc"xr i+iZ_늂3 a=nΊ P2;?ţj?OϟfJ}Ji4W-̖A$KR0ag<(fNnhfVR" @U4 !ZJ=Rw)Z뺛WR60t11I fFfNS0 !8asTji!3nwWWϛoObمMJQGY*S]0e9+ %WmL)mַW//QB⼩'Mן/Cfeݫo_g ^8~< 1QR<+Kڙ9GN*"XI4 f4ʹiC31i("ͦg[ɗWIɲh<)w#";D9']nǀc,<)uBpR2@+? & *`82!l7~?nG.u=͚ !<蓪˶}T"4n^{oܼy pPN )}~yy|^Uq@F8,Krjfh9t[sn7MJIhՈȻXUUs|8lXΣg2M|ylgmeYM\=/nTD>]<~ѣ'~/v`c)}.C+'SNI V~0lԛ3#9)ԩC5n^ӟ?˗áViDLCAOw?'&rKB&L|_||>B,(cRF>n]9O>%#þ>>[nnw/p4sm[fu٬Pmoo8V5> o^\ӺU5ֹ'9@U訤~zy7\>ٗGz³ !Py!3l駿gX^z̎ݓܮx4l2z6>MJݳozۧ~ȟ}{mrjf)et9SU"H/QRSrc+IVB}IY__߯9a_~1L 9UUsvu}ǟ<}я1fubM֞{Q"pXL$_ {+!`u]տ_gujR?=[.擶il\(jl>_6O>z̃zW!W/8mÌU9$CAv$" ŷ<{˫('cs5,: YJ^}տ|?ǟGjI~8t@\nֻ㳶g<_NGU}ѦWG"I̬[du̪'OM*5(D @ɓ>l8k'3*x&jdXk{PJ~])EKyuosU7M3ggg$ov6CRYvuə"R9K+Gf%4ܳHDXw8[3lfzu{SL}i'.>ITu] {S]ovM\ Ų&!)pѣTEU,]aʹ1E M[-v/"Tt>pӮOɰ,뢨D?yxRԻd ({ iǗU¦r,|4Mcmu2{a嗝)v֖qo?ׯl6Mǣ`ԥJc [[׵+nZm6o 1w`kF{h8__-eԥSղ,! b6u+B>߾y[Λ>}<~t9]=qEׅ՛o^b<5&;kժmOŃYaMbH8$oOrUU |lj:TeYUEQ43%S뻮M].6u?])1ݱu޾UE4<lEa/.&EU.3࣫; JM " Fl_Ea3Wr{5-ʪafг1V~Avow8)%뜪vD{d2Ma5UmqUUR1qY£GO^~OLTqFpmr1\>3"r~23hvXV +ʲjnnn6-[z߽y񇇗ˇ?a07UU*#3z^nvh0.Űr+-\*k 2"@IUcjˋި,˲t#c>ψHUB}XWu|4nu8u_[V]v]J  ƪ(1]o=#4 ".K7GWA9Cy -@5eFMYs.UΰK, J%5|!!rL|Ǐ^6ڈ@I$[u])~Ç޿}ӳ̗M1$y!kr&$ں(v勻OHd_W_^\C(RJ4Ml:]N~z[ulrwwh<ͧe٬jd$uuSPZR4P*g$@xƘa}LrWbLnY~Pzt*&pr6&@"[V&PU뺷.:UAPX+l錛M.`k @:Y5!|v<yruu5N~U5_IPEGӮ?ۺ?S J ^nx}SY{A $Dd攔BČdYp$FBa3:w7wZTH - ~lۭvK0"* lG>#)TT5Ka͟^^g(9߀b̙ e6fd)<9i޻DM~VUU$E"+{?ϗ"r2E2U$B۷vtzuuӇG,aUT`f"[.>\E=VB@d_NG!jAD&PUATnĆ $"#2c~ c4Aڪ)Ǐ>%JBkeӶqXVݶK}`pA""ulF4s8t?_9y6G~vgϮ^3;ɱՙs/SkmT^SJyB8 ne^m!%(jD@wl@ e2춛vO"_~u8 kl8/vQxQ.OOH8Q7>3:L4)D\cnv]\ :f yz^mV1|q9^m YG>\! \aOUU2VUChjcv*"* `$` "!̒ bc5x0s\%[l5^|_D^} " +?n "v /TIrI"Ϫ;FwHcBǼwn[owz\Ej٬~  D%j$5&eln!Ig[.ղ !bTq~ALGB&1].b'El#N'e̚Z(\l7fڶo3f!:go{xSt.Uΰ T9`gۢcp/c^y( ![ EU\=c˷o!hBT"*o}P4HJVOPJ؂m)bMIH}ozn+TmQ6ȬϛDGA!`s2 (Bȓ0{S+w|ΰW/:6_ѱʥЩBԱ|?5>'Ud,wHTmT5FOD6O:cb5!34!zAW\5Hz`Ra 1cA$ i>_~qFuĺ{"ւ`0԰U"*Lyo$ *"*i$98T>|#gL9 _W9gX9e@0,J^AFդ r deQzSE`$,&!iJ I:/"0eQQ.;J*bJ>`ɄŞ?n^o\2Wv׭+cv$BF޷vbO)~"rH?[" IDAT 0ΊTB=]GTYa8/s$0$F4LJ(BqLFCstg'm"&9 4C=fR$HX1 D`Cz"1%A5eѳX=xŔ2DNn͕SH?5)(QVaUEY1xI`cFB?\/?\o'>Zv]W֥/oEvޮ)x$Y/)*$ι:?uY!YKm* {/G9^r$bbfA9R)j $"B H (cgaV~q?˜Eɫ@gÀ-]U8VDsIѰ56%q?&b695JL)&QQVD0/%= IQDe, LIQB1SvrՒq+n2I~֫5X'_`@5 >~wYv?+BsZ-J$WDYŸ:Vm4}A|_5cGPF@I)Ds1Ho(РAb6xI@D$2e @NDrjJTSݪ+M"2r>+ jW]H͠ N|bƏ_xz5_/ݺk(dЫ˲pv,zFrl\i*N@_EJYrvPR%ۅ^tgQ/ND) _WCWD. IcbPUIDI BPQU&AA%HJ )(hR "%A"Tك–{5 @IqozIA pB~^ݾzW~]Gt2{x:UQEAPw^[obwkʧ%*'.%_}dg3} ݮ]]yP({S|s بkSB yM^C]E!ybP$dAҵДQ*F!6.HoXw>D˟~ONͳǏ~`ثZ6 TPݧ:Dpjds(^Ő) fs}A:y3̜az0 (-,cG !Ө {u }F2AJ &nf B nu@iHDT0y(o@YX{GR8+AQj}XM>J,/v Q]]M^]>}bk%f @6h|O$؛#ⷁ%ϣgޭ'>zxZk˲\w)kI$E11ɨ` z*u"Rf @4 ٗK -h4bDIRV lA H o e2]fW.&I+˂䓀 )03hJIc"O_:Y`ƿo_̦_Viv*P%$8CN$ qZe_loTd~Fn0=xpt<-VoP$K[; `BJ$ac2NU ɋYG Ie揄8gJ u#9V~XwD,'x#GXG( "ʉoI ,CJI1ua? '13A{G2wKTJ1ZR{}g&CĄ@L^7 ʲar達@rKIgʟ K?!gGC7A ;71&P=9 dJj벩"UQÜU{)olN+zaH!p3sY2 겪\aɒR0M/n9폤Ȩ'ST/y%B|s&Tΰw+c Z Yy7 )H9]rbcmumh&&F@IٴI y{d̠("? (#dѪ)H BLD5Eꫪ*²a$$G`T`ȪcR"D7Q!€$tϰwbjOD?/V~|ʞbKm۶ (eYu/%]]H`H3=%@jOQҚAn/Bf]`2RQYoĆI  lj4!H4qJ:Qֆ)%Bv~Z䠯>VR$:ڞa_|v"TTfPHP̷=.>_|d"{~!4lG{izxVΗ6ȇRCE_eUT{t^{kKoRhozP%(R'iD` !Ϩ :."S!״mm%Хcrr9T{ιՊj"S (Ƥ @"@dBس0U51(j0?G"1˙2AꘓDĮHBluev[kH?p/__oOZv X(*2S{A. O@P:*I~I% cPUU|g$q8 mSvEilZ# !YYJh[n 8û0k`W5MųYwaƢt&ȋMITQ^h;%3 B˲>mQϩj iL9Y*"QȰ~]NgUa[EdM!)@JDŽɠv:)ꋂ1zQ_nC͇x2cb kIҐF0H6Z&)j~3 P70HFrF/j1 hzu!eĜ0EvfgEQqͶ VOjȚ b^V c{Othd瓙Y\.kK6ƈ1(8R""mJ"lVh_ϨYDDcR DhXɢV3|/*g㫞_Qsy]Zkxa.@rL"6x2)nveU7l(fZ6HrJeZQuImlv~XxLI YT)@u]5Z57=37wu>~^.1F`"0VZR|#Arw{)*<ﺝ+|>nYY 1p7 ]<K)eFyPP׵1wz(/LaS8FQRYY3qx>_>@D$ɊĔq[O3.A7EAp$@+cLI1bԫ^\QV8d~Zl6%]z 9Dt>) 1 !z·IRbɃhJJj&d}qΨBPFn>(2[W&]!iaLa$R7Rn r]}{^3Zj게Qڶ[,Dc6X[EAdTQˇx3rH]׃HDE$'@!@D(1$f{)ΰK0 *)آJ*Y-ҏ7Zaj 9Tx*1G bX/MOCE9=L{֩+{0ǃ(*"J.bR [_[b7˪#1Rxxлm_M7SJftq,^Li C,/4!B}Lɲ)dCZ-o>7rΥ2(ٞtE]ݶ&h:-w7M銦Ou]Z-fYCN$y8_) g~ '3ُN;VE@DF$҆2C9lA^nZiȶGEQ7n>ܔf3s}ehכnmmشm@ΖuSx!"SBz SjWw6<Zu?Ŭ`Pg Z5&*(ծm?y~ں6m>lwj[`92{\F,gɻO&[zEhf4=*rq>_~ 3k<>_ +Ġ `HRc"D@5 $%)d&I͓/~*)nC4_-u Q;g`IvmcbحV t=:S܄qmӔӲ.a&"&"kwq1$xQ!vM7MkJc`%6DڵQnnnkFE5.2nSvK@)J{u,˲, "gKsUD%b~=M^|iuوHagvXW+LUU,gXZ`@BDBtI8.[= |tɆYa ,KFLM}+lweUQ7m۝^܁(Q# ;HJEAES| wn" )x4'?uU,fvVu3EBD?ݬC蜝$Dl6lEfl^Z@1H w+AǏ_u1 82& lT_7/!Xfx2vo>{:% "[xJk/// +}"xĔ3LAIx)@"ug<PDU5IRH\cTB\ÐI@c I1J]Arln?YK|Em 1- Rc*RJ)&%BCF@CR[ؘh烵4զ(?}|ܬw](.v6fǹ]zjk\Nx1^bKu]o7*ܠ0I6K)0ږE= !>c1h ݻ߯7|:=zih= !lnbra J A.^cݮ.JArwx|5D$ðZqr޺""J-E !& Pi}|g˵_X̐RJ00!=Te<|IA$%Mea/ݶ7EȆ0(SQƽ-(Zx^߽]-h4y@d44&v)xvNlDǘ:UdbTh uۄn*R6ooz/s Q"*s@(Q/!{7/N_=Ckn_<#9{y9[GDTb+SҮO+so$] LӋIvϽ2MV~9[UC(OS`DJb> N/?, U3 *TaZ",p0lB tI{[v;U1ւ:r1$TRL!Ԑb[_Oѣ=gdX׵sNDnlȈ9XZPS Z]Nn-֋~u=_R$Ru͠1T !w6'v" +? SJ\Ca,ATDg8DJYʮ!OQ{[;P5EoL(ZFks7x%hե1߿ﶣADDƷAUYؔ!x,*z?~xml׿I,U9KW(KJPi:޾yz4|qZ$*1Hd2h<F6jݫׯ~m]ڧ|=~t`4Eaͻ3X߅p`8.77no!t\ 1`!C1Fn6n ,]Э֋nvl 3oTI1*u !Ů]n( 8 %/O*":fݪZWu!rR`"s9T5H"l2=z$;jZݕUTADS+KlSRP m0 Ln7 ^LFAtU3{%[ )̳'Q$2o_ppcM]u*k-!*hL1ЅvfZl6fX|z\|*N'O~fN)x/EQE3ߐD t<=yxo>|t9}P5!Uo 9^֛fYQի 3rsDDFT@0 c7wnl6z~S_]1eC#bި߻/wwㇷQM!tldQC(h]9˨7o׶ʺ7MQbڭ7f`vehBVjZ-nooW˜Q/t<{ëdZEUTHSvE @!pqn`/^sƳtr "FP mY/7fO>mWt6_=zb4l6)!;՞$sQIWϾn%v4Hv]>ӻo~5v٬;;)gXY'1[d~xsv>xsǦLvPlPUeYUpd<?N&``S k%%h7ÇbޮV̆fd̻蜫jxb, Vjr1N˪,crƑ3TLDDM]dd\^ezu]E/&ߦWR.ˢ1t*[۶^,lvuu5dXSSOLGEiʍdpu9~އ2BQu]eYU`0UU5M3)˒I# ďR @,5 zbrZ6V2e56JY~4x<F^,O_3bdz M9ʱlӛf.1UU]]]fh\Z{Pΰ ! P1$~e۶Y"1L9玷=d~PgXɍL4,8/..sƘ .;V(o_<h4X׻n^a%28fWL]]s0F;voy0}Xۉd:Io-˞n^V+7Ma=4cJ2/rٶx:jivsy:,f: 9aQ(ocp1U^OS۶tUE*1OT.YEށ֒Rޣab( R('JZTqB:tSCsպւPYd,h. 5@=f nu]v;s9UVSNQoRU,VNAK_͠q %7v{2OXN%A&p7̵,ʫэLYu !L*nܼ^9~잺@Sx1kο [PzkEP6䜋17]ʏ2y T8. I0 ey hE>$rd $>NJMI3jѺg݋ n@\.ݦ̢{Zetϵ}lh'ݐ31$vGeH|2ߴb韌n.*f/TEaEaL1ciTxӜ|r-HN\ES4 ^Rʇm "ti0i ^%w ð\.a /C;lHJt&G|mdA5Y8ǘY7psR7!o ]*F:O &HPhABP(V ҊBP(( BP(ߎViEfY9_,H,?j۲oq1VURb^uJ RC(x1=WMEbH/* ZBg?}BIm!UPu]fMB k8H !o=BI<([IENDB`././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/data/mpl_logo.png0000664000175000017500000001405200000000000023514 0ustar00zuulzuul00000000000000PNG  IHDR,,N~GPLTE$\R>|7uG$K'R!C9w4A;51864%M7/k<{;z;{=|>~3.b-_+Z*Y0g0f1T4O3pLGEB B!D?#H:y=}*X<|>~/c-` >|.a/d2l =y:u ;x&P,]=@?95CBA @~#F$I$K@+[)V8<5'Q4o:v ;w'R {8t8s(T&O&N2?1i ?|(S4n2j5p5o6q7r9u;$J2k76>!E!C%L#G24A13l3m!.tRNSfIDATxkp]Uo#yk_cvpo3QFSpPDDPDjyy<2<, *hC7yXk9'wӜ{kڿML'*`9X`9`9X {ԓ VPX*P/X݈*U1UVT1Y)68ݨEűiUԬfKZVmJezzkcUqPZҍuUq[2ZY'=,DX-, +';Vr:RtXu}S `k9X`)U88S :r @V_Na) `}) 9XS ?8zN}4X 9XS ;zA?jGiay!+ #1Wth)@0g:I4XO 䧭 ,k`ݕ_Ez*z|sDuyn.`=[}"t.+O_VBμRrDuiNN ł>t԰.`ݑSߙpG"_N~L-MK օua{"~:XGw| uAg\"Z;B4Xw_~~:*(`ݚC_Ouk':DuD-E]QM9s ºDu,[ W%Csy̙hd- 8cDuW϶Mfl}F4X_c2g$x9 ,%A'rdud m^\7+6vX`}Nrc)[JZFV:X߶ܕbG_򥩁ַ$ѕ!~!O䎔>2_O~ԳvF*FLhun4vF*zfuIpB$LUT GqnθE-5E٥J5uX1 L16fg=-R,skBIcaezǕ+NQv21c SdM4~6YRj$Hk+M֭֩l/ kSNV0,o`m-$vW²LB24XPؽB|[2-Bz" IXz Y>n!JpXU+8 Q`4gb5هfJ!iC`Nov8 aa8a*TXL7lf=ugTܙTS`7e,%1DX3socm,߻),U%MFZf&pgcH$gY.؟>mMxg5,3M6/vϥP8'o nn.i3&rOILt g<O z,qc1/c:e4WSjw'fne:L}m^LՈ:n G~bX&͐7$cǠH+ q6ZS:6QNvWXG>D̈;Q?`ԈC؜F4%Yc߀ݍ5dc jDsFrFL](-y8F"^DhgqN-* I ;כLj4X$Wªf+ FVআ%ՀؚbetXA w}X+mgyxg& UB3u4ٍkb6/~m&0&ҬlETgB{ UATXIbTf٦o*˜> V&Vj@Z|TbWUUlKk 3Vu"Rժž`UBi#fT&DlWa*w5m`TcըJ]@aD[K*c5d vVlc}U{ P38U|%E6=^㑑fk0-t:Hd 2kA'Aڕ⟓ Saў}sӈ,oe"al hh' : R:g` k' 15!VM. |pB Vta0Y  \K :*¢eǁdbڵڑmĿ`8 C|2s#hl;$Mk8[G0K2FD씬?O3*F HF0]M~pXvȺ q@xC Ei5aŠk,*_ i|u/4 \+hQkXiHk$K U\̒zڭQoRX ^ ސ ZjfͪW(`EZ[G ^3Z}EJBdRZɨw*PCS~2}!E Cɰ A e03!5=B dj(^PBCtX +E1"V}͵T) u ֛* @ PAUuO UoXra*rOXrt9ozK*(:2V`CImBu`Xw$XZ9XZ̅[gTb].XC4X;5 `[ٰ6t/s*˶3amgXXZ`mkc7 X m,lrMfj5SXZ`=`q(Jm,VVVޝk3A %v@X;vvl7XPCw3X4X;Q;fw0ug9S +qܿD~+뗠KMEH~mkz9^F1/8dڠz~~}qF)( `S>h jH#*N%Oq(NĵK.-*%` rՄ|4ڔ4,Tqv`-l?@|A0FaRq_ ys1A)Pԃy;`)'t@)POS6zXA I2A)PJx93#QeMnNŜN eRߍޟQ@Ѱ0\3LHv EK%5n% zX Uzʄx PP| rԍa*+姳l:;( QDeMn@i쨗y'끚(|R3z@\ Olf?WAeIRL!eM` Olg(fR&!ĿXb`=H#Q8 PAjcJY3W`L3;ņ'h\Fm)krs\bO ? (!`PiRj@3' q':S?D ؙ80GeL'qԝ AKcgh@W?'3:X7G I'1bg$dB F^Hc7jcLYS5Up:XWqG)x*!r! G7ϛ +:Nuvqv1r0<'lj*]x .  pUwWy2fU/`cq*Vvza*,Ne grRn VOBko,V ±J#Q,0l}Vu)*5h{u שp',r,r,[a>IENDB`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5940814 python-muranoclient-2.8.0/muranoclient/glance/0000775000175000017500000000000000000000000021514 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/glance/__init__.py0000664000175000017500000000432200000000000023626 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from glanceclient import exc class ArtifactType(object): generic_properties = ('created_at', 'id', 'name', 'owner', 'state', 'type_name', 'type_version', 'updated_at', 'version', 'visibility', 'description', 'tags', 'published_at', 'deleted_at') supported_show_levels = ('none', 'basic', 'direct', 'transitive') def __init__(self, **kwargs): try: for prop in self.generic_properties: setattr(self, prop, kwargs.pop(prop)) except KeyError: msg = "Invalid parameters were provided" raise exc.HTTPBadRequest(msg) self.type_specific_properties = {} for key, value in kwargs.items(): try: if _is_dependency(value): self.type_specific_properties[key] = ArtifactType(**value) elif _is_dependencies_list(value): self.type_specific_properties[key] = [ArtifactType(**elem) for elem in value] else: self.type_specific_properties[key] = value except exc.HTTPBadRequest: # if it's not possible to generate artifact object then # assign the value as a regular dict. self.type_specific_properties[key] = value def _is_dependency(d): if type(d) is dict and d.get('type_name') and d.get('type_version'): return True return False def _is_dependencies_list(l): if type(l) is list and all(_is_dependency(d) for d in l): return True return False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/glance/artifacts.py0000664000175000017500000003557500000000000024065 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from glanceclient.common import utils from glanceclient import exc from oslo_utils import encodeutils from urllib import parse from muranoclient.glance import ArtifactType glare_urls = { 'create': '/v{version}/artifacts/{type_name}/v{type_version}/drafts', 'update_get_delete': '/v{version}/artifacts/{type_name}/v{type_version}' '/{artifact_id}', 'list_drafts': '/v{version}/artifacts/{type_name}/v{type_version}/drafts?', 'list_no_drafts': '/v{version}/artifacts/{type_name}/v{type_version}?', 'publish': '/v{version}/artifacts/{type_name}/v{type_version}/' '{artifact_id}/publish', 'blob': '/v{version}/artifacts/{type_name}/v{type_version}/{artifact_id}' '/{blob_property}', } class Controller(object): def __init__(self, http_client, type_name=None, type_version=None, version='0.1'): self.http_client = http_client self.type_name = type_name self.type_version = type_version self.version = version self.default_page_size = 20 self.sort_dir_values = ('asc', 'desc') def _check_type_params(self, type_name, type_version): """Check that type name and type versions were specified""" type_name = type_name or self.type_name type_version = type_version or self.type_version if type_name is None: msg = "Type name must be specified" raise exc.HTTPBadRequest(msg) if type_version is None: msg = "Type version must be specified" raise exc.HTTPBadRequest(msg) return type_name, type_version def _validate_sort_param(self, sort): """Validates sorting argument for invalid keys and directions values. :param sort: comma-separated list of sort keys with optional <:dir> after each key """ for sort_param in sort.strip().split(','): key, _sep, dir = sort_param.partition(':') if dir and dir not in self.sort_dir_values: msg = ('Invalid sort direction: %(sort_dir)s.' ' It must be one of the following: %(available)s.' ) % {'sort_dir': dir, 'available': ', '.join(self.sort_dir_values)} raise exc.HTTPBadRequest(msg) return sort def create(self, name, version, type_name=None, type_version=None, **kwargs): """Create an artifact of given type and version. :param name: name of creating artifact. :param version: semver string describing an artifact version """ type_name, type_version = self._check_type_params(type_name, type_version) kwargs.update({'name': name, 'version': version}) url = glare_urls['create'].format(version=self.version, type_name=type_name, type_version=type_version) resp, body = self.http_client.post(url, data=kwargs) return ArtifactType(**body) def update(self, artifact_id, type_name=None, type_version=None, remove_props=None, **kwargs): """Update attributes of an artifact. :param artifact_id: ID of the artifact to modify. :param remove_props: List of property names to remove :param **kwargs: Artifact attribute names and their new values. """ type_name, type_version = self._check_type_params(type_name, type_version) url = glare_urls['update_get_delete'].format(version=self.version, type_name=type_name, type_version=type_version, artifact_id=artifact_id) hdrs = { 'Content-Type': 'application/openstack-images-v2.1-json-patch'} artifact_obj = self.get(artifact_id, type_name, type_version) changes = [] if remove_props: for prop in remove_props: if prop in ArtifactType.generic_properties: msg = "Generic properties cannot be removed" raise exc.HTTPBadRequest(msg) if prop not in kwargs: changes.append({'op': 'remove', 'path': '/' + prop}) for prop in kwargs: if prop in artifact_obj.generic_properties: op = 'add' if getattr(artifact_obj, prop) is None else 'replace' elif prop in artifact_obj.type_specific_properties: if artifact_obj.type_specific_properties[prop] is None: op = 'add' else: op = 'replace' else: msg = ("Property '%s' doesn't exist in type '%s' with version" " '%s'" % (prop, type_name, type_version)) raise exc.HTTPBadRequest(msg) changes.append({'op': op, 'path': '/' + prop, 'value': kwargs[prop]}) resp, body = self.http_client.patch(url, headers=hdrs, data=changes) return ArtifactType(**body) def get(self, artifact_id, type_name=None, type_version=None, show_level=None): """Get information about an artifact. :param artifact_id: ID of the artifact to get. :param show_level: value of datalization. Possible values: "none", "basic", "direct", "transitive" """ type_name, type_version = self._check_type_params(type_name, type_version) url = glare_urls['update_get_delete'].format(version=self.version, type_name=type_name, type_version=type_version, artifact_id=artifact_id) if show_level: if show_level not in ArtifactType.supported_show_levels: msg = "Invalid show level: %s" % show_level raise exc.HTTPBadRequest(msg) url += '?show_level=%s' % show_level resp, body = self.http_client.get(url) return ArtifactType(**body) def list(self, **kwargs): return self._list(drafts=False, **kwargs) def drafts(self, **kwargs): return self._list(drafts=True, **kwargs) def _list(self, drafts, type_name=None, type_version=None, **kwargs): """Retrieve a listing of Image objects. :param page_size: Number of images to request in each paginated request. :returns: generator over list of artifacts. """ type_name, type_version = self._check_type_params(type_name, type_version) limit = kwargs.get('limit') page_size = kwargs.get('page_size') or self.default_page_size def paginate(url, page_size, limit=None): next_url = url while True: if limit and page_size > limit: next_url = next_url.replace("limit=%s" % page_size, "limit=%s" % limit) resp, body = self.http_client.get(next_url) for artifact in body['artifacts']: yield ArtifactType(**artifact) if limit: limit -= 1 if limit <= 0: raise StopIteration try: next_url = body['next'] except KeyError: return filters = kwargs.get('filters', {}) filters['limit'] = page_size url_params = [] for param, items in filters.items(): values = [items] if not isinstance(items, list) else items for value in values: if isinstance(value, str): value = encodeutils.safe_encode(value) url_params.append({param: value}) if drafts: url = glare_urls['list_drafts'].format(version=self.version, type_name=type_name, type_version=type_version) else: url = glare_urls['list_no_drafts'].format( version=self.version, type_name=type_name, type_version=type_version ) for param in url_params: url = '%s&%s' % (url, parse.urlencode(param)) if 'sort' in kwargs: url = '%s&sort=%s' % (url, self._validate_sort_param( kwargs['sort'])) for artifact in paginate(url, page_size, limit): yield artifact def active(self, artifact_id, type_name=None, type_version=None): """Set artifact status to 'active'. :param artifact_id: ID of the artifact to get. """ type_name, type_version = self._check_type_params(type_name, type_version) url = glare_urls['publish'].format(version=self.version, type_name=type_name, type_version=type_version, artifact_id=artifact_id) resp, body = self.http_client.post(url) return ArtifactType(**body) def deactivate(self, artifact_id, type_name=None, type_version=None): raise NotImplementedError() def delete(self, artifact_id, type_name=None, type_version=None): """Delete an artifact and all its data. :param artifact_id: ID of the artifact to delete. """ type_name, type_version = self._check_type_params(type_name, type_version) url = glare_urls['update_get_delete'].format(version=self.version, type_name=type_name, type_version=type_version, artifact_id=artifact_id) self.http_client.delete(url) def upload_blob(self, artifact_id, blob_property, data, position=None, type_name=None, type_version=None): """Upload blob data. :param artifact_id: ID of the artifact to download a blob :param blob_property: blob property name :param position: if blob_property is a list then the position must be specified """ type_name, type_version = self._check_type_params(type_name, type_version) hdrs = {'Content-Type': 'application/octet-stream'} url = glare_urls['blob'].format(version=self.version, type_name=type_name, type_version=type_version, artifact_id=artifact_id, blob_property=blob_property) if position: url += "/%s" % position self.http_client.put(url, headers=hdrs, data=data) def download_blob(self, artifact_id, blob_property, position=None, type_name=None, type_version=None, do_checksum=True): """Get blob data. :param artifact_id: ID of the artifact to download a blob :param blob_property: blob property name :param position: if blob_property is a list then the position must be specified :param do_checksum: Enable/disable checksum validation. """ type_name, type_version = self._check_type_params(type_name, type_version) url = glare_urls['blob'].format(version=self.version, type_name=type_name, type_version=type_version, artifact_id=artifact_id, blob_property=blob_property) if position: url += '/%s' % position url += '/download' resp, body = self.http_client.get(url) checksum = resp.headers.get('content-md5', None) content_length = int(resp.headers.get('content-length', 0)) if checksum is not None and do_checksum: body = utils.integrity_iter(body, checksum) return utils.IterableWithLength(body, content_length) def delete_blob(self, artifact_id, blob_property, position=None, type_name=None, type_version=None): """Delete blob and related data. :param artifact_id: ID of the artifact to delete a blob :param blob_property: blob property name :param position: if blob_property is a list then the position must be specified """ type_name, type_version = self._check_type_params(type_name, type_version) url = glare_urls['blob'].format(version=self.version, type_name=type_name, type_version=type_version, artifact_id=artifact_id, blob_property=blob_property) if position: url += '/%s' % position self.http_client.delete(url) def add_property(self, artifact_id, dependency_id, position=None, type_name=None, type_version=None): raise NotImplementedError() def replace_property(self, artifact_id, dependency_id, position=None, type_name=None, type_version=None): raise NotImplementedError() def remove_property(self, artifact_id, dependency_id, position=None, type_name=None, type_version=None): raise NotImplementedError() def artifact_export(self, artifact_id, type_name=None, type_version=None): raise NotImplementedError() def artifact_import(self, data, type_name=None, type_version=None): raise NotImplementedError() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/glance/client.py0000664000175000017500000000313500000000000023346 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from glanceclient.common import http from glanceclient.common import utils from muranoclient.glance import artifacts class Client(object): """Client for the OpenStack glance-glare API. :param string endpoint: A user-supplied endpoint URL for the glance service. :param string token: Token for authentication. :param integer timeout: Allows customization of the timeout for client http requests. (optional) """ def __init__(self, endpoint, type_name, type_version, **kwargs): endpoint, version = utils.strip_version(endpoint) self.version = version or '0.1' self.http_client = http.HTTPClient(endpoint, **kwargs) self.type_name = type_name self.type_version = type_version self.artifacts = artifacts.Controller(self.http_client, self.type_name, self.type_version, self.version) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/i18n.py0000664000175000017500000000152400000000000021416 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """oslo.i18n integration module. See https://docs.openstack.org/oslo.i18n/latest/user/index.html """ import oslo_i18n _translators = oslo_i18n.TranslatorFactory(domain='muranoclient') # The primary translation function using the well-known name "_" _ = _translators.primary ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5780766 python-muranoclient-2.8.0/muranoclient/locale/0000775000175000017500000000000000000000000021522 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5780766 python-muranoclient-2.8.0/muranoclient/locale/en_GB/0000775000175000017500000000000000000000000022474 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5940814 python-muranoclient-2.8.0/muranoclient/locale/en_GB/LC_MESSAGES/0000775000175000017500000000000000000000000024261 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/locale/en_GB/LC_MESSAGES/muranoclient.po0000664000175000017500000000663600000000000027334 0ustar00zuulzuul00000000000000# Andi Chandler , 2017. #zanata msgid "" msgstr "" "Project-Id-Version: python-muranoclient VERSION\n" "Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" "POT-Creation-Date: 2018-01-30 01:30+0000\n" "MIME-Version: 1.0\n" "Content-Type: text/plain; charset=UTF-8\n" "Content-Transfer-Encoding: 8bit\n" "PO-Revision-Date: 2017-10-21 09:07+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" #, python-format msgid "AmbiguousEndpoints: %s" msgstr "AmbiguousEndpoints: %s" msgid "" "Application catalog API version, default={0}(Env:" "OS_APPLICATION_CATALOG_API_VERSION)" msgstr "" "Application catalogue API version, default={0}(Env:" "OS_APPLICATION_CATALOG_API_VERSION)" #, python-format msgid "AuthSystemNotFound: %s" msgstr "AuthSystemNotFound: %s" #, python-format msgid "Authentication failed. Missing options: %s" msgstr "Authentication failed. Missing options: %s" msgid "Bad Gateway" msgstr "Bad Gateway" msgid "Bad Request" msgstr "Bad Request" msgid "Cannot find endpoint or token for request" msgstr "Cannot find endpoint or token for request" msgid "Conflict" msgstr "Conflict" msgid "Defaults to env[MURANO_URL]." msgstr "Defaults to env[MURANO_URL]." msgid "Error {0} occurred while setting image {1} public" msgstr "Error {0} occurred while setting image {1} public" msgid "Expectation Failed" msgstr "Expectation Failed" msgid "Forbidden" msgstr "Forbidden" msgid "Gateway Timeout" msgstr "Gateway Timeout" msgid "Gone" msgstr "Gone" msgid "HTTP Client Error" msgstr "HTTP Client Error" msgid "HTTP Error" msgstr "HTTP Error" msgid "HTTP Redirection" msgstr "HTTP Redirection" msgid "HTTP Server Error" msgstr "HTTP Server Error" msgid "HTTP Version Not Supported" msgstr "HTTP Version Not Supported" msgid "Internal Server Error" msgstr "Internal Server Error" #, python-format msgid "" "Invalid %(api_name)s client version '%(version)s'. Must be one of: " "%(version_map)s" msgstr "" "Invalid %(api_name)s client version '%(version)s'. Must be one of: " "%(version_map)s" msgid "Length Required" msgstr "Length Required" msgid "Method Not Allowed" msgstr "Method Not Allowed" #, python-format msgid "Missing arguments: %s" msgstr "Missing arguments: %s" msgid "Multiple Choices" msgstr "Multiple Choices" #, python-format msgid "No %(name)s matching %(args)s." msgstr "No %(name)s matching %(args)s." msgid "Not Acceptable" msgstr "Not Acceptable" msgid "Not Found" msgstr "Not Found" msgid "Not Implemented" msgstr "Not Implemented" msgid "Payment Required" msgstr "Payment Required" msgid "Precondition Failed" msgstr "Precondition Failed" msgid "Proxy Authentication Required" msgstr "Proxy Authentication Required" msgid "Request Entity Too Large" msgstr "Request Entity Too Large" msgid "Request Timeout" msgstr "Request Timeout" msgid "Request-URI Too Long" msgstr "Request-URI Too Long" msgid "Requested Range Not Satisfiable" msgstr "Requested Range Not Satisfiable" msgid "Service Unavailable" msgstr "Service Unavailable" #, python-format msgid "Some attributes are missing in %(pkg_name)s: %(attrs)s." msgstr "Some attributes are missing in %(pkg_name)s: %(attrs)s." msgid "Unauthorized" msgstr "Unauthorised" msgid "Unprocessable Entity" msgstr "Unprocessable Entity" msgid "Unsupported Media Type" msgstr "Unsupported Media Type" ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5940814 python-muranoclient-2.8.0/muranoclient/osc/0000775000175000017500000000000000000000000021047 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/osc/__init__.py0000664000175000017500000000000000000000000023146 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/osc/plugin.py0000664000175000017500000001101200000000000022712 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """OpenStackClient plugin for Application Catalog service.""" from osc_lib import utils from oslo_log import log as logging from muranoclient.apiclient import exceptions as exc from muranoclient.glance import client as art_client from muranoclient.i18n import _ LOG = logging.getLogger(__name__) DEFAULT_APPLICATION_CATALOG_API_VERSION = "1" API_VERSION_OPTION = "os_application_catalog_api_version" API_NAME = "application_catalog" API_VERSIONS = { '1': 'muranoclient.v1.client.Client', } def make_client(instance): """Returns an application-catalog service client""" application_catalog_client = utils.get_client_class( API_NAME, instance._api_version[API_NAME], API_VERSIONS) LOG.debug("Instantiating application-catalog client: {0}".format( application_catalog_client)) kwargs = { 'session': instance.session, 'service_type': 'application-catalog', 'region_name': instance._region_name } murano_packages_service = \ instance.get_configuration().get('murano_packages_service') if murano_packages_service == 'glare': glare_endpoint = instance.get_configuration().get('glare_url') if not glare_endpoint: try: # no glare_endpoint and we requested to store packages in glare # check keystone catalog glare_endpoint = instance.get_endpoint_for_service_type( 'artifact', region_name=instance._region_name, interface=instance._interface ) except Exception: raise exc.CommandError( "You set murano-packages-service to {}" " but there is not 'artifact' endpoint in keystone" " Either register one or specify endpoint " " via either --glare-url or env[GLARE_API]".format( murano_packages_service)) artifacts_client = art_client.Client( endpoint=glare_endpoint, type_name='murano', type_version=1, token=instance.auth_ref.auth_token) kwargs['artifacts_client'] = artifacts_client murano_endpoint = instance.get_configuration().get('murano_url') if not murano_endpoint: murano_endpoint = instance.get_endpoint_for_service_type( 'application-catalog', region_name=instance._region_name, interface=instance._interface ) client = application_catalog_client(murano_endpoint, **kwargs) return client def build_option_parser(parser): """Hook to add global options""" parser.add_argument( '--os-application-catalog-api-version', metavar='', default=utils.env( 'OS_APPLICATION_CATALOG_API_VERSION', default=DEFAULT_APPLICATION_CATALOG_API_VERSION), help=_("Application catalog API version, default={0}" "(Env:OS_APPLICATION_CATALOG_API_VERSION)").format( DEFAULT_APPLICATION_CATALOG_API_VERSION)) parser.add_argument('--murano-url', default=utils.env('MURANO_URL'), help=_('Defaults to env[MURANO_URL].')) parser.add_argument('--glare-url', default=utils.env('GLARE_URL'), help='Defaults to env[GLARE_URL].') parser.add_argument('--murano-packages-service', choices=['murano', 'glare'], default=utils.env('MURANO_PACKAGES_SERVICE', default='murano'), help='Specifies if murano-api ("murano") or ' 'Glance Artifact Repository ("glare") ' 'should be used to store murano packages. ' 'Defaults to env[MURANO_PACKAGES_SERVICE] or ' 'to "murano"') return parser ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5940814 python-muranoclient-2.8.0/muranoclient/osc/v1/0000775000175000017500000000000000000000000021375 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/osc/v1/__init__.py0000664000175000017500000000000000000000000023474 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/osc/v1/action.py0000664000175000017500000000574400000000000023236 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Application-catalog v1 action implementation""" import json from osc_lib.command import command from oslo_log import log as logging from muranoclient.apiclient import exceptions LOG = logging.getLogger(__name__) class StaticActionCall(command.ShowOne): """Call static method of the MuranoPL class.""" def get_parser(self, prog_name): parser = super(StaticActionCall, self).get_parser(prog_name) parser.add_argument( "class_name", metavar='', help="FQN of the class with static method", ) parser.add_argument( "method_name", metavar='', help="Static method to run", ) parser.add_argument( "--arguments", metavar='', nargs='*', help="Method arguments. No arguments by default", ) parser.add_argument( "--package-name", metavar='', default='', help='Optional FQN of the package to look for the class in', ) parser.add_argument( "--class-version", default='', help='Optional version of the class, otherwise version =0 is ' 'used ', ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog arguments = {} for argument in parsed_args.arguments or []: if '=' not in argument: raise exceptions.CommandError( "Argument should be in form of KEY=VALUE. Found: " "{0}".format(argument)) key, value = argument.split('=', 1) try: value = json.loads(value) except ValueError: # treat value as a string if it doesn't load as json pass arguments[key] = value request_body = { "className": parsed_args.class_name, "methodName": parsed_args.method_name, "packageName": parsed_args.package_name or None, "classVersion": parsed_args.class_version or '=0', "parameters": arguments } print("Waiting for result...") result = client.static_actions.call(request_body).get_result() return ["Static action result"], [result] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/osc/v1/category.py0000664000175000017500000001070200000000000023564 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Application-catalog v1 category action implementation""" import textwrap from osc_lib.command import command from osc_lib import utils from oslo_log import log as logging from muranoclient.apiclient import exceptions LOG = logging.getLogger(__name__) class ListCategories(command.Lister): """List all available categories.""" def take_action(self, parsed_args=None): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog if parsed_args is None: parsed_args = {} data = client.categories.list() fields = ["id", "name"] field_labels = ["ID", "Name"] return ( field_labels, list(utils.get_item_properties( s, fields, ) for s in data) ) class ShowCategory(command.ShowOne): """Display category details.""" def get_parser(self, prog_name): parser = super(ShowCategory, self).get_parser(prog_name) parser.add_argument( "id", metavar="", help=("ID of a category(s) to show."), ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog category = client.categories.get(parsed_args.id) packages = client.packages.filter(category=category.name) to_display = dict(id=category.id, name=category.name, packages=', '.join(p.name for p in packages)) to_display['packages'] = '\n'.join(textwrap.wrap(to_display['packages'] or '', 55)) return self.dict2columns(to_display) class CreateCategory(command.Lister): """Create a category.""" def get_parser(self, prog_name): parser = super(CreateCategory, self).get_parser(prog_name) parser.add_argument( "name", metavar="", help=("Category name."), ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog data = [client.categories.add({"name": parsed_args.name})] fields = ["id", "name"] field_labels = ["ID", "Name"] return ( field_labels, list(utils.get_item_properties( s, fields, ) for s in data) ) class DeleteCategory(command.Lister): """Delete a category.""" def get_parser(self, prog_name): parser = super(DeleteCategory, self).get_parser(prog_name) parser.add_argument( "id", metavar="", nargs="+", help=("ID of a category(ies) to delete."), ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog failure_count = 0 for category_id in parsed_args.id: try: client.categories.delete(category_id) except Exception: failure_count += 1 print("Failed to delete '{0}'; category not found". format(category_id)) if failure_count == len(parsed_args.id): raise exceptions.CommandError("Unable to find and delete any of " "the specified categories.") data = client.categories.list() fields = ["id", "name"] field_labels = ["ID", "Name"] return ( field_labels, list(utils.get_item_properties( s, fields, ) for s in data) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/osc/v1/deployment.py0000664000175000017500000000514300000000000024132 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Application-catalog v1 deployments action implementation""" from osc_lib.command import command from osc_lib import utils from oslo_log import log as logging from muranoclient.apiclient import exceptions LOG = logging.getLogger(__name__) class ListDeployment(command.Lister): """List deployments for an environment.""" def get_parser(self, prog_name): parser = super(ListDeployment, self).get_parser(prog_name) parser.add_argument( "id", metavar="", nargs="?", default=None, help=("Environment ID for which to list deployments."), ) parser.add_argument( "--all-environments", action="store_true", default=False, help="List all deployments for all environments in user's tenant." ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog all_environments = getattr(parsed_args, 'all_environments', False) env_id = getattr(parsed_args, 'id', None) if env_id and all_environments: raise exceptions.CommandError( 'Environment ID and all-environments flag cannot both be set.') elif not env_id and not all_environments: raise exceptions.CommandError( 'Either environment ID or all-environments flag must be set.') if all_environments: data = client.deployments.list(None, all_environments) else: environment = utils.find_resource(client.environments, env_id) data = client.deployments.list(environment.id) columns = ('id', 'state', 'created', 'updated', 'finished') column_headers = [c.capitalize() for c in columns] return ( column_headers, list(utils.get_item_properties( s, columns, ) for s in data) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/osc/v1/environment.py0000664000175000017500000003772700000000000024333 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Application-catalog v1 stack action implementation""" import json import sys import urllib import jsonpatch from osc_lib.command import command from osc_lib import utils from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import uuidutils from muranoclient.apiclient import exceptions from muranoclient.common import utils as murano_utils LOG = logging.getLogger(__name__) class ListEnvironments(command.Lister): """Lists environments""" def get_parser(self, prog_name): parser = super(ListEnvironments, self).get_parser(prog_name) parser.add_argument( '--all-tenants', action='store_true', default=False, help='List environments from all tenants (admin only).', ) parser.add_argument( '--tenant', metavar='', default=None, help='Allows to list environments for a given tenant (admin only).' ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog data = client.environments.list( parsed_args.all_tenants, parsed_args.tenant) columns = ('id', 'name', 'status', 'created', 'updated') column_headers = [c.capitalize() for c in columns] return ( column_headers, list(utils.get_item_properties( s, columns, ) for s in data) ) class ShowEnvironment(command.ShowOne): """Display environment details""" def get_parser(self, prog_name): parser = super(ShowEnvironment, self).get_parser(prog_name) parser.add_argument( "id", metavar="", help=("Name or ID of the environment to display"), ) parser.add_argument( "--only-apps", action='store_true', default=False, help="Only print apps of the environment (useful for automation).", ) parser.add_argument( "--session-id", metavar="", default='', help="Id of a config session.", ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog environment = utils.find_resource(client.environments, parsed_args.id) data = client.environments.get(environment.id, parsed_args.session_id).to_dict() data['services'] = jsonutils.dumps(data['services'], indent=2) if getattr(parsed_args, 'only_apps', False): return(['services'], [data['services']]) else: return self.dict2columns(data) class RenameEnvironment(command.Lister): """Rename an environment.""" def get_parser(self, prog_name): parser = super(RenameEnvironment, self).get_parser(prog_name) parser.add_argument( 'id', metavar="", help="Environment ID or name.", ) parser.add_argument( 'name', metavar="", help="A name to which the environment will be renamed.", ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog environment = utils.find_resource(client.environments, parsed_args.id) data = client.environments.update(environment.id, parsed_args.name) columns = ('id', 'name', 'status', 'created', 'updated') column_headers = [c.capitalize() for c in columns] return ( column_headers, [utils.get_item_properties( data, columns, )] ) class EnvironmentSessionCreate(command.ShowOne): """Creates a new configuration session for environment ID.""" def get_parser(self, prog_name): parser = super(EnvironmentSessionCreate, self).get_parser(prog_name) parser.add_argument( 'id', metavar="", help="ID of Environment to add session to.", ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog environment_id = parsed_args.id session_id = client.sessions.configure(environment_id).id sessionid = murano_utils.text_wrap_formatter(session_id) return (['id'], [sessionid]) class EnvironmentCreate(command.Lister): """Create an environment.""" def get_parser(self, prog_name): parser = super(EnvironmentCreate, self).get_parser(prog_name) parser.add_argument( 'name', metavar="", help="Environment name.", ) parser.add_argument( '--region', metavar="", help="Name of the target OpenStack region.", ) parser.add_argument( '--join-subnet-id', metavar="", help="Subnetwork id to join.", ) parser.add_argument( '--join-net-id', metavar="", help="Network id to join.", ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog body = {"name": parsed_args.name, "region": parsed_args.region} if parsed_args.join_net_id or parsed_args.join_subnet_id: res = { 'defaultNetworks': { 'environment': { '?': { 'id': uuidutils.generate_uuid(dashed=False), 'type': 'io.murano.resources.ExistingNeutronNetwork' }, }, 'flat': None } } if parsed_args.join_net_id: res['defaultNetworks']['environment']['internalNetworkName'] \ = parsed_args.join_net_id if parsed_args.join_subnet_id: res['defaultNetworks']['environment']['internalSubnetworkName' ] = \ parsed_args.join_subnet_id body.update(res) data = client.environments.create(body) columns = ('id', 'name', 'status', 'created', 'updated') column_headers = [c.capitalize() for c in columns] return ( column_headers, [utils.get_item_properties( data, columns, )] ) class EnvironmentDelete(command.Lister): """Delete an environment.""" def get_parser(self, prog_name): parser = super(EnvironmentDelete, self).get_parser(prog_name) parser.add_argument( 'id', metavar="", nargs="+", help="Id or name of environment(s) to delete.", ) parser.add_argument( '--abandon', action='store_true', default=False, help="If set will abandon environment without deleting any of its" " resources.", ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog abandon = getattr(parsed_args, 'abandon', False) failure_count = 0 for environment_id in parsed_args.id: try: environment = murano_utils.find_resource(client.environments, environment_id) client.environments.delete(environment.id, abandon) except exceptions.NotFound: failure_count += 1 print("Failed to delete '{0}'; environment not found". format(environment_id)) if failure_count == len(parsed_args.id): raise exceptions.CommandError("Unable to find and delete any of " "the specified environments.") data = client.environments.list() columns = ('id', 'name', 'status', 'created', 'updated') column_headers = [c.capitalize() for c in columns] return ( column_headers, list(utils.get_item_properties( s, columns, ) for s in data) ) class EnvironmentDeploy(command.ShowOne): """Start deployment of a murano environment session.""" def get_parser(self, prog_name): parser = super(EnvironmentDeploy, self).get_parser(prog_name) parser.add_argument( 'id', metavar="", help="ID of Environment to deploy.", ) parser.add_argument( '--session-id', metavar="", help="ID of configuration session to deploy.", ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog client.sessions.deploy(parsed_args.id, parsed_args.session_id) environment = utils.find_resource(client.environments, parsed_args.id) data = client.environments.get(environment.id, parsed_args.session_id).to_dict() data['services'] = jsonutils.dumps(data['services'], indent=2) if getattr(parsed_args, 'only_apps', False): return(['services'], [data['services']]) else: return self.dict2columns(data) class EnvironmentAppsEdit(command.Command): """Edit environment's services list. `FILE` is path to a file, that contains jsonpatch, that describes changes to be made to environment's object-model. [ { "op": "add", "path": "/-", "value": { ... your-app object model here ... } }, { "op": "replace", "path": "/0/?/name", "value": "new_name" }, ] NOTE: Values '===id1===', '===id2===', etc. in the resulting object-model will be substituted with uuids. For more info on jsonpatch see RFC 6902 """ def get_parser(self, prog_name): parser = super(EnvironmentAppsEdit, self).get_parser(prog_name) parser.add_argument( 'id', metavar="", help="ID of Environment to edit.", ) parser.add_argument( 'filename', metavar="", help="File to read jsonpatch from (defaults to stdin).", ) parser.add_argument( '--session-id', metavar="", help="ID of configuration session to edit.", ) return parser def take_action(self, parsed_args): LOG.debug("take_action(%s)", parsed_args) client = self.app.client_manager.application_catalog jp_obj = None if not parsed_args.filename: jp_obj = json.load(sys.stdin) else: with open(parsed_args.filename) as fpatch: jp_obj = json.load(fpatch) jpatch = jsonpatch.JsonPatch(jp_obj) environment_id = parsed_args.id session_id = parsed_args.session_id environment = client.environments.get(environment_id, session_id) object_model = jpatch.apply(environment.services) murano_utils.traverse_and_replace(object_model) client.services.put( environment_id, path='/', data=jpatch.apply(environment.services), session_id=session_id) class EnvironmentModelShow(command.ShowOne): """Show environment's object model.""" def get_parser(self, prog_name): parser = super(EnvironmentModelShow, self).get_parser(prog_name) parser.add_argument( 'id', metavar="", help="ID of Environment to show.", ) parser.add_argument( "--path", metavar="", default='/', help="Path to Environment model section. Defaults to '/'." ) parser.add_argument( '--session-id', metavar="", help="Id of a config session.", ) return parser def take_action(self, parsed_args): LOG.debug("take_action(%s)", parsed_args) client = self.app.client_manager.application_catalog session_id = parsed_args.session_id or None path = urllib.parse.quote(parsed_args.path) env_model = client.environments.get_model(parsed_args.id, path, session_id) return self.dict2columns(env_model) class EnvironmentModelEdit(command.ShowOne): """Edit environment's object model.""" def get_parser(self, prog_name): parser = super(EnvironmentModelEdit, self).get_parser(prog_name) parser.add_argument( 'id', metavar="", help="ID of Environment to edit.", ) parser.add_argument( "filename", metavar="", nargs="?", help="File to read JSON-patch from (defaults to stdin)." ) parser.add_argument( '--session-id', metavar="", help="Id of a config session.", ) return parser def take_action(self, parsed_args): LOG.debug("take_action(%s)", parsed_args) client = self.app.client_manager.application_catalog jp_obj = None if not parsed_args.filename: jp_obj = json.load(sys.stdin) else: with open(parsed_args.filename) as fpatch: jp_obj = json.load(fpatch) if not isinstance(jp_obj, list): raise exceptions.CommandError( 'JSON-patch must be a list of changes') for change in jp_obj: if 'op' not in change or 'path' not in change: raise exceptions.CommandError( 'Every change in JSON-patch must contain "op" and "path" ' 'keys') op = change['op'] if op not in ['add', 'replace', 'remove']: raise exceptions.CommandError('The value of "op" item must be ' '"add", "replace" or "remove", ' 'got {0}'.format(op)) if op != 'remove' and 'value' not in change: raise exceptions.CommandError('"add" or "replace" change in ' 'JSON-patch must contain "value"' ' key') session_id = parsed_args.session_id new_model = client.environments.update_model(parsed_args.id, jp_obj, session_id) return self.dict2columns(new_model) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/osc/v1/package.py0000664000175000017500000007071400000000000023353 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Application-catalog v1 package action implementation""" import collections import functools import itertools import os import shutil import sys import tempfile import zipfile from osc_lib.command import command from osc_lib import exceptions as exc from osc_lib import utils from oslo_log import log as logging from oslo_serialization import jsonutils from oslo_utils import strutils from muranoclient.apiclient import exceptions from muranoclient.common import exceptions as common_exceptions from muranoclient.common import utils as murano_utils from muranoclient.v1.package_creator import hot_package from muranoclient.v1.package_creator import mpl_package LOG = logging.getLogger(__name__) DEFAULT_REPO_URL = "http://apps.openstack.org/api/v1/murano_repo/liberty/" _bool_from_str_strict = functools.partial( strutils.bool_from_string, strict=True) class CreatePackage(command.Command): """Create an application package.""" def get_parser(self, prog_name): parser = super(CreatePackage, self).get_parser(prog_name) parser.add_argument( '-t', '--template', metavar='', help=("Path to the Heat template to import as " "an Application Definition."), ) parser.add_argument( '-c', '--classes-dir', metavar='', help=("Path to the directory containing application classes."), ) parser.add_argument( '-r', '--resources-dir', metavar='', help=("Path to the directory containing application resources."), ) parser.add_argument( '-n', '--name', metavar='', help=("Display name of the Application in Catalog."), ) parser.add_argument( '-f', '--full-name', metavar='', help=("Fully-qualified name of the Application in Catalog."), ) parser.add_argument( '-a', '--author', metavar='', help=("Name of the publisher."), ) parser.add_argument( '--tags', metavar='', nargs='*', help=("A list of keywords connected to the application."), ) parser.add_argument( '-d', '--description', metavar='', help=("Detailed description for the Application in Catalog."), ) parser.add_argument( '-o', '--output', metavar='', help=("The name of the output file archive to save locally."), ) parser.add_argument( '-u', '--ui', metavar='', help=("Dynamic UI form definition."), ) parser.add_argument( '--type', metavar='', help=("Package type. Possible values: Application or Library."), ) parser.add_argument( '-l', '--logo', metavar='', help=("Path to the package logo."), ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) parsed_args.os_username = os.getenv('OS_USERNAME') def _make_archive(archive_name, path): zip_file = zipfile.ZipFile(archive_name, 'w') for root, dirs, files in os.walk(path): for f in files: zip_file.write(os.path.join(root, f), arcname=os.path.join( os.path.relpath(root, path), f)) if parsed_args.template and parsed_args.classes_dir: raise exc.CommandError( "Provide --template for a HOT-based package, OR" " --classes-dir for a MuranoPL-based package") if not parsed_args.template and not parsed_args.classes_dir: raise exc.CommandError( "Provide --template for a HOT-based package, OR at least" " --classes-dir for a MuranoPL-based package") directory_path = None try: archive_name = parsed_args.output if parsed_args.output else None if parsed_args.template: directory_path = hot_package.prepare_package(parsed_args) if not archive_name: archive_name = os.path.basename(parsed_args.template) archive_name = os.path.splitext(archive_name)[0] + ".zip" else: directory_path = mpl_package.prepare_package(parsed_args) if not archive_name: archive_name = tempfile.mkstemp( prefix="murano_", dir=os.getcwd())[1] + ".zip" _make_archive(archive_name, directory_path) print("Application package is available at " + os.path.abspath(archive_name)) finally: if directory_path: shutil.rmtree(directory_path) class ListPackages(command.Lister): """List available packages.""" def get_parser(self, prog_name): parser = super(ListPackages, self).get_parser(prog_name) parser.add_argument( "--limit", type=int, default=0, help='Show limited number of packages' ) parser.add_argument( "--marker", default='', help='Show packages starting from package with id excluding it' ) parser.add_argument( "--include-disabled", default=False, action="store_true" ) parser.add_argument( "--owned", default=False, action="store_true" ) parser.add_argument( '--search', metavar='', dest='search', required=False, help='Show packages, that match search keys fuzzily' ) parser.add_argument( '--name', metavar='', dest='name', required=False, help='Show packages, whose name match parameter exactly' ) parser.add_argument( '--fqn', metavar="", dest='fqn', required=False, help='Show packages, ' 'whose fully qualified name match parameter exactly' ) parser.add_argument( '--type', metavar='', dest='type', required=False, help='Show packages, whose type match parameter exactly' ) parser.add_argument( '--category', metavar='', dest='category', required=False, help='Show packages, whose categories include parameter' ) parser.add_argument( '--class_name', metavar='', dest='class_name', required=False, help='Show packages, whose class name match parameter exactly' ) parser.add_argument( '--tag', metavar='', dest='tag', required=False, help='Show packages, whose tags include parameter' ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog filter_args = { "include_disabled": getattr(parsed_args, 'include_disabled', False), "owned": getattr(parsed_args, 'owned', False), } if parsed_args: if parsed_args.limit < 0: raise exceptions.CommandError( '--limit parameter must be non-negative') if parsed_args.limit != 0: filter_args['limit'] = parsed_args.limit if parsed_args.marker: filter_args['marker'] = parsed_args.marker if parsed_args.search: filter_args['search'] = parsed_args.search if parsed_args.name: filter_args['name'] = parsed_args.name if parsed_args.fqn: filter_args['fqn'] = parsed_args.fqn if parsed_args.type: filter_args['type'] = parsed_args.type if parsed_args.category: filter_args['category'] = parsed_args.category if parsed_args.class_name: filter_args['class_name'] = parsed_args.class_name if parsed_args.tag: filter_args['tag'] = parsed_args.tag data = client.packages.filter(**filter_args) columns = ('id', 'name', 'fully_qualified_name', 'author', 'active', 'is public', 'type', 'version') column_headers = [c.capitalize() for c in columns] if not parsed_args or parsed_args.limit == 0: return ( column_headers, list(utils.get_item_properties( s, columns, ) for s in data) ) else: return ( column_headers, list(utils.get_item_properties( s, columns, ) for s in itertools.islice(data, parsed_args.limit)) ) class DeletePackage(command.Lister): """Delete a package.""" def get_parser(self, prog_name): parser = super(DeletePackage, self).get_parser(prog_name) parser.add_argument( 'id', metavar="", nargs="+", help="Package ID to delete.", ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog failure_count = 0 for package_id in parsed_args.id: try: client.packages.delete(package_id) except exceptions.NotFound: failure_count += 1 print("Failed to delete '{0}'; package not found". format(package_id)) if failure_count == len(parsed_args.id): raise exceptions.CommandError("Unable to find and delete any of " "the specified packages.") data = client.packages.filter() columns = ('id', 'name', 'fully_qualified_name', 'author', 'active', 'is public', 'type', 'version') column_headers = [c.capitalize() for c in columns] return ( column_headers, list(utils.get_item_properties( s, columns, ) for s in data) ) def _handle_package_exists(mc, data, package, exists_action): name = package.manifest['FullName'] version = package.manifest.get('Version', '0') while True: print("Importing package {0}".format(name)) try: return mc.packages.create(data, {name: package.file()}) except common_exceptions.HTTPConflict: print("Importing package {0} failed. Package with the same" " name/classes is already registered.".format(name)) allowed_results = ['s', 'u', 'a'] res = exists_action if not res: while True: print("What do you want to do? (s)kip, (u)pdate, (a)bort") res = input() if res in allowed_results: break if res == 's': print("Skipping.") return None elif res == 'a': print("Exiting.") sys.exit() elif res == 'u': pkgs = list(mc.packages.filter(fqn=name, version=version, owned=True)) if not pkgs: msg = ( "Got a conflict response, but could not find the " "package '{0}' in the current tenant.\nThis probably " "means the conflicting package is in another tenant.\n" "Please delete it manually." ).format(name) raise exceptions.CommandError(msg) elif len(pkgs) > 1: msg = ( "Got {0} packages with name '{1}'.\nI do not trust " "myself, please delete the package manually." ).format(len(pkgs), name) raise exceptions.CommandError(msg) print("Deleting package {0}({1})".format(name, pkgs[0].id)) mc.packages.delete(pkgs[0].id) continue class ImportPackage(command.Lister): """Import a package.""" def get_parser(self, prog_name): parser = super(ImportPackage, self).get_parser(prog_name) parser.add_argument( 'filename', metavar='', nargs='+', help='URL of the murano zip package, FQPN, path to zip package' ' or path to directory with package.' ) parser.add_argument( '--categories', metavar='', nargs='*', help='Category list to attach.', ) parser.add_argument( '--is-public', action='store_true', default=False, help="Make the package available for users from other tenants.", ) parser.add_argument( '--package-version', default='', help='Version of the package to use from repository ' '(ignored when importing with multiple packages).' ) parser.add_argument( '--exists-action', default='', choices=['a', 's', 'u'], help='Default action when a package already exists: ' '(s)kip, (u)pdate, (a)bort.' ) parser.add_argument( '--dep-exists-action', default='', choices=['a', 's', 'u'], help='Default action when a dependency package already exists: ' '(s)kip, (u)pdate, (a)bort.' ) parser.add_argument('--murano-repo-url', default=murano_utils.env( 'MURANO_REPO_URL', default=DEFAULT_REPO_URL), help=('Defaults to env[MURANO_REPO_URL] ' 'or {0}'.format(DEFAULT_REPO_URL))) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog data = {"is_public": parsed_args.is_public} version = parsed_args.package_version if version and len(parsed_args.filename) >= 2: print("Requested to import more than one package, " "ignoring version.") version = '' if parsed_args.categories: data["categories"] = parsed_args.categories total_reqs = collections.OrderedDict() main_packages_names = [] for filename in parsed_args.filename: if os.path.isfile(filename) or os.path.isdir(filename): _file = filename else: print("Package file '{0}' does not exist, attempting to " "download".format(filename)) _file = murano_utils.to_url( filename, version=version, base_url=parsed_args.murano_repo_url, extension='.zip', path='apps/', ) try: package = murano_utils.Package.from_file(_file) except Exception as e: print("Failed to create package for '{0}', reason: {1}".format( filename, e)) continue total_reqs.update( package.requirements(base_url=parsed_args.murano_repo_url)) main_packages_names.append(package.manifest['FullName']) imported_list = [] dep_exists_action = parsed_args.dep_exists_action if dep_exists_action == '': dep_exists_action = parsed_args.exists_action for name, package in iter(total_reqs.items()): image_specs = package.images() if image_specs: print("Inspecting required images") try: imgs = murano_utils.ensure_images( glance_client=client.glance_client, image_specs=image_specs, base_url=parsed_args.murano_repo_url, is_package_public=parsed_args.is_public) for img in imgs: print("Added {0}, {1} image".format( img['name'], img['id'])) except Exception as e: print("Error {0} occurred while installing " "images for {1}".format(e, name)) if name in main_packages_names: exists_action = parsed_args.exists_action else: exists_action = dep_exists_action try: imported_package = _handle_package_exists( client, data, package, exists_action) if imported_package: imported_list.append(imported_package) except Exception as e: print("Error {0} occurred while installing package {1}".format( e, name)) columns = ('id', 'name', 'fully_qualified_name', 'author', 'active', 'is public', 'type', 'version') column_headers = [c.capitalize() for c in columns] return ( column_headers, list(utils.get_item_properties( s, columns, ) for s in imported_list) ) class ImportBundle(command.Lister): """Import a bundle.""" def get_parser(self, prog_name): parser = super(ImportBundle, self).get_parser(prog_name) parser.add_argument( 'filename', metavar='', nargs='+', help='Bundle URL, bundle name, or path to the bundle file.' ) parser.add_argument( '--is-public', action='store_true', default=False, help="Make the package available for users from other tenants.", ) parser.add_argument( '--exists-action', default='', choices=['a', 's', 'u'], help='Default action when a package already exists: ' '(s)kip, (u)pdate, (a)bort.' ) parser.add_argument('--murano-repo-url', default=murano_utils.env( 'MURANO_REPO_URL', default=DEFAULT_REPO_URL), help=('Defaults to env[MURANO_REPO_URL] ' 'or {0}'.format(DEFAULT_REPO_URL))) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog total_reqs = collections.OrderedDict() for filename in parsed_args.filename: local_path = None if os.path.isfile(filename): _file = filename local_path = os.path.dirname(os.path.abspath(filename)) else: print("Bundle file '{0}' does not exist, attempting " "to download".format(filename)) _file = murano_utils.to_url( filename, base_url=parsed_args.murano_repo_url, path='bundles/', extension='.bundle', ) try: bundle_file = murano_utils.Bundle.from_file(_file) except Exception as e: print("Failed to create bundle for '{0}', reason: {1}".format( filename, e)) continue data = {"is_public": parsed_args.is_public} for package in bundle_file.packages( base_url=parsed_args.murano_repo_url, path=local_path): requirements = package.requirements( base_url=parsed_args.murano_repo_url, path=local_path, ) total_reqs.update(requirements) imported_list = [] for name, dep_package in total_reqs.items(): image_specs = dep_package.images() if image_specs: print("Inspecting required images") try: imgs = parsed_args.ensure_images( glance_client=client.glance_client, image_specs=image_specs, base_url=parsed_args.murano_repo_url, local_path=local_path, is_package_public=parsed_args.is_public) for img in imgs: print("Added {0}, {1} image".format( img['name'], img['id'])) except Exception as e: print("Error {0} occurred while installing " "images for {1}".format(e, name)) try: imported_package = _handle_package_exists( client, data, dep_package, parsed_args.exists_action) if imported_package: imported_list.append(imported_package) except exceptions.CommandError: raise except Exception as e: print("Error {0} occurred while " "installing package {1}".format(e, name)) columns = ('id', 'name', 'fully_qualified_name', 'author', 'active', 'is public', 'type', 'version') column_headers = [c.capitalize() for c in columns] return ( column_headers, list(utils.get_item_properties( s, columns, ) for s in imported_list) ) class ShowPackage(command.ShowOne): """Display details for a package.""" def get_parser(self, prog_name): parser = super(ShowPackage, self).get_parser(prog_name) parser.add_argument( "id", metavar="", help=("Package ID to show."), ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog try: package = client.packages.get(parsed_args.id) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Package with id %s not " "found" % parsed_args.id) else: to_display = dict( id=package.id, type=package.type, owner_id=package.owner_id, name=package.name, fully_qualified_name=package.fully_qualified_name, is_public=package.is_public, enabled=package.enabled, class_definitions=jsonutils.dumps(package.class_definitions, indent=2), categories=jsonutils.dumps(package.categories, indent=2), tags=jsonutils.dumps(package.tags, indent=2), description=package.description ) return self.dict2columns(to_display) class UpdatePackage(command.ShowOne): """Update an existing package.""" def get_parser(self, prog_name): parser = super(UpdatePackage, self).get_parser(prog_name) parser.add_argument( 'id', metavar="", help="Package ID to update.", ) parser.add_argument( '--is-public', type=_bool_from_str_strict, metavar="{true|false}", help="Make package available to users from other tenants.", ) parser.add_argument( '--enabled', type=_bool_from_str_strict, metavar="{true|false}", help="Make package active and available for deployments.", ) parser.add_argument( '--name', default=None, help="New name for the package.", ) parser.add_argument( '--description', default=None, help="New package description.", ) parser.add_argument( '--tags', metavar='', nargs='*', default=None, help="A list of keywords connected to the application.", ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog data = {} parameters = ('is_public', 'enabled', 'name', 'description', 'tags') for parameter in parameters: param_value = getattr(parsed_args, parameter, None) if param_value is not None: data[parameter] = param_value _, package = client.packages.update(parsed_args.id, data) to_display = dict( id=package["id"], type=package["type"], owner_id=package["owner_id"], name=package["name"], fully_qualified_name=package["fully_qualified_name"], is_public=package["is_public"], enabled=package["enabled"], class_definitions=jsonutils.dumps(package["class_definitions"], indent=2), categories=jsonutils.dumps(package["categories"], indent=2), tags=jsonutils.dumps(package["tags"], indent=2), description=package["description"] ) return self.dict2columns(to_display) class DownloadPackage(command.Command): """Download a package to a filename or stdout.""" def get_parser(self, prog_name): parser = super(DownloadPackage, self).get_parser(prog_name) parser.add_argument( "id", metavar="", help=("Package ID to download."), ) parser.add_argument( "filename", metavar="file", nargs="?", help=("Filename to save package to. If it is not " "specified and there is no stdout redirection " "the package won't be saved."), ) return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog def download_to_fh(package_id, fh): try: fh.write(client.packages.download(package_id)) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Package with id %s not " "found" % parsed_args.id) if parsed_args.filename: with open(parsed_args.filename, 'wb') as fh: download_to_fh(parsed_args.id, fh) print("Package downloaded to %s" % parsed_args.filename) elif not sys.stdout.isatty(): download_to_fh(parsed_args.id, sys.stdout) else: msg = ("No stdout redirection or local file specified for " "downloaded package. Please specify a local file to " "save downloaded package or redirect output to " "another source.") raise exceptions.CommandError(msg) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/osc/v1/schema.py0000664000175000017500000000356200000000000023215 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """Application-catalog v1 class-schema action implementation""" from osc_lib.command import command from oslo_log import log as logging LOG = logging.getLogger(__name__) class ShowSchema(command.ShowOne): """Show class schema.""" def get_parser(self, prog_name): parser = super(ShowSchema, self).get_parser(prog_name) parser.add_argument( "class_name", metavar="", help="Class FQN") parser.add_argument( "method_names", metavar="", help="Method name", nargs='*') parser.add_argument( "--package-name", default=None, help="FQN of the package where the class is located") parser.add_argument( "--class-version", default='=0', help="Class version or version range (version spec)") return parser def take_action(self, parsed_args): LOG.debug("take_action({0})".format(parsed_args)) client = self.app.client_manager.application_catalog schema = client.schemas.get( parsed_args.class_name, parsed_args.method_names, class_version=parsed_args.class_version, package_name=parsed_args.package_name) return self.dict2columns(schema.data) @property def formatter_default(self): return 'json' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/shell.py0000664000175000017500000005341600000000000021755 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. """ Command-line interface to the Murano Project. """ import argparse import sys import glanceclient from keystoneclient.auth.identity.generic.cli import DefaultCLI from keystoneclient.auth.identity import v3 as identity from keystoneclient import discover from keystoneclient import exceptions as ks_exc from keystoneclient import session as ksession from oslo_log import handlers from oslo_log import log as logging from oslo_utils import encodeutils from oslo_utils import importutils import urllib.parse as urlparse import muranoclient from muranoclient.apiclient import exceptions as exc from muranoclient import client as murano_client from muranoclient.common import utils from muranoclient.glance import client as art_client logger = logging.getLogger(__name__) DEFAULT_REPO_URL = "http://apps.openstack.org/api/v1/murano_repo/liberty/" # quick local fix for keystoneclient bug which blocks built-in reauth # functionality in case of expired token. # bug: https://bugs.launchpad.net/python-keystoneclient/+bug/1551392 # fix: https://review.opendev.org/#/c/286236/ class AuthCLI(DefaultCLI): def invalidate(self): retval = super(AuthCLI, self).invalidate() if self._token: self._token = None retval = True return retval class MuranoShell(object): def _append_global_identity_args(self, parser): # Register the CLI arguments that have moved to the session object. ksession.Session.register_cli_options(parser) identity.Password.register_argparse_arguments(parser) def get_base_parser(self, argv): parser = argparse.ArgumentParser( prog='murano', description=__doc__.strip(), epilog='See "murano help COMMAND" ' 'for help on a specific command.', add_help=False, formatter_class=HelpFormatter, ) # Global arguments parser.add_argument('-h', '--help', action='store_true', help=argparse.SUPPRESS, ) parser.add_argument('--version', action='version', version=muranoclient.__version__, help="Show program's version number and exit.") parser.add_argument('-d', '--debug', default=bool(utils.env('MURANOCLIENT_DEBUG')), action='store_true', help='Defaults to env[MURANOCLIENT_DEBUG].') parser.add_argument('-v', '--verbose', default=False, action="store_true", help="Print more verbose output.") parser.add_argument('--api-timeout', help='Number of seconds to wait for an ' 'API response, ' 'defaults to system socket timeout.') parser.add_argument('--os-tenant-id', default=utils.env('OS_TENANT_ID'), help='Defaults to env[OS_TENANT_ID].') parser.add_argument('--os-tenant-name', default=utils.env('OS_TENANT_NAME'), help='Defaults to env[OS_TENANT_NAME].') parser.add_argument('--os-region-name', default=utils.env('OS_REGION_NAME'), help='Defaults to env[OS_REGION_NAME].') parser.add_argument('--os-auth-token', default=utils.env('OS_AUTH_TOKEN'), help='Defaults to env[OS_AUTH_TOKEN].') parser.add_argument('--os-no-client-auth', default=utils.env('OS_NO_CLIENT_AUTH'), action='store_true', help="Do not contact keystone for a token. " "Defaults to env[OS_NO_CLIENT_AUTH].") parser.add_argument('--murano-url', default=utils.env('MURANO_URL'), help='Defaults to env[MURANO_URL].') parser.add_argument('--glance-url', default=utils.env('GLANCE_URL'), help='Defaults to env[GLANCE_URL].') parser.add_argument('--glare-url', default=utils.env('GLARE_URL'), help='Defaults to env[GLARE_URL].') parser.add_argument('--murano-api-version', default=utils.env( 'MURANO_API_VERSION', default='1'), help='Defaults to env[MURANO_API_VERSION] ' 'or 1.') parser.add_argument('--os-service-type', default=utils.env('OS_SERVICE_TYPE'), help='Defaults to env[OS_SERVICE_TYPE].') parser.add_argument('--os-endpoint-type', default=utils.env('OS_ENDPOINT_TYPE'), help='Defaults to env[OS_ENDPOINT_TYPE].') parser.add_argument('--include-password', default=bool(utils.env('MURANO_INCLUDE_PASSWORD')), action='store_true', help='Send os-username and os-password to murano.') parser.add_argument('--murano-repo-url', default=utils.env( 'MURANO_REPO_URL', default=DEFAULT_REPO_URL), help=('Defaults to env[MURANO_REPO_URL] ' 'or {0}'.format(DEFAULT_REPO_URL))) parser.add_argument('--murano-packages-service', choices=['murano', 'glance', 'glare'], default=utils.env('MURANO_PACKAGES_SERVICE', default='murano'), help='Specifies if murano-api ("murano") or ' 'Glance Artifact Repository ("glare") ' 'should be used to store murano packages. ' 'Defaults to env[MURANO_PACKAGES_SERVICE] or ' 'to "murano"') # The following 3 arguments are deprecated and are all added # by keystone session register_cli_opts later. Only add these # arguments if they are present on the command line. if '--cert-file' in argv: parser.add_argument('--cert-file', dest='os_cert', help='DEPRECATED! Use --os-cert.') if '--key-file' in argv: parser.add_argument('--key-file', dest='os_key', help='DEPRECATED! Use --os-key.') if '--ca-file' in argv: parser.add_argument('--ca-file', dest='os_cacert', help='DEPRECATED! Use --os-cacert.') self._append_global_identity_args(parser) return parser def get_subcommand_parser(self, version, argv): parser = self.get_base_parser(argv) self.subcommands = {} subparsers = parser.add_subparsers(metavar='') submodule = importutils.import_versioned_module('muranoclient', version, 'shell') self._find_actions(subparsers, submodule) self._find_actions(subparsers, self) return parser def _find_actions(self, subparsers, actions_module): for attr in (a for a in dir(actions_module) if a.startswith('do_')): # I prefer to be hypen-separated instead of underscores. command = attr[3:].replace('_', '-') callback = getattr(actions_module, attr) desc = callback.__doc__ or '' help = desc.strip().split('\n')[0] arguments = getattr(callback, 'arguments', []) subparser = subparsers.add_parser(command, help=help, description=desc, add_help=False, formatter_class=HelpFormatter) subparser.add_argument('-h', '--help', action='help', help=argparse.SUPPRESS) self.subcommands[command] = subparser for (args, kwargs) in arguments: subparser.add_argument(*args, **kwargs) subparser.set_defaults(func=callback) def _discover_auth_versions(self, session, auth_url): # discover the API versions the server is supporting base on the # given URL v2_auth_url = None v3_auth_url = None try: ks_discover = discover.Discover(session=session, auth_url=auth_url) v2_auth_url = ks_discover.url_for('2.0') v3_auth_url = ks_discover.url_for('3.0') except ks_exc.ClientException as e: # Identity service may not support discover API version. # Lets trying to figure out the API version from the original URL. url_parts = urlparse.urlparse(auth_url) (scheme, netloc, path, params, query, fragment) = url_parts path = path.lower() if path.startswith('/v3'): v3_auth_url = auth_url elif path.startswith('/v2'): v2_auth_url = auth_url else: # not enough information to determine the auth version msg = ('Unable to determine the Keystone version ' 'to authenticate with using the given ' 'auth_url. Identity service may not support API ' 'version discovery. Please provide a versioned ' 'auth_url instead. error=%s') % (e) raise exc.CommandError(msg) return (v2_auth_url, v3_auth_url) def _setup_logging(self, debug): # Output the logs to command-line interface color_handler = handlers.ColorHandler(sys.stdout) logger_root = logging.getLogger(None).logger logger_root.level = logging.DEBUG if debug else logging.WARNING logger_root.addHandler(color_handler) # Set the logger level of special library logging.getLogger('iso8601') \ .logger.setLevel(logging.WARNING) logging.getLogger('urllib3.connectionpool') \ .logger.setLevel(logging.WARNING) def main(self, argv): # Parse args once to find version parser = self.get_base_parser(argv) (options, args) = parser.parse_known_args(argv) self._setup_logging(options.debug) # build available subcommands based on version api_version = options.murano_api_version subcommand_parser = self.get_subcommand_parser(api_version, argv) self.parser = subcommand_parser keystone_session = None keystone_auth = None # Handle top-level --help/-h before attempting to parse # a command off the command line. if (not args and options.help) or not argv: self.do_help(options) return 0 # Parse args again and call whatever callback was selected. args = subcommand_parser.parse_args(argv) # Short-circuit and deal with help command right away. if args.func == self.do_help: self.do_help(args) return 0 elif args.func == self.do_bash_completion: self.do_bash_completion(args) return 0 if not args.os_username and not args.os_auth_token: raise exc.CommandError("You must provide a username via" " either --os-username or env[OS_USERNAME]" " or a token via --os-auth-token or" " env[OS_AUTH_TOKEN]") if args.murano_packages_service == 'glance': args.murano_packages_service = 'glare' if args.os_no_client_auth: if not args.murano_url: raise exc.CommandError( "If you specify --os-no-client-auth" " you must also specify a Murano API URL" " via either --murano-url or env[MURANO_URL]") if (not args.glare_url and args.murano_packages_service == 'glare'): raise exc.CommandError( "If you specify --os-no-client-auth and" " set murano-packages-service to 'glare'" " you must also specify a glare API URL" " via either --glare-url or env[GLARE_API]") if (not any([args.os_tenant_id, args.os_project_id]) and args.murano_packages_service == 'glare'): # TODO(kzaitsev): see if we can use project's name here # NOTE(kzaitsev): glare v0.1 needs project_id to operate # correctly raise exc.CommandError( "If you specify --os-no-client-auth and" " set murano-packages-service to 'glare'" " you must also specify your project's id" " via either --os-project-id or env[OS_PROJECT_ID] or" " --os-tenant-id or env[OS_TENANT_ID]") else: # Tenant name or ID is needed to make keystoneclient retrieve a # service catalog, it's not required if os_no_client_auth is # specified, neither is the auth URL. if not any([args.os_tenant_name, args.os_tenant_id, args.os_project_id, args.os_project_name]): raise exc.CommandError("You must provide a project name or" " project id via --os-project-name," " --os-project-id, env[OS_PROJECT_ID]" " or env[OS_PROJECT_NAME]. You may" " use os-project and os-tenant" " interchangeably.") if not args.os_auth_url: raise exc.CommandError("You must provide an auth url via" " either --os-auth-url or via" " env[OS_AUTH_URL]") endpoint_type = args.os_endpoint_type or 'publicURL' endpoint = args.murano_url glance_endpoint = args.glance_url if args.os_no_client_auth: # Authenticate through murano, don't use session kwargs = { 'username': args.os_username, 'password': args.os_password, 'auth_token': args.os_auth_token, 'auth_url': args.os_auth_url, 'token': args.os_auth_token, 'insecure': args.insecure, 'timeout': args.api_timeout, 'tenant': args.os_project_id or args.os_tenant_id, } glance_kwargs = kwargs.copy() if args.os_region_name: kwargs['region_name'] = args.os_region_name glance_kwargs['region_name'] = args.os_region_name else: # Create a keystone session and keystone auth keystone_session = ksession.Session.load_from_cli_options(args) args.os_project_name = args.os_project_name or args.os_tenant_name args.os_project_id = args.os_project_id or args.os_tenant_id # make args compatible with DefaultCLI/AuthCLI args.os_token = args.os_auth_token args.os_endpoint = '' # avoid password prompt if no password given args.os_password = args.os_password or '' (v2_auth_url, v3_auth_url) = self._discover_auth_versions( keystone_session, args.os_auth_url) if v3_auth_url: if (not args.os_user_domain_id and not args.os_user_domain_name): args.os_user_domain_name = 'default' if (not args.os_project_domain_id and not args.os_project_domain_name): args.os_project_domain_name = 'default' keystone_auth = AuthCLI.load_from_argparse_arguments(args) service_type = args.os_service_type or 'application-catalog' if not endpoint: endpoint = keystone_auth.get_endpoint( keystone_session, service_type=service_type, interface=endpoint_type, region_name=args.os_region_name) kwargs = { 'session': keystone_session, 'auth': keystone_auth, 'service_type': service_type, 'region_name': args.os_region_name, } glance_kwargs = kwargs.copy() # glance doesn't need endpoint_type kwargs['endpoint_type'] = endpoint_type kwargs['tenant'] = keystone_auth.get_project_id(keystone_session) if args.api_timeout: kwargs['timeout'] = args.api_timeout if not glance_endpoint: try: glance_endpoint = keystone_auth.get_endpoint( keystone_session, service_type='image', interface=endpoint_type, region_name=args.os_region_name) except Exception: pass glance_client = None if glance_endpoint: try: # TODO(starodubcevna): switch back to glance APIv2 when it will # be ready for use. glance_client = glanceclient.Client( '1', glance_endpoint, **glance_kwargs) except Exception: pass if glance_client: kwargs['glance_client'] = glance_client else: logger.warning("Could not initialize glance client. " "Image creation will be unavailable.") kwargs['glance_client'] = None if args.murano_packages_service == 'glare': glare_endpoint = args.glare_url if not glare_endpoint: # no glare_endpoint and we requested to store packages in glare # let's check keystone try: glare_endpoint = keystone_auth.get_endpoint( keystone_session, service_type='artifact', interface=endpoint_type, region_name=args.os_region_name) except Exception: raise exc.CommandError( "You set murano-packages-service to {}" " but there is not 'artifact' endpoint in keystone" " Either register one or specify endpoint " " via either --glare-url or env[GLARE_API]".format( args.murano_packages_service)) auth_token = \ args.os_auth_token or keystone_auth.get_token(keystone_session) artifacts_client = art_client.Client(endpoint=glare_endpoint, type_name='murano', type_version=1, token=auth_token, insecure=args.insecure) kwargs['artifacts_client'] = artifacts_client client = murano_client.Client(api_version, endpoint, **kwargs) args.func(client, args) def do_bash_completion(self, args): """Prints all of the commands and options to stdout.""" commands = set() options = set() for sc_str, sc in self.subcommands.items(): commands.add(sc_str) for option in list(sc._optionals._option_string_actions): options.add(option) commands.remove('bash-completion') print(' '.join(commands | options)) @utils.arg('command', metavar='', nargs='?', help='Display help for ') def do_help(self, args): """Display help about this program or one of its subcommands.""" if getattr(args, 'command', None): if args.command in self.subcommands: self.subcommands[args.command].print_help() else: msg = "'%s' is not a valid subcommand" raise exc.CommandError(msg % args.command) else: self.parser.print_help() class HelpFormatter(argparse.HelpFormatter): def start_section(self, heading): # Title-case the headings heading = '%s%s' % (heading[0].upper(), heading[1:]) super(HelpFormatter, self).start_section(heading) def main(args=sys.argv[1:]): try: MuranoShell().main(args) except KeyboardInterrupt: print('... terminating murano client', file=sys.stderr) sys.exit(1) except Exception as e: if '--debug' in args or '-d' in args: raise else: print(encodeutils.safe_encode(str(e)), file=sys.stderr) sys.exit(1) if __name__ == "__main__": main() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5940814 python-muranoclient-2.8.0/muranoclient/tests/0000775000175000017500000000000000000000000021425 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/__init__.py0000664000175000017500000000000000000000000023524 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5940814 python-muranoclient-2.8.0/muranoclient/tests/functional/0000775000175000017500000000000000000000000023567 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/functional/__init__.py0000664000175000017500000000000000000000000025666 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5940814 python-muranoclient-2.8.0/muranoclient/tests/functional/cli/0000775000175000017500000000000000000000000024336 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5780766 python-muranoclient-2.8.0/muranoclient/tests/functional/cli/MockApp/0000775000175000017500000000000000000000000025670 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5940814 python-muranoclient-2.8.0/muranoclient/tests/functional/cli/MockApp/Classes/0000775000175000017500000000000000000000000027265 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/functional/cli/MockApp/Classes/mock_muranopl.yaml0000664000175000017500000000142300000000000033017 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.apps std: io.murano # Name: MockApp # Using Name from manifest.yaml Extends: std:Application Properties: greeting: Usage: Static Contract: $.string() Default: 'Hello, ' Methods: testAction: Usage: Action Body: - sleep(1) - $this.find(std:Environment).reporter.report($this, 'Completed') deploy: Body: - $this.find(std:Environment).reporter.report($this, 'Follow the white rabbit') staticAction: Scope: Public Usage: Static Arguments: - myName: Contract: $.string().notNull() - myAge: Contract: $.int().notNull() Body: - $futureAge: $myAge + 5 - Return: concat($.greeting, $myName, ". In 5 years you will be {0} years old.".format($futureAge)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/functional/cli/__init__.py0000664000175000017500000000000000000000000026435 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/functional/cli/murano_test_utils.py0000664000175000017500000002202000000000000030464 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from http import server as SimpleHTTPServer import json import multiprocessing import os import shutil import socketserver import tempfile import time from oslo_utils import uuidutils from tempest.lib.cli import output_parser from tempest.lib import exceptions from muranoclient.tests.functional.cli import utils from muranoclient.tests.functional import muranoclient class CLIUtilsTestBase(muranoclient.ClientTestBase): """Basic methods for Murano CLI client.""" def delete_murano_object(self, murano_object, obj_to_del): """Delete Murano object Delete Murano object like environment, category or environment-template. """ if obj_to_del not in self.listing('{0}-list'.format(murano_object)): return object_list = self.listing('{0}-delete'.format(murano_object), params=obj_to_del['ID']) start_time = time.time() while obj_to_del in self.listing('{0}-list'.format(murano_object)): if start_time - time.time() > 60: self.fail("{0} is not deleted in 60 seconds". format(murano_object)) return object_list def create_murano_object(self, murano_object, prefix_object_name): """Create Murano object Create Murano object like environment, category or environment-template. """ object_name = self.generate_name(prefix_object_name) mrn_objects = self.listing('{0}-create'.format(murano_object), params=object_name) mrn_object = None for obj in mrn_objects: if object_name == obj['Name']: mrn_object = obj break if mrn_object is None: self.fail("Murano {0} has not been created!".format(murano_object)) self.addCleanup(self.delete_murano_object, murano_object, mrn_object) return mrn_object def create_murano_object_parameter(self, murano_object, prefix_object_name, param): """Create Murano object Create Murano object like environment, category or environment-template. """ object_name = self.generate_name(prefix_object_name) params = '{0} {1}'.format(param, object_name) mrn_objects = self.listing('{0}-create'.format(murano_object), params=params) mrn_object = None for obj in mrn_objects: if object_name == obj['Name']: mrn_object = obj break if mrn_object is None: self.fail("Murano {0} has not been created!".format(murano_object)) self.addCleanup(self.delete_murano_object, murano_object, mrn_object) return mrn_object @staticmethod def generate_uuid(): """Generate uuid for objects.""" return uuidutils.generate_uuid(dashed=False) @staticmethod def generate_name(prefix): """Generate name for objects.""" suffix = CLIUtilsTestBase.generate_uuid()[:8] return "{0}_{1}".format(prefix, suffix) def get_table_struct(self, command, params=""): """Get table structure i.e. header of table.""" return output_parser.table(self.murano(command, params=params))['headers'] def get_object(self, object_list, object_value): """"Get Murano object by value from list of Murano objects.""" for obj in object_list: if object_value in obj.values(): return obj def get_property_value(self, obj, prop): return [o['Value'] for o in obj if o['Property'] == '{0}'.format(prop)][0] class TestSuiteRepository(CLIUtilsTestBase): def setUp(self): super(TestSuiteRepository, self).setUp() self.serve_dir = tempfile.mkdtemp(suffix="repo") self.app_name = self.generate_name("dummy_app") self.dummy_app_path = self._compose_app(name=self.app_name) def tearDown(self): super(TestSuiteRepository, self).tearDown() shutil.rmtree(self.serve_dir) def run_server(self): def serve_function(): class Handler(SimpleHTTPServer.SimpleHTTPRequestHandler): pass os.chdir(self.serve_dir) httpd = socketserver.TCPServer( ("0.0.0.0", 8089), Handler, bind_and_activate=False) httpd.allow_reuse_address = True httpd.server_bind() httpd.server_activate() httpd.serve_forever() self.p = multiprocessing.Process(target=serve_function) self.p.start() def stop_server(self): self.p.terminate() def _compose_app(self, name, require=None): package_dir = os.path.join(self.serve_dir, 'apps/', name) shutil.copytree(os.path.join(os.path.dirname( os.path.realpath(__file__)), 'MockApp'), package_dir) app_name = utils.compose_package( name, os.path.join(package_dir, 'manifest.yaml'), package_dir, require=require, archive_dir=os.path.join(self.serve_dir, 'apps/'), ) return app_name class CLIUtilsTestPackagesBase(TestSuiteRepository): """Basic methods for Murano Packages CLI client.""" def import_package(self, pkg_name, pkg_path, *args): """Create Murano dummy package and import it by url.""" actions = ' '.join(args) params = '{0} {1}'.format(pkg_path, actions) package = self.listing('package-import', params=params) package = self.get_object(package, pkg_name) self.addCleanup(self.delete_murano_object, 'package', package) return package def prepare_file_with_obj_model(self, obj_model): temp_file = tempfile.NamedTemporaryFile(prefix="murano-obj-model", delete=False) self.addCleanup(os.remove, temp_file.name) with open(temp_file.name, 'w') as tf: tf.write(json.dumps([obj_model])) return temp_file.name def wait_deployment_result(self, env_id, timeout=180): start_time = time.time() env = self.listing('environment-show', params=env_id) env_status = self.get_property_value(env, 'status') expected_statuses = ['ready', 'deploying'] while env_status != 'ready': if time.time() - start_time > timeout: msg = ("Environment exceeds timeout {0} to change state " "to Ready. Environment: {1}".format(timeout, env)) raise exceptions.TimeoutException(msg) env = self.listing('environment-show', params=env_id) env_status = self.get_property_value(env, 'status') if env_status not in expected_statuses: msg = ("Environment status %s is not in expected " "statuses: %s" % (env_status, expected_statuses)) raise exceptions.TempestException(msg) time.sleep(2) return True def prepare_bundle_with_non_existed_package(self): temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False) self.addCleanup(os.remove, temp_file.name) with open(temp_file.name, 'w') as tf: tf.write(json.dumps({'Packages': [ {'Name': 'first_app'}, {'Name': 'second_app', 'Version': '1.0'} ]})) return temp_file.name def prepare_bundle_with_invalid_format(self): temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False) self.addCleanup(os.remove, temp_file.name) with open(temp_file.name, 'w') as tf: tf.write('Packages: [{Name: first_app}, {Name: second_app}]') return temp_file.name def deploy_environment(self, env_id, obj_model): session = self.listing('environment-session-create', params=env_id) session_id = self.get_property_value(session, 'id') temp_file = self.prepare_file_with_obj_model(obj_model) self.listing('environment-apps-edit', params='--session-id {0} {1} {2}'. format(session_id, env_id, temp_file)) self.listing('environment-deploy', params='{0} --session-id {1}'. format(env_id, session_id)) result = self.wait_deployment_result(env_id) self.assertTrue(result) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/functional/cli/test_murano.py0000664000175000017500000010100000000000000027240 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import json import unittest from muranoclient.tests.functional.cli import \ murano_test_utils as utils from muranoclient.tests.functional import muranoclient as murano_client from oslo_utils.strutils import bool_from_string as str2bool # TODO(mstolyarenko): need to remove this raw when # https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed backend_name =\ murano_client.ClientTestBase.get_backend_flag().rstrip().split()[-1] class SimpleReadOnlyMuranoClientTest(utils.CLIUtilsTestBase): """Basic, read-only tests for Murano CLI client. Basic smoke test for the Murano CLI commands which do not require creating or modifying murano objects. """ def test_category_list(self): category = self.get_table_struct('category-list') self.assertEqual(['ID', 'Name'], category) def test_env_template_list(self): templates = self.get_table_struct('env-template-list') self.assertEqual(['ID', 'Name', 'Created', 'Updated', 'Is public'], templates) def test_environment_list(self): environment = self.get_table_struct('environment-list') self.assertEqual(['ID', 'Name', 'Status', 'Created', 'Updated'], environment) def test_package_list(self): packages = self.get_table_struct('package-list') self.assertEqual(['ID', 'Name', 'FQN', 'Author', 'Active', 'Is Public', 'Type', 'Version'], packages) class TableStructureMuranoClientTest(utils.CLIUtilsTestBase): """Smoke test for the Murano CLI commands Smoke test for the Murano CLI commands which checks table structure after create or delete category, env-template environment and package. """ def test_table_struct_deployment_list(self): """Test scenario: 1) create environment 2) check table structure """ environment = self.create_murano_object('environment', 'MuranoTestTS-depl-list') table_struct = self.get_table_struct('deployment-list', params=environment['ID']) self.assertEqual(['ID', 'State', 'Created', 'Updated', 'Finished'], table_struct) def test_table_struct_of_environment_create(self): """Test scenario: 1) create environment 2) check table structure """ self.create_murano_object('environment', 'MuranoTestTS-env-create') table_struct = self.get_table_struct('environment-list') self.assertEqual(['ID', 'Name', 'Status', 'Created', 'Updated'], table_struct) def test_table_struct_of_environment_delete(self): """Test scenario: 1) create environment 2) delete environment 3) check table structure """ environment = self.create_murano_object('environment', 'MuranoTestTS-env-del') self.delete_murano_object('environment', environment) table_struct = self.get_table_struct('environment-list') self.assertEqual(['ID', 'Name', 'Status', 'Created', 'Updated'], table_struct) def test_table_struct_of_category_create(self): """Test scenario: 1) create category 2) check table structure """ self.create_murano_object('category', 'MuranoTestTS-cat-create') table_struct = self.get_table_struct('category-list') self.assertEqual(['ID', 'Name'], table_struct) def test_table_struct_of_category_delete(self): """Test scenario: 1) create category 2) delete category 3) check table structure """ category = self.create_murano_object('category', 'MuranoTestTS-cat-create') self.delete_murano_object('category', category) category = self.get_table_struct('category-list') self.assertEqual(['ID', 'Name'], category) def test_table_struct_of_env_template_create(self): """Test scenario: 1) create env_template 2) check table structure """ self.create_murano_object('env-template', 'MuranoTestTS-env-tmp-create') table_struct = self.get_table_struct('env-template-list') self.assertEqual(['ID', 'Name', 'Created', 'Updated', 'Is public'], table_struct) def test_table_struct_of_env_template_delete(self): """Test scenario: 1) create env_template 2) delete env_template 3) check table structure """ env_template = self.create_murano_object('env-template', 'MuranoTestTS-env-tmp-create') self.delete_murano_object('env-template', env_template) table_struct = self.get_table_struct('env-template-list') self.assertEqual(['ID', 'Name', 'Created', 'Updated', 'Is public'], table_struct) class EnvironmentMuranoSanityClientTest(utils.CLIUtilsTestBase): """Sanity tests for testing actions with environment. Smoke test for the Murano CLI commands which checks basic actions with environment command like create, delete, rename etc. """ def test_environment_create(self): """Test scenario: 1) create environment 2) check that created environment exist """ environment = self.create_murano_object('environment', 'TestMuranoSanityEnv') env_list = self.listing('environment-list') # Deleting dates from dictionaries to skip it in assert list(map(lambda x: x.pop('Updated', None), env_list + [environment])) list(map(lambda x: x.pop('Created', None), env_list + [environment])) self.assertIn(environment, env_list) def test_environment_delete(self): """Test scenario: 1) create environment 2) delete environment """ environment = self.create_murano_object('environment', 'TestMuranoSanityEnv') self.delete_murano_object('environment', environment) env_list = self.listing('environment-list') self.assertNotIn(environment, env_list) def test_environment_rename(self): """Test scenario: 1) create environment 2) rename environment """ environment = self.create_murano_object('environment', 'TestMuranoSanityEnv') new_env_name = self.generate_name('TestMuranoSEnv-env-rename') rename_params = "{0} {1}".format(environment['Name'], new_env_name) new_list = self.listing('environment-rename', params=rename_params) renamed_env = self.get_object(new_list, new_env_name) self.addCleanup(self.delete_murano_object, 'environment', renamed_env) new_env_list = self.listing('environment-list') # Deleting dates from dictionaries to skip it in assert list(map(lambda x: x.pop('Updated', None), new_env_list + [environment] + [renamed_env])) list(map(lambda x: x.pop('Created', None), new_env_list + [environment] + [renamed_env])) self.assertIn(renamed_env, new_env_list) self.assertNotIn(environment, new_env_list) def test_table_struct_env_show(self): """Test scenario: 1) create environment 2) check structure of env_show object """ environment = self.create_murano_object('environment', 'TestMuranoSanityEnv') env_show = self.listing('environment-show', params=environment['Name']) # Check structure of env_show object self.assertEqual(['acquired_by', 'created', 'description_text', 'id', 'name', 'services', 'status', 'tenant_id', 'updated', 'version'], list(map(lambda x: x['Property'], env_show))) def test_environment_show(self): """Test scenario: 1) create environment 2) check that env_name, ID, updated and created values exist in env_show object """ environment = self.create_murano_object('environment', 'TestMuranoSanityEnv') env_show = self.listing('environment-show', params=environment['Name']) self.assertIn(environment['Created'], list(map(lambda x: x['Value'], env_show))) self.assertIn(environment['Updated'], list(map(lambda x: x['Value'], env_show))) self.assertIn(environment['Name'], list(map(lambda x: x['Value'], env_show))) self.assertIn(environment['ID'], list(map(lambda x: x['Value'], env_show))) def test_environment_delete_by_id(self): """Test scenario: 1) create environment 2) delete environment by environment ID """ env_name = self.generate_name('TestMuranoSanityEnv') environment = self.create_murano_object('environment', env_name) result = self.murano('environment-delete', params=environment['ID'], fail_ok=False) self.assertNotIn(environment['Name'], result) env_list = self.listing('environment-list') self.assertNotIn(environment, env_list) def test_environment_model_show(self): """Test scenario: 1) create environment 2) check that the result of environment-model-show is a valid non-empty json """ env_name = self.generate_name('TestMuranoSanityEnv') environment = self.create_murano_object('environment', env_name) model = self.murano('environment-model-show', params=environment['ID']) result = json.loads(model) self.assertEqual(4, len(result)) class CategoryMuranoSanityClientTest(utils.CLIUtilsTestBase): """Sanity tests for testing actions with Category. Smoke test for the Murano CLI commands which checks basic actions with category command like create, delete etc. """ def test_category_create(self): """Test scenario: 1) create category 2) check that created category exist """ category = self.create_murano_object('category', 'TestMuranoSanityCategory') category_list = self.listing('category-list') self.assertIn(category, category_list) def test_category_delete(self): """Test scenario: 1) create category 2) delete category 3) check that category has been deleted successfully """ category = self.create_murano_object('category', 'TestMuranoSanityCategory') self.delete_murano_object('category', category) category_list = self.listing('category-list') self.assertNotIn(category, category_list) def test_table_struct_category_show(self): """Test scenario: 1) create category 2) check table structure of category-show object """ category = self.create_murano_object('category', 'TestMuranoSanityCategory') category_show = self.listing('category-show', params=category['ID']) self.assertEqual(['id', 'name', 'packages'], list(map(lambda x: x['Property'], category_show))) def test_category_show(self): """Test scenario: 1) create category 2) check that category values exist in category_show object """ category = self.create_murano_object('category', 'TestMuranoSanityCategory') category_show = self.listing('category-show', params=category['ID']) self.assertIn(category['ID'], list(map(lambda x: x['Value'], category_show))) self.assertIn(category['Name'], list(map(lambda x: x['Value'], category_show))) def test_non_existing_category_delete(self): """Test scenario: 1) try to call category-delete for non existing category 2) check that error message contains user friendly substring """ result = self.murano('category-delete', params='non-existing', fail_ok=True) self.assertIn("Failed to delete 'non-existing'; category not found", result) def test_non_existing_category_show(self): """Test scenario: 1) try to call category-show for non existing category 2) check that error message contains user friendly substring """ result = self.murano('category-show', params='non-existing', fail_ok=True) self.assertIn("Category id 'non-existing' not found", str(result)) def test_category_create_with_long_name(self): """Test scenario: 1) try to create category with long name (>80) 2) check that error message contains user friendly substring """ result = self.murano('category-create', params='name' * 21, fail_ok=True) self.assertIn( "Category name should be 80 characters maximum", result) class EnvTemplateMuranoSanityClientTest(utils.CLIUtilsTestBase): """Sanity tests for testing actions with Environment template. Smoke test for the Murano CLI commands which checks basic actions with env-temlate command like create, delete etc. """ def test_environment_template_create(self): """Test scenario: 1) create environment template 2) check that created environment template exist """ env_template = self.create_murano_object('env-template', 'TestMuranoSanityEnvTemp') env_template_list = self.listing('env-template-list') # Deleting dates from dictionaries to skip it in assert list(map(lambda x: x.pop('Updated', None), env_template_list + [env_template])) list(map(lambda x: x.pop('Created', None), env_template_list + [env_template])) self.assertIn(env_template, env_template_list) def test_environment_template_delete(self): """Test scenario: 1) create environment template 2) delete environment template 3) check that deleted environment template doesn't exist """ env_template = self.create_murano_object('env-template', 'TestMuranoSanityEnvTemp') env_template_list = self.delete_murano_object('env-template', env_template) self.assertNotIn(env_template, env_template_list) def test_table_struct_env_template_show(self): """Test scenario: 1) create environment template 2) check table structure of env-template-show object """ env_template = self.create_murano_object('env-template', 'TestMuranoSanityEnvTemp') env_template_show = self.listing('env-template-show', params=env_template['ID']) tested_env_template = list( map(lambda x: x['Property'], env_template_show)) self.assertIn('created', tested_env_template) self.assertIn('id', tested_env_template) self.assertIn('name', tested_env_template) self.assertIn('services', tested_env_template) self.assertIn('tenant_id', tested_env_template) self.assertIn('updated', tested_env_template) self.assertIn('version', tested_env_template) def test_env_template_show(self): """Test scenario: 1) create environment template 2) check that environment template values exist in env-template-show object """ env_template = self.create_murano_object('env-template', 'TestMuranoSanityEnvTemp') env_template_show = self.listing('env-template-show', params=env_template['ID']) tested_env = list(map(lambda x: x['Value'], env_template_show)) self.assertIn(env_template['ID'], tested_env) self.assertIn(env_template['Name'], tested_env) def test_env_template_create_environment(self): """Test scenario: 1) create environment template 2) create environment from template """ env_template = self.create_murano_object('env-template', 'TestMuranoSanityEnvTemp') new_env_name = self.generate_name('EnvFromTemp') params = "{0} {1}".format(env_template['ID'], new_env_name) env_created = self.listing('env-template-create-env', params=params) tested_env_created = list(map(lambda x: x['Property'], env_created)) self.assertIn('environment_id', tested_env_created) self.assertIn('session_id', tested_env_created) def test_env_template_clone(self): """Test scenario: 1) create environment template 2) clone template 3) check that create environment template has the new name 4) delete new template """ env_template = self.create_murano_object_parameter( 'env-template', 'TestMuranoSanityEnvTemp', '--is-public') new_template = self.generate_name('TestMuranoSanityEnvTemp') params = "{0} {1}".format(env_template['ID'], new_template) template_created = self.listing('env-template-clone', params=params) tp_list = list( map(lambda x: ({x['Property']: x['Value']}), template_created)) result_name = list(filter(lambda x: x.get('name'), tp_list))[0]['name'] result_id = list(filter(lambda x: x.get('id'), tp_list))[0]['id'] self.listing('env-template-delete', params=result_id) self.assertIn(result_name, new_template) class PackageMuranoSanityClientTest(utils.CLIUtilsTestPackagesBase): """Sanity tests for testing actions with Packages. Smoke tests for the Murano CLI commands which check basic actions with packages like import, create, delete etc. """ def test_package_import_by_url(self): """Test scenario: 1) import package by url 2) check that package exists """ try: self.run_server() package = self.import_package( self.app_name, 'http://localhost:8089/apps/{0}.zip'.format(self.app_name) ) finally: self.stop_server() package_list = self.listing('package-list') self.assertIn(package, package_list) def test_package_import_by_path(self): """Test scenario: 1) import package by path 2) check that package exists """ package = self.import_package( self.app_name, self.dummy_app_path ) package_list = self.listing('package-list') self.assertIn(package, package_list) def test_package_is_public(self): """Test scenario: 1) import package 2) check that package is public """ package = self.import_package( self.app_name, self.dummy_app_path, '--is-public') package_show = self.listing('package-show', params=package['ID']) package_show = {item['Property']: item['Value'] for item in package_show} self.assertEqual(package['Is Public'], 'True') self.assertEqual( str2bool(package['Is Public']), str2bool(package_show['is_public'])) def test_package_delete(self): """Test scenario: 1) import package 2) delete package 3) check that package has been deleted """ package = self.import_package( self.app_name, self.dummy_app_path ) package_list = self.delete_murano_object('package', package) self.assertNotIn(package, package_list) def test_package_show(self): """Test scenario: 1) import package 2) check that package values exist in return by package-show object """ package = self.import_package( self.app_name, self.dummy_app_path ) package_show = self.listing('package-show', params=package['ID']) package_show = {item['Property']: item['Value'] for item in package_show} self.assertEqual( str2bool(package['Active']), str2bool(package_show['enabled'])) self.assertEqual( package['FQN'], package_show['fully_qualified_name']) self.assertEqual( package['ID'], package_show['id']) self.assertEqual( str2bool(package['Is Public']), str2bool(package_show['is_public'])) self.assertEqual( package['Name'], package_show['name']) self.assertEqual( package['Type'], package_show['type']) def test_package_import_update(self): """Test scenario: 1) import package 2) import new_package using option 'u' - update 3) check that package has been updated """ package = self.import_package( self.app_name, self.dummy_app_path ) upd_package = self.import_package( self.app_name, self.dummy_app_path, '--exists-action', 'u' ) self.assertEqual(package['Name'], upd_package['Name']) self.assertNotEqual(package['ID'], upd_package['ID']) def test_package_import_skip(self): """Test scenario: 1) import package using option 's' - skip for existing package 2) try to import the same package using option 's' - skip 3) check that package hasn't been updated """ package = self.import_package( self.app_name, self.dummy_app_path, '--exists-action', 's' ) updated_package = self.import_package( self.app_name, self.dummy_app_path, '--exists-action', 's' ) package_list = self.listing("package-list") self.assertIn(package, package_list) self.assertIsNone(updated_package) def test_package_import_abort(self): """Test scenario: 1) import package 2) import new_package using option 'a' - skip 3) check that package hasn't been updated """ package = self.import_package( self.app_name, self.dummy_app_path ) package_list = self.listing('package-list') self.assertIn(package, package_list) package = self.import_package( self.app_name, self.dummy_app_path, '--exists-action', 'a' ) package_list = self.listing('package-list') self.assertNotIn(package, package_list) class DeployMuranoEnvironmentTest(utils.CLIUtilsTestPackagesBase): """Test for testing Murano environment deployment. Test for the Murano CLI commands which checks addition of app to the environment, session creation and deployment of environment. """ # TODO(mstolyarenko): need to unskip this test when # https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed @unittest.skipIf(backend_name == 'glare', "This test fails when GLARE is used as packages " "service. To be fixed as part of #1625039") def test_environment_deployment(self): """Test scenario: 1) import package 2) create environment 3) create session for created environment 4) add application to the environment 5) send environment to deploy 6) check that deployment was successful """ self.import_package( self.app_name, self.dummy_app_path ) env_id = self.create_murano_object('environment', 'TestMuranoDeployEnv')['ID'] obj_model = { 'op': 'add', 'path': '/-', 'value': { '?': { 'type': 'io.murano.apps.{0}'.format(self.app_name), 'id': '{0}'.format(self.generate_uuid()), } } } self.deploy_environment(env_id, obj_model) deployments = self.listing('deployment-list', params=env_id) self.assertEqual('success', deployments[0]['State']) self.assertEqual(1, len(deployments)) # TODO(mstolyarenko): need to unskip this test when # https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed @unittest.skipIf(backend_name == 'glare', "This test fails when GLARE is used as packages " "service. To be fixed as part of #1625039") def test_add_component_to_deployed_env(self): """Test scenario: 1) import package 2) create environment 3) create session for created environment 4) add application to the environment 5) send environment to deploy 6) check that deployment was successful 7) add application to environment 8) deploy environment again """ self.import_package( self.app_name, self.dummy_app_path ) env_id = self.create_murano_object('environment', 'TestMuranoDeployEnv')['ID'] obj_model = { 'op': 'add', 'path': '/-', 'value': { '?': { 'type': 'io.murano.apps.{0}'.format(self.app_name), 'id': '', } } } obj_model['value']['?']['id'] = self.generate_uuid() self.deploy_environment(env_id, obj_model) deployments = self.listing('deployment-list', params=env_id) self.assertEqual('success', deployments[0]['State']) self.assertEqual(1, len(deployments)) obj_model['value']['?']['id'] = self.generate_uuid() self.deploy_environment(env_id, obj_model) deployments = self.listing('deployment-list', params=env_id) self.assertEqual('success', deployments[1]['State']) self.assertEqual(2, len(deployments)) # TODO(mstolyarenko): need to unskip this test when # https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed @unittest.skipIf(backend_name == 'glare', "This test fails when GLARE is used as packages " "service. To be fixed as part of #1625039") def test_delete_component_from_deployed_env(self): """Test scenario: 1) import package 2) create environment 3) create session for created environment 4) add application to the environment 5) send environment to deploy 6) check that deployment was successful 7) delete application from environment 8) deploy environment again """ self.import_package( self.app_name, self.dummy_app_path ) env_id = self.create_murano_object('environment', 'TestMuranoDeployEnv')['ID'] obj_model = { 'op': 'add', 'path': '/-', 'value': { '?': { 'type': 'io.murano.apps.{0}'.format(self.app_name), 'id': '{0}'.format(self.generate_uuid()), } } } self.deploy_environment(env_id, obj_model) obj_model = { 'op': 'remove', 'path': '/0' } self.deploy_environment(env_id, obj_model) deployments = self.listing('deployment-list', params=env_id) self.assertEqual('success', deployments[1]['State']) self.assertEqual(2, len(deployments)) class BundleMuranoSanityClientTest(utils.CLIUtilsTestPackagesBase): """Sanity tests for testing actions with bundle. Tests for the Murano CLI commands which check basic actions with bundles. """ def test_bundle_import_without_bundle_name(self): """Test scenario: 1) Execute murano bundle-import command without bundle name 2) check that error message contains user friendly substring """ result = self.murano('bundle-import', params='', fail_ok=True) self.assertIn("murano bundle-import: error: the following " "arguments are required", result) @unittest.skip("Skip due to apps.openstack.org website is retired.") def test_bundle_import_with_non_existing_package_name(self): """Test scenario: 1) Execute murano bundle-import command with non-existing packages name inside 2) check that error message contains user friendly substring """ result = self.murano( 'bundle-import', params=self.prepare_bundle_with_non_existed_package(), fail_ok=False) self.assertIn("Couldn't find file for package", result) self.assertIn("Error Got non-ok status(404) while connecting", result) @unittest.skip("Skip due to apps.openstack.org website is retired.") def test_bundle_import_with_non_existing_name(self): """Test scenario: 1) Execute murano bundle-import command with non-existing bundle name 2) check that error message contains user friendly substring """ result = self.murano('bundle-import', params=self.app_name, fail_ok=True) self.assertIn("Bundle file '{}' does not exist".format(self.app_name), result) self.assertIn("reason: Got non-ok status(404) while connecting to", result) def test_bundle_import_with_invalid_file_format(self): """Test scenario: 1) Execute murano bundle-import command with invalid bundle file format 2) check that error message contains user friendly substring """ try: self.murano( 'bundle-import', params=self.prepare_bundle_with_invalid_format(), fail_ok=False) except utils.exceptions.CommandFailed as exception: self.assertIn("Can't parse bundle contents", str(exception)) class StaticActionMuranoClientTest(utils.CLIUtilsTestPackagesBase): """Tests for testing static actions execution. Tests for the Murano CLI commands which check the result of sample static action execution. """ def test_static_action_call(self): """Test scenario: 1) import package 2) call static action of the class in that package 3) check the result of action """ package = self.import_package( self.app_name, self.dummy_app_path ) result = self.murano( 'static-action-call', params='{0} staticAction --package-name {1} ' '--arguments myName=John myAge=28'.format(package['FQN'], package['FQN'])) expected = "Waiting for result...\nStatic action result: Hello, " \ "John. In 5 years you will be 33 years old.\n" self.assertEqual(expected, result) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/functional/cli/utils.py0000664000175000017500000000406100000000000026051 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import zipfile import yaml MANIFEST = {'Format': 'MuranoPL/1.0', 'Type': 'Application', 'Description': 'MockApp for CLI tests', 'Author': 'Mirantis, Inc'} def compose_package(app_name, manifest, package_dir, require=None, archive_dir=None): """Composes a murano package Composes package `app_name` with `manifest` file as a template for the manifest and files from `package_dir`. Includes `require` section if any in the manifest file. Puts the resulting .zip file into `archive_dir` if present or in the `package_dir`. """ with open(manifest, 'w') as f: fqn = 'io.murano.apps.' + app_name mfest_copy = MANIFEST.copy() mfest_copy['FullName'] = fqn mfest_copy['Name'] = app_name mfest_copy['Classes'] = {fqn: 'mock_muranopl.yaml'} if require: mfest_copy['Require'] = require f.write(yaml.dump(mfest_copy, default_flow_style=False)) name = app_name + '.zip' if not archive_dir: archive_dir = os.path.dirname(os.path.abspath(__file__)) archive_path = os.path.join(archive_dir, name) with zipfile.ZipFile(archive_path, 'w') as zip_file: for root, dirs, files in os.walk(package_dir): for f in files: zip_file.write( os.path.join(root, f), arcname=os.path.join(os.path.relpath(root, package_dir), f) ) return archive_path ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/functional/muranoclient.py0000664000175000017500000000530000000000000026637 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import configparser import os from tempest.lib.cli import base class ClientTestBase(base.ClientTestBase): def murano(self, action, flags='', params='', fail_ok=False, endpoint_type='publicURL', merge_stderr=True): flags += self.get_backend_flag() return self.clients.cmd_with_auth( 'murano', action, flags, params, fail_ok, merge_stderr) def _get_clients(self): cli_dir = os.environ.get( 'OS_MURANOCLIENT_EXEC_DIR', os.path.join(os.path.abspath('.'), '.tox/functional/bin')) self.username = os.environ.get('OS_USERNAME') self.password = os.environ.get('OS_PASSWORD') self.tenant_name = os.environ.get('OS_PROJECT_NAME', os.environ.get('OS_TENANT_NAME')) self.uri = os.environ.get('OS_AUTH_URL') config = configparser.RawConfigParser() if config.read('functional_creds.conf'): # the OR pattern means the environment is preferred for # override self.username = self.username or config.get('admin', 'user') self.password = self.password or config.get('admin', 'pass') self.tenant_name = self.tenant_name or config.get('admin', 'tenant') self.uri = self.uri or config.get('auth', 'uri') clients = base.CLIClient( username=self.username, password=self.password, tenant_name=self.tenant_name, uri=self.uri, cli_dir=cli_dir ) return clients def listing(self, command, params=""): return self.parser.listing(self.murano(command, params=params)) def get_value(self, need_field, known_field, known_value, somelist): for element in somelist: if element[known_field] == known_value: return element[need_field] @staticmethod def get_backend_flag(): backend = os.environ.get('MURANO_PACKAGES_SERVICE', 'murano') backend_flag = " --murano-packages-service {0} ".format(backend) return backend_flag ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5980825 python-muranoclient-2.8.0/muranoclient/tests/unit/0000775000175000017500000000000000000000000022404 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/__init__.py0000664000175000017500000000000000000000000024503 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/base.py0000664000175000017500000000320100000000000023664 0ustar00zuulzuul00000000000000# Copyright 2011 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import fixtures import testtools class TestCaseShell(testtools.TestCase): TEST_REQUEST_BASE = { 'verify': True, } def setUp(self): super(TestCaseShell, self).setUp() if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or os.environ.get('OS_STDOUT_CAPTURE') == '1'): stdout = self.useFixture(fixtures.StringStream('stdout')).stream self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or os.environ.get('OS_STDERR_CAPTURE') == '1'): stderr = self.useFixture(fixtures.StringStream('stderr')).stream self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) class TestAdditionalAsserts(testtools.TestCase): def check_dict_is_subset(self, dict1, dict2): # There is an assert for this in Python 2.7 but not 2.6 self.assertTrue(all(k in dict2 and dict2[k] == v for k, v in dict1.items())) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/fakes.py0000664000175000017500000000242100000000000024046 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_serialization import jsonutils class FakeHTTPResponse(object): version = 1.1 def __init__(self, status_code, reason, headers, content): self.headers = headers self.content = content self.status_code = status_code self.reason = reason self.raw = FakeRaw() def getheader(self, name, default=None): return self.headers.get(name, default) def getheaders(self): return self.headers.items() def read(self, amt=None): b = self.content self.content = None return b def iter_content(self, chunksize): return self.content def json(self): return jsonutils.loads(self.content) class FakeRaw(object): version = 110 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5980825 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/0000775000175000017500000000000000000000000025063 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5980825 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/empty-app/0000775000175000017500000000000000000000000026777 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/empty-app/manifest.yaml0000664000175000017500000000123400000000000031471 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. Format: 1.3 Type: Application FullName: empty Name: empty Description: empty description Author: 'Mirantis, Inc' ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/heat-template.yaml0000664000175000017500000000004200000000000030475 0ustar00zuulzuul00000000000000heat_template_version: 2013-05-23 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/logo.png0000664000175000017500000002656200000000000026544 0ustar00zuulzuul00000000000000PNG  IHDR22?NiCCPICC ProfileX YgTsF2H"9Ee\ ""AE$ PfQ0!$(*b$׋}=yꞞZԘȨ8(q@pRccmjgӢGâ"ASBf.P9aAVùBC\= D3u0rd,D5{Bz\%ñm@g`o>XHL1]?9&bu}C؁ | U .ދ0n@՚^!g}L#=&t>?-0@D09u} cA P-3c' . |QP'8@*ބC$8mΟq,xK9AC"};l=;| ?تߞn͠aײ~E%F, LIA8cq@=1@4Or'GZ_>31kf+8"V P?5l2v=@k6lws^h8!YI?J:5?`D 5pUZQUeu{:]oΫ{5*Ȣ>P[ dRy<-=>|@*k\"!Dv,bpu{9\o0 f" Dp |"#6!3"H ;<r9 !҃—Og-~@ p Z"̈́(aH$zD"G""#6{o?HL$*ɜM"eJHgH^8iAaCCC>CM. lҌza;Kk2`ĴIɉ))LdvلC'#&7Q()!śGGR)?931_ce"bIJ%2K+ +5  [$3l&؉Rf8 ?N*o9 ҜVay9;9عԹܹrqƸ#/qp/ .]{ɏwO?JS@W_ W *(',MRCpFHXB(FPД0pppHHH{Q.Q#R6i1A1Kxbbk׸XS8xxxDY!IImɃ%礤+B"c EBO -.{D[Ӑ +G5CůYbYH!AkEnE[ 늟$ +TPPR~®brS嫪jjE\mZuy@O5844vkh,kji4k5'$|ʵ9h?liЙ_.nݥuutuN^j= }Q}_#bT 7 ǍdŒ}2V6_53YgjlZv66_cl~|BbE%ƲrJߪjZ:պ͆lbs捭-jgmWh^>pr(tx(ѩ靳s}.g\f]]]ɸŻ׸yzTL|UMv>=lC>>Y>7npM7ln=DuVPgM0 ( <8t h"X/0x2 $d*$p0˰casW"<""I7أ£ڢFdŌlYx4͆v*[ ?;ewſNO(Kxy+֨IrI9I'okIKّ:(Dֲ]|{;wxq N7323GwY:ŜEܭX6.;43G-P܀<弒={-ݻ/h_gfQ H>0ZhWxH({%%2?8Rj[ZHCKCՕ  8{h1cyzĵ JBeB*'O֜?wjtj3gϢgN9}|}B: B}/\rYr+W9^C%]r}ޫ7RuA6Fƕ䦙;wF[6֡q㫝׺uoi5ɽ>G=nO}G?S0{`phwt!eKm:x߉ɪ藺\r5g|&+:EKj]^;7Zo6ߺpv]cMSe;-)maw]i?`aWGw7tuvRz<)  V?Mxf8DחƋúã|x}(6Z766wMc&&WRX3ӎᄂ|z[l\Gc wWVV_a">I#)0H3J3!+S1;cobf]dR .zBisI&)i +Y_$µ:'q*bռ44nh?yzaQnӏ$ !KU+3k7(i;w9e9gquH r6`c{Sb vW˃{(CºoFDEGgж cEb'd%m5KRN&—ʕƺ}6͎;3wm̲mmCݑwrϽ>+9pcTɧ?)*?uoOxVSd㩆ψx: 5UZޛ[ ojNׅO&(&'I! J LȉgfUfYgl5\}t8lQc:uNhT(UT;v$swמ{rae+\Q~s[Ѣɯy-V7ypOG˝] ?Qi!/^1N˛Hhuvމ>jowABg$#\u  ^0Ӵ0s r`Fy4^XYBY懱^EFQCt7zLÒjlG­E*qx!!BOIb "C5 9&{j2GnHRr(]`S Xyuƹ˙k;S+[g7-"0*xLOX^=b5%"mde)rUHSQQTVyzF-G=DZSQKkE^wtMϚ3H$+k CcS[WGq6qʛ7VQ;~H nY3 ?1ݻŜ_bmݩ22mw.Y{wO>W _)=xR 5)׎\|Wՙ[]_[\3fEku@VkF-k B(UueQB XU7(FmQzmE?c acR#x=6-J"$~% It /1y2]0GѡgfbD"Et0dtCCqc`cifaeϮ#Ye;Gw Kzbky%_>KYcXk૘tD^7u~ + V: zDhƸД۬ۊͺ}ː{ד A>?6E1~Z !;+#AuZtp|kb|ROiUI;22],jٱ9}yn{&eW,xV][r#i]aP|T{cMYsݵu7.\:xt5R}M[ݷ34'[zHchg/A+C^8x?&c {1N|}'IG333gCfg&7?@YY]X,]dT|'dm }c`WVI@<rbr%L6^2Wt.ןN? #q pHYs  iTXtXML:com.adobe.xmp 1440 900 u IDAThݚyp]}ǿMzZےก2Cmh'ҖL)Sht&SBC3SR4JRL0 v^d˖-ɲ{' 1t޹Y~~S455Ŀ,\Ԥ=cD3,SQuVޅB^qּU֪.0?x=՛$r&DMu5='Ӂ*IYʗPeU/RQTյNz/EZ aG|F`&_W6'Vەe*T*|h ceQ)o4UJ8S#ZLKRi\iꦘ!E$> }ꕢ*%JPTZΫQ)mDi6ATS植\&j4hJTt!v\G-o95K =C5}KbQY\ WQPIb0,RB5INT&E m^e*|N;M{ OIdNk4+Qy|T ϰ8_h4]H+#V\L*G*5dG'&5@$ G:U5խk"}.[懃\u]=ژJ{<% :( R4g)Bm]&uPU9 /GVIi}'S\PyP~ ̟E93ԐeMC:ڝ>ĈL3e)W#Liw"m+mےT%]t{TW= ] { "t _)ʽ[UApLFHr+$$^p?ǴwX)>pI a)x7J 8T.UQO5=cm(dC>z@uW@xx7DMɲ IxRP2 qCTW`DLHę̌EzO-{t%K[6frpgܡUD}Ge<*BIQkQtX h)3}LCTAJy$ـD8> 2%3zyTq4%wx jdd LHˋ%]!}rǭ[Z}up("R8eR 0d!3^!4FfNbKVdD nbhIH{UFz @v^ćYh8!8\qo}I&L3ueS1}Ԧ*u@ {h396t{QE (iG|RBʌ$0-ՠsc8&]Ʉ_.~NLUV @n? f,"(=dZ.N л;ekc"TEPȜ57a98:Re:zPy3ŢLb60 vTh*q:i4/I1OӾ1;}< M`jv)*%vX3IDLmRCJ⡬2M+ ,k栾s8 1׆ 'P&c RS݅ɰ1k1TL`4 2:F9wBb0S_ߒc݀.ށ#L ĥg=z`Mt!8B37Y6b.'NL'>zh^ ё9 +3G . J:}I>6U^p/ ZjDFj6Mnz`8/.H{;S A%` k^[38`3x ^l`ۤմƾcXxC)*XlދC;g \9W391NX^'́wq @z%nvf eU]8g/`7"{D a;-4ggP3iH'c\pKДt019s!%YuTbπ-R!¼o׾AQ Zy=H^rl۝&w?9ob1lNI\}3³H X{j \.OV<8ժ iht:ᆭœ̩%罨ОXCs \떺7vrڑor@"(K׵hSr 2Ϋf }?x޳IE?|[Ɛ g[:s^Cxr޹A> cM瑌OPlmzu`BǑ" />Ë !Nxq )E3e|2GkJ3I߮{Y<8ӝu@_g/ܯg!Axۺ-&7 O7X$0ajZjWH ISf Bw{ͿԿ_ާhP;@؋Qp>T)_̵z~ևCf 52 :×! twX:H"})GJם! qKwݧ Z0'p"C:@m xfW^qcg(,zen?eo{DŽV>Wµ8m}kP+u~mͷ)ھ/rG@b^ERqV$?\y9Ld~ȼjͽ+I+O#>ޔ8ڛ& 5iP:b^/hS ~WA4i f6Ksg) *{_~Xa exxDvgsv_z3 &gO52e$\֓H1Nww;7jiYdN-s*׽D)A~6gnKՎGJHlHmx *î"R+A`tw/DC elHOpY>fda^08AP)9\{k]7^zz8L^@gY5dKO?DN@8D܂VzY@I>8:>ߍQG1ǙxnrkQ9p&)T_:Džϭ&Y5o# {PBoyRoi?!ׁj%$D5sGP<?ѨKԺH;@! `}eiƨ֮)ޅCg!^; ޵k>[ݻIN,[5LpAYLH.8oۋ;OXǴwvA ـ]`0sZ0 -P 3NY?g̢'`G7?'ڦݣ~5cڛ_P0ߑ.շ[rI;k4yG OL s-%s6^a۶?]4::'.;<aŠl⺻;w[u֪JXVS[-o[CyS&$V00IENDB`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5980825 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/test-app/0000775000175000017500000000000000000000000026620 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5980825 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/test-app/Classes/0000775000175000017500000000000000000000000030215 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/test-app/Classes/testapp.yaml0000664000175000017500000000136100000000000032562 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.apps.test std: io.murano res: io.murano.resources Name: APP Extends: std:Application Properties: name: Contract: $.string().notNull() instance: Contract: $.class(res:Instance).notNull() Workflow: initialize: Body: - $.environment: $.find(std:Environment).require() deploy: Body: - $securityGroupIngress: - ToPort: 23 FromPort: 23 IpProtocol: tcp External: True - $.environment.securityGroupManager.addGroupIngress($securityGroupIngress) - $.instance.deploy() - $resources: new('io.murano.system.Resources') - $template: $resources.yaml('Deploy.template') - $.instance.agent.call($template, $resources) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5980825 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/test-app/Resources/0000775000175000017500000000000000000000000030572 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/test-app/Resources/Deploy.template0000664000175000017500000000051300000000000033562 0ustar00zuulzuul00000000000000FormatVersion: 2.0.0 Version: 1.0.0 Name: Deploy Parameters: appName: $appName Body: | return deploy(args.appName).stdout Scripts: deploy: Type: Application Version: 1.0.0 EntryPoint: deploy.sh Files: - installer.sh - common.sh Options: captureStdout: true captureStderr: false ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.5980825 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/0000775000175000017500000000000000000000000032261 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/common.sh0000664000175000017500000000001400000000000034100 0ustar00zuulzuul00000000000000#!/bin/bash ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/deploy.sh0000664000175000017500000000001400000000000034104 0ustar00zuulzuul00000000000000#!/bin/bash ././@PaxHeader0000000000000000000000000000020600000000000011453 xustar0000000000000000112 path=python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/installer.sh 22 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/installer.0000664000175000017500000000001400000000000034252 0ustar00zuulzuul00000000000000#!/bin/bash ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/fixture_data/test-app/ui.yaml0000664000175000017500000000001300000000000030113 0ustar00zuulzuul00000000000000Version: 2 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.6020837 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/0000775000175000017500000000000000000000000023170 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/__init__.py0000664000175000017500000000000000000000000025267 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/test_plugin.py0000664000175000017500000000232400000000000026100 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from muranoclient.osc import plugin from muranoclient.tests.unit import base class TestApplicationCatalogPlugin(base.TestCaseShell): @mock.patch("muranoclient.v1.client.Client") def test_make_client(self, p_client): instance = mock.Mock() instance._api_version = {"application_catalog": '1'} instance._region_name = 'murano_region' instance.session = 'murano_session' plugin.make_client(instance) p_client.assert_called_with( mock.ANY, region_name='murano_region', session='murano_session', service_type='application-catalog') ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.6020837 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/0000775000175000017500000000000000000000000023516 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/__init__.py0000664000175000017500000000000000000000000025615 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fakes.py0000664000175000017500000000146700000000000025171 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from osc_lib.tests import utils from unittest import mock class TestApplicationCatalog(utils.TestCommand): def setUp(self): super(TestApplicationCatalog, self).setUp() self.app.client_manager.application_catalog = mock.Mock() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.6020837 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/0000775000175000017500000000000000000000000026175 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/heat-template.yaml0000664000175000017500000000004200000000000031607 0ustar00zuulzuul00000000000000heat_template_version: 2013-05-23 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/logo.png0000664000175000017500000002656200000000000027656 0ustar00zuulzuul00000000000000PNG  IHDR22?NiCCPICC ProfileX YgTsF2H"9Ee\ ""AE$ PfQ0!$(*b$׋}=yꞞZԘȨ8(q@pRccmjgӢGâ"ASBf.P9aAVùBC\= D3u0rd,D5{Bz\%ñm@g`o>XHL1]?9&bu}C؁ | U .ދ0n@՚^!g}L#=&t>?-0@D09u} cA P-3c' . |QP'8@*ބC$8mΟq,xK9AC"};l=;| ?تߞn͠aײ~E%F, LIA8cq@=1@4Or'GZ_>31kf+8"V P?5l2v=@k6lws^h8!YI?J:5?`D 5pUZQUeu{:]oΫ{5*Ȣ>P[ dRy<-=>|@*k\"!Dv,bpu{9\o0 f" Dp |"#6!3"H ;<r9 !҃—Og-~@ p Z"̈́(aH$zD"G""#6{o?HL$*ɜM"eJHgH^8iAaCCC>CM. lҌza;Kk2`ĴIɉ))LdvلC'#&7Q()!śGGR)?931_ce"bIJ%2K+ +5  [$3l&؉Rf8 ?N*o9 ҜVay9;9عԹܹrqƸ#/qp/ .]{ɏwO?JS@W_ W *(',MRCpFHXB(FPД0pppHHH{Q.Q#R6i1A1Kxbbk׸XS8xxxDY!IImɃ%礤+B"c EBO -.{D[Ӑ +G5CůYbYH!AkEnE[ 늟$ +TPPR~®brS嫪jjE\mZuy@O5844vkh,kji4k5'$|ʵ9h?liЙ_.nݥuutuN^j= }Q}_#bT 7 ǍdŒ}2V6_53YgjlZv66_cl~|BbE%ƲrJߪjZ:պ͆lbs捭-jgmWh^>pr(tx(ѩ靳s}.g\f]]]ɸŻ׸yzTL|UMv>=lC>>Y>7npM7ln=DuVPgM0 ( <8t h"X/0x2 $d*$p0˰casW"<""I7أ£ڢFdŌlYx4͆v*[ ?;ewſNO(Kxy+֨IrI9I'okIKّ:(Dֲ]|{;wxq N7323GwY:ŜEܭX6.;43G-P܀<弒={-ݻ/h_gfQ H>0ZhWxH({%%2?8Rj[ZHCKCՕ  8{h1cyzĵ JBeB*'O֜?wjtj3gϢgN9}|}B: B}/\rYr+W9^C%]r}ޫ7RuA6Fƕ䦙;wF[6֡q㫝׺uoi5ɽ>G=nO}G?S0{`phwt!eKm:x߉ɪ藺\r5g|&+:EKj]^;7Zo6ߺpv]cMSe;-)maw]i?`aWGw7tuvRz<)  V?Mxf8DחƋúã|x}(6Z766wMc&&WRX3ӎᄂ|z[l\Gc wWVV_a">I#)0H3J3!+S1;cobf]dR .zBisI&)i +Y_$µ:'q*bռ44nh?yzaQnӏ$ !KU+3k7(i;w9e9gquH r6`c{Sb vW˃{(CºoFDEGgж cEb'd%m5KRN&—ʕƺ}6͎;3wm̲mmCݑwrϽ>+9pcTɧ?)*?uoOxVSd㩆ψx: 5UZޛ[ ojNׅO&(&'I! J LȉgfUfYgl5\}t8lQc:uNhT(UT;v$swמ{rae+\Q~s[Ѣɯy-V7ypOG˝] ?Qi!/^1N˛Hhuvމ>jowABg$#\u  ^0Ӵ0s r`Fy4^XYBY懱^EFQCt7zLÒjlG­E*qx!!BOIb "C5 9&{j2GnHRr(]`S Xyuƹ˙k;S+[g7-"0*xLOX^=b5%"mde)rUHSQQTVyzF-G=DZSQKkE^wtMϚ3H$+k CcS[WGq6qʛ7VQ;~H nY3 ?1ݻŜ_bmݩ22mw.Y{wO>W _)=xR 5)׎\|Wՙ[]_[\3fEku@VkF-k B(UueQB XU7(FmQzmE?c acR#x=6-J"$~% It /1y2]0GѡgfbD"Et0dtCCqc`cifaeϮ#Ye;Gw Kzbky%_>KYcXk૘tD^7u~ + V: zDhƸД۬ۊͺ}ː{ד A>?6E1~Z !;+#AuZtp|kb|ROiUI;22],jٱ9}yn{&eW,xV][r#i]aP|T{cMYsݵu7.\:xt5R}M[ݷ34'[zHchg/A+C^8x?&c {1N|}'IG333gCfg&7?@YY]X,]dT|'dm }c`WVI@<rbr%L6^2Wt.ןN? #q pHYs  iTXtXML:com.adobe.xmp 1440 900 u IDAThݚyp]}ǿMzZےก2Cmh'ҖL)Sht&SBC3SR4JRL0 v^d˖-ɲ{' 1t޹Y~~S455Ŀ,\Ԥ=cD3,SQuVޅB^qּU֪.0?x=՛$r&DMu5='Ӂ*IYʗPeU/RQTյNz/EZ aG|F`&_W6'Vەe*T*|h ceQ)o4UJ8S#ZLKRi\iꦘ!E$> }ꕢ*%JPTZΫQ)mDi6ATS植\&j4hJTt!v\G-o95K =C5}KbQY\ WQPIb0,RB5INT&E m^e*|N;M{ OIdNk4+Qy|T ϰ8_h4]H+#V\L*G*5dG'&5@$ G:U5խk"}.[懃\u]=ژJ{<% :( R4g)Bm]&uPU9 /GVIi}'S\PyP~ ̟E93ԐeMC:ڝ>ĈL3e)W#Liw"m+mےT%]t{TW= ] { "t _)ʽ[UApLFHr+$$^p?ǴwX)>pI a)x7J 8T.UQO5=cm(dC>z@uW@xx7DMɲ IxRP2 qCTW`DLHę̌EzO-{t%K[6frpgܡUD}Ge<*BIQkQtX h)3}LCTAJy$ـD8> 2%3zyTq4%wx jdd LHˋ%]!}rǭ[Z}up("R8eR 0d!3^!4FfNbKVdD nbhIH{UFz @v^ćYh8!8\qo}I&L3ueS1}Ԧ*u@ {h396t{QE (iG|RBʌ$0-ՠsc8&]Ʉ_.~NLUV @n? f,"(=dZ.N л;ekc"TEPȜ57a98:Re:zPy3ŢLb60 vTh*q:i4/I1OӾ1;}< M`jv)*%vX3IDLmRCJ⡬2M+ ,k栾s8 1׆ 'P&c RS݅ɰ1k1TL`4 2:F9wBb0S_ߒc݀.ށ#L ĥg=z`Mt!8B37Y6b.'NL'>zh^ ё9 +3G . J:}I>6U^p/ ZjDFj6Mnz`8/.H{;S A%` k^[38`3x ^l`ۤմƾcXxC)*XlދC;g \9W391NX^'́wq @z%nvf eU]8g/`7"{D a;-4ggP3iH'c\pKДt019s!%YuTbπ-R!¼o׾AQ Zy=H^rl۝&w?9ob1lNI\}3³H X{j \.OV<8ժ iht:ᆭœ̩%罨ОXCs \떺7vrڑor@"(K׵hSr 2Ϋf }?x޳IE?|[Ɛ g[:s^Cxr޹A> cM瑌OPlmzu`BǑ" />Ë !Nxq )E3e|2GkJ3I߮{Y<8ӝu@_g/ܯg!Axۺ-&7 O7X$0ajZjWH ISf Bw{ͿԿ_ާhP;@؋Qp>T)_̵z~ևCf 52 :×! twX:H"})GJם! qKwݧ Z0'p"C:@m xfW^qcg(,zen?eo{DŽV>Wµ8m}kP+u~mͷ)ھ/rG@b^ERqV$?\y9Ld~ȼjͽ+I+O#>ޔ8ڛ& 5iP:b^/hS ~WA4i f6Ksg) *{_~Xa exxDvgsv_z3 &gO52e$\֓H1Nww;7jiYdN-s*׽D)A~6gnKՎGJHlHmx *î"R+A`tw/DC elHOpY>fda^08AP)9\{k]7^zz8L^@gY5dKO?DN@8D܂VzY@I>8:>ߍQG1ǙxnrkQ9p&)T_:Džϭ&Y5o# {PBoyRoi?!ׁj%$D5sGP<?ѨKԺH;@! `}eiƨ֮)ޅCg!^; ޵k>[ݻIN,[5LpAYLH.8oۋ;OXǴwvA ـ]`0sZ0 -P 3NY?g̢'`G7?'ڦݣ~5cڛ_P0ߑ.շ[rI;k4yG OL s-%s6^a۶?]4::'.;<aŠl⺻;w[u֪JXVS[-o[CyS&$V00IENDB`././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.6020837 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/0000775000175000017500000000000000000000000027732 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.6020837 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Classes/0000775000175000017500000000000000000000000031327 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Classes/testapp.yaml0000664000175000017500000000136100000000000033674 0ustar00zuulzuul00000000000000Namespaces: =: io.murano.apps.test std: io.murano res: io.murano.resources Name: APP Extends: std:Application Properties: name: Contract: $.string().notNull() instance: Contract: $.class(res:Instance).notNull() Workflow: initialize: Body: - $.environment: $.find(std:Environment).require() deploy: Body: - $securityGroupIngress: - ToPort: 23 FromPort: 23 IpProtocol: tcp External: True - $.environment.securityGroupManager.addGroupIngress($securityGroupIngress) - $.instance.deploy() - $resources: new('io.murano.system.Resources') - $template: $resources.yaml('Deploy.template') - $.instance.agent.call($template, $resources) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.6020837 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/0000775000175000017500000000000000000000000031704 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000021000000000000011446 xustar0000000000000000114 path=python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/Deploy.template 22 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/Deploy.temp0000664000175000017500000000051300000000000034026 0ustar00zuulzuul00000000000000FormatVersion: 2.0.0 Version: 1.0.0 Name: Deploy Parameters: appName: $appName Body: | return deploy(args.appName).stdout Scripts: deploy: Type: Application Version: 1.0.0 EntryPoint: deploy.sh Files: - installer.sh - common.sh Options: captureStdout: true captureStderr: false ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.6020837 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/0000775000175000017500000000000000000000000033373 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000021200000000000011450 xustar0000000000000000116 path=python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/common.sh 22 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/com0000664000175000017500000000001400000000000034067 0ustar00zuulzuul00000000000000#!/bin/bash ././@PaxHeader0000000000000000000000000000021200000000000011450 xustar0000000000000000116 path=python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/deploy.sh 22 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/dep0000664000175000017500000000001400000000000034061 0ustar00zuulzuul00000000000000#!/bin/bash ././@PaxHeader0000000000000000000000000000021500000000000011453 xustar0000000000000000119 path=python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/installer.sh 22 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/ins0000664000175000017500000000001400000000000034102 0ustar00zuulzuul00000000000000#!/bin/bash ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/fixture_data/test-app/ui.yaml0000664000175000017500000000001300000000000031225 0ustar00zuulzuul00000000000000Version: 2 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/test_action.py0000664000175000017500000000573600000000000026417 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from muranoclient.osc.v1 import action as osc_action from muranoclient.tests.unit.osc.v1 import fakes from muranoclient.v1 import static_actions as api_static_actions class TestAction(fakes.TestApplicationCatalog): def setUp(self): super(TestAction, self).setUp() self.static_actions_mock = \ self.app.client_manager.application_catalog.static_actions class TestStaticActionCall(TestAction): def setUp(self): super(TestStaticActionCall, self).setUp() self.static_actions_mock.call.return_value = \ api_static_actions.StaticActionResult('result') # Command to test self.cmd = osc_action.StaticActionCall(self.app, None) @mock.patch('osc_lib.utils.get_item_properties') def test_static_action_call_basic(self, mock_util): mock_util.return_value = 'result' arglist = ['class.name', 'method.name'] verifylist = [('class_name', 'class.name'), ('method_name', 'method.name')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['Static action result'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = ['result'] self.assertEqual(expected_data, data) @mock.patch('osc_lib.utils.get_item_properties') def test_static_action_call_full(self, mock_util): mock_util.return_value = 'result' arglist = ['class.name', 'method.name', '--arguments', 'food=spam', 'parrot=dead', '--package-name', 'package.name', '--class-version', '>1'] verifylist = [('class_name', 'class.name'), ('method_name', 'method.name'), ('arguments', ['food=spam', 'parrot=dead']), ('package_name', 'package.name'), ('class_version', '>1')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['Static action result'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = ['result'] self.assertEqual(expected_data, data) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/test_category.py0000664000175000017500000001215100000000000026744 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from muranoclient.osc.v1 import category as osc_category from muranoclient.tests.unit.osc.v1 import fakes from muranoclient.v1 import categories as api_category from muranoclient.v1 import packages as api_packages CATEGORY_INFO = {'id': 'xyz123', 'name': 'fake1', 'packages': [{'name': 'package1'}, {'name': 'package2'}]} class TestCategory(fakes.TestApplicationCatalog): def setUp(self): super(TestCategory, self).setUp() self.category_mock = self.app.client_manager.application_catalog.\ categories self.category_mock.reset_mock() self.packages_mock = \ self.app.client_manager.application_catalog.packages class TestListCategories(TestCategory): def setUp(self): super(TestListCategories, self).setUp() self.category_mock.list.return_value = [api_category.Category(None, CATEGORY_INFO)] # Command to test self.cmd = osc_category.ListCategories(self.app, None) @mock.patch('osc_lib.utils.get_item_properties') def test_category_list(self, mock_util): mock_util.return_value = ('xyz123', 'fake1') columns, data = self.cmd.take_action(parsed_args=None) # Check that columns are correct expected_columns = ['ID', 'Name'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('xyz123', 'fake1')] self.assertEqual(expected_data, data) class TestShowCategory(TestCategory): def setUp(self): super(TestShowCategory, self).setUp() self.category_mock.get.return_value = api_category.\ Category(None, CATEGORY_INFO) self.packages_mock.filter.return_value = [ api_packages.Package(None, pkg_info) for pkg_info in CATEGORY_INFO[ 'packages'] ] # Command to test self.cmd = osc_category.ShowCategory(self.app, None) @mock.patch('textwrap.wrap') def test_category_show(self, mock_wrap): arglist = ['xyz123'] verifylist = [('id', 'xyz123')] mock_wrap.return_value = ['package1, package2'] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ('id', 'name', 'packages') self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = ('xyz123', 'fake1', 'package1, package2') self.assertEqual(expected_data, data) class TestCreateCategory(TestCategory): def setUp(self): super(TestCreateCategory, self).setUp() self.category_mock.add.return_value = [api_category.Category(None, CATEGORY_INFO)] # Command to test self.cmd = osc_category.CreateCategory(self.app, None) @mock.patch('osc_lib.utils.get_item_properties') def test_category_list(self, mock_util): arglist = ['fake1'] verifylist = [('name', 'fake1')] mock_util.return_value = ('xyz123', 'fake1') parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['ID', 'Name'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('xyz123', 'fake1')] self.assertEqual(expected_data, data) class TestDeleteCategory(TestCategory): def setUp(self): super(TestDeleteCategory, self).setUp() self.category_mock.delete.return_value = None self.category_mock.list.return_value = [api_category.Category(None, CATEGORY_INFO)] # Command to test self.cmd = osc_category.DeleteCategory(self.app, None) @mock.patch('osc_lib.utils.get_item_properties') def test_category_list(self, mock_util): arglist = ['abc123', '123abc'] verifylist = [('id', ['abc123', '123abc'])] mock_util.return_value = ('xyz123', 'fake1') parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['ID', 'Name'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('xyz123', 'fake1')] self.assertEqual(expected_data, data) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/test_deployment.py0000664000175000017500000000643100000000000027313 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from unittest import mock from muranoclient.osc.v1 import deployment as osc_deployment from muranoclient.tests.unit.osc.v1 import fakes from muranoclient.v1 import deployments as api_deployment DEPLOYMENT_COLUMNS = ('id', 'state', 'created', 'updated', 'finished') DEPLOYMENT_DATA = ('xyz123', 'success', '2016-06-25T12:21:37', '2016-06-25T12:21:47', '2016-06-25T12:21:47') ALL_DEPLOYMENT_DATA = (('abc123', 'success', '2016-06-25T12:21:37', '2016-06-25T12:21:47', '2016-06-25T12:21:47'), ('xyz456', 'success', '2017-01-31T11:22:35', '2017-01-31T11:22:47', '2017-01-31T11:22:47')) class TestDeployment(fakes.TestApplicationCatalog): def setUp(self): super(TestDeployment, self).setUp() self.deployment_mock = self.app.client_manager.application_catalog.\ deployments self.deployment_mock.reset_mock() self.environment_mock = self.app.client_manager.application_catalog.\ environments class TestListDeployment(TestDeployment): def setUp(self): super(TestListDeployment, self).setUp() deployment_info = dict(zip(DEPLOYMENT_COLUMNS, DEPLOYMENT_DATA)) self.deployment_mock.list.return_value = \ [api_deployment.Deployment(None, deployment_info)] # Command to test self.cmd = osc_deployment.ListDeployment(self.app, None) @mock.patch('osc_lib.utils.get_item_properties') def test_deployment_list(self, mock_util): arglist = ['xyz123'] verifylist = [('id', 'xyz123')] mock_util.return_value = DEPLOYMENT_DATA parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = [c.title() for c in DEPLOYMENT_COLUMNS] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [DEPLOYMENT_DATA] self.assertEqual(expected_data, data) @mock.patch('osc_lib.utils.get_item_properties', autospec=True) def test_deployment_list_all_environments(self, mock_util): arglist = ['--all-environments'] verifylist = [('id', None), ('all_environments', True)] mock_util.return_value = ALL_DEPLOYMENT_DATA parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = [c.title() for c in DEPLOYMENT_COLUMNS] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [ALL_DEPLOYMENT_DATA] self.assertEqual(expected_data, data) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/test_environment.py0000664000175000017500000005302600000000000027501 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import json import tempfile from unittest import mock from muranoclient.osc.v1 import environment as osc_env from muranoclient.tests.unit.osc.v1 import fakes from muranoclient.v1 import environments as api_env ENV_INFO = {'id': '1234', 'name': 'Fake Environment', 'created': '2015-12-16T17:31:54', 'updated': '2015-12-16T17:31:54', 'networking': {}, 'services': ['fake services'], 'status': 'fake deployed', 'tenant_id': 'xyz123', 'version': '1'} ENV_MODEL = { "defaultNetworks": { "environment": { "name": "env-network", "?": { "type": "io.murano.resources.NeutronNetwork", "id": "5678" } }, "flat": None }, "region": "RegionOne", "name": "env", "?": { "updated": "2016-10-03 09:33:41.039789", "type": "io.murano.Environment", "id": "1234" } } class TestEnvironment(fakes.TestApplicationCatalog): def setUp(self): super(TestEnvironment, self).setUp() self.environment_mock = self.app.client_manager.application_catalog.\ environments self.session_mock = self.app.client_manager.application_catalog.\ sessions self.services_mock = self.app.client_manager.application_catalog.\ services self.environment_mock.reset_mock() class TestListEnvironment(TestEnvironment): def setUp(self): super(TestListEnvironment, self).setUp() self.environment_mock.list.return_value = [api_env.Environment(None, ENV_INFO)] # Command to test self.cmd = osc_env.ListEnvironments(self.app, None) @mock.patch('osc_lib.utils.get_item_properties') def test_environment_list_with_no_options(self, mock_util): arglist = [] verifylist = [] mock_util.return_value = ('1234', 'Environment of all tenants', 'fake deployed', '2015-12-16T17:31:54', '2015-12-16T17:31:54' ) parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('1234', 'Environment of all tenants', 'fake deployed', '2015-12-16T17:31:54', '2015-12-16T17:31:54')] self.assertEqual(expected_data, data) @mock.patch('osc_lib.utils.get_item_properties') def test_environment_list_with_all_tenants(self, mock_util): arglist = ['--all-tenants'] verifylist = [('all_tenants', True), ('tenant', None)] mock_util.return_value = ('1234', 'Environment of all tenants', 'fake deployed', '2015-12-16T17:31:54', '2015-12-16T17:31:54' ) parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('1234', 'Environment of all tenants', 'fake deployed', '2015-12-16T17:31:54', '2015-12-16T17:31:54')] self.assertEqual(expected_data, data) self.environment_mock.list.assert_called_once_with(True, None) @mock.patch('osc_lib.utils.get_item_properties') def test_environment_list_with_tenant(self, mock_util): arglist = ['--tenant=ABC'] verifylist = [('all_tenants', False), ('tenant', 'ABC')] mock_util.return_value = ('1234', 'Environment of tenant ABC', 'fake deployed', '2015-12-16T17:31:54', '2015-12-16T17:31:54' ) parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('1234', 'Environment of tenant ABC', 'fake deployed', '2015-12-16T17:31:54', '2015-12-16T17:31:54')] self.assertEqual(expected_data, data) self.environment_mock.list.assert_called_once_with(False, 'ABC') class TestShowEnvironment(TestEnvironment): def setUp(self): super(TestShowEnvironment, self).setUp() mock_to_dict = self.environment_mock.get.return_value.to_dict mock_to_dict.return_value = ENV_INFO self.cmd = osc_env.ShowEnvironment(self.app, None) @mock.patch('oslo_serialization.jsonutils.dumps') def test_environment_show_with_no_options(self, mock_json): arglist = ['fake'] verifylist = [('id', 'fake')] mock_json.return_value = ['fake services'] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ('created', 'id', 'name', 'networking', 'services', 'status', 'tenant_id', 'updated', 'version') self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = ('2015-12-16T17:31:54', '1234', 'Fake Environment', {}, ['fake services'], 'fake deployed', 'xyz123', '2015-12-16T17:31:54', '1') self.assertEqual(expected_data, data) @mock.patch('oslo_serialization.jsonutils.dumps') def test_environment_show_with_only_app_option(self, mock_json): arglist = ['fake', '--only-apps'] verifylist = [('id', 'fake'), ('only_apps', True)] mock_json.return_value = ['fake services'] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['services'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [['fake services']] self.assertEqual(expected_data, data) @mock.patch('oslo_serialization.jsonutils.dumps') def test_environment_show_with_session_id_option(self, mock_json): arglist = ['fake', '--session-id', 'abc123'] verifylist = [('id', 'fake'), ('session_id', 'abc123')] mock_json.return_value = ['fake services'] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ('created', 'id', 'name', 'networking', 'services', 'status', 'tenant_id', 'updated', 'version') self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = ('2015-12-16T17:31:54', '1234', 'Fake Environment', {}, ['fake services'], 'fake deployed', 'xyz123', '2015-12-16T17:31:54', '1') self.assertEqual(expected_data, data) class TestRenameEnvironment(TestEnvironment): def setUp(self): super(TestRenameEnvironment, self).setUp() self.environment_mock.update.return_value = [api_env.Environment(None, ENV_INFO)] # Command to test self.cmd = osc_env.RenameEnvironment(self.app, None) @mock.patch('osc_lib.utils.get_item_properties') def test_environment_rename(self, mock_util): arglist = ['1234', 'fake-1'] verifylist = [('id', '1234'), ('name', 'fake-1')] mock_util.return_value = ('1234', 'fake-1', 'fake deployed', '2015-12-16T17:31:54', '2015-12-16T17:31:54' ) parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('1234', 'fake-1', 'fake deployed', '2015-12-16T17:31:54', '2015-12-16T17:31:54')] self.assertEqual(expected_data, data) class TestEnvironmentSessionCreate(TestEnvironment): def setUp(self): super(TestEnvironmentSessionCreate, self).setUp() # Command to test self.cmd = osc_env.EnvironmentSessionCreate(self.app, None) @mock.patch('muranoclient.common.utils.text_wrap_formatter') def test_environment_session_create(self, mock_util): arglist = ['1234'] verifylist = [('id', '1234')] mock_util.return_value = '1abc2xyz' parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['id'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = ['1abc2xyz'] self.assertEqual(expected_data, data) class TestEnvironmentCreate(TestEnvironment): def setUp(self): super(TestEnvironmentCreate, self).setUp() self.environment_mock.create.return_value = [api_env.Environment(None, ENV_INFO)] # Command to test self.cmd = osc_env.EnvironmentCreate(self.app, None) @mock.patch('osc_lib.utils.get_item_properties') def test_environment_create_with_no_option(self, mock_util): arglist = ['fake'] verifylist = [('name', 'fake')] mock_util.return_value = ('1234', 'fake', 'ready', '2015-12-16T17:31:54', '2015-12-16T17:31:54') parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('1234', 'fake', 'ready', '2015-12-16T17:31:54', '2015-12-16T17:31:54')] self.assertEqual(expected_data, data) @mock.patch('osc_lib.utils.get_item_properties') def test_environment_create_with_region_option(self, mock_util): arglist = ['fake', '--region', 'region_one'] verifylist = [('name', 'fake'), ('region', 'region_one')] mock_util.return_value = ('1234', 'fake', 'ready', '2015-12-16T17:31:54', '2015-12-16T17:31:54') parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that correct arguments are passed self.environment_mock.create.assert_has_calls([mock.call( {'name': 'fake', 'region': 'region_one'})]) # Check that columns are correct expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('1234', 'fake', 'ready', '2015-12-16T17:31:54', '2015-12-16T17:31:54')] self.assertEqual(expected_data, data) @mock.patch('osc_lib.utils.get_item_properties') def test_environment_create_with_net_option(self, mock_util): arglist = ['fake', '--join-net-id', 'x1y2z3'] verifylist = [('name', 'fake'), ('join_net_id', 'x1y2z3')] mock_util.return_value = ('1234', 'fake', 'ready', '2015-12-16T17:31:54', '2015-12-16T17:31:54') parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) expected_call = { 'defaultNetworks': { 'environment': { 'internalNetworkName': 'x1y2z3', '?': { 'type': 'io.murano.resources.ExistingNeutronNetwork', 'id': mock.ANY } }, 'flat': None }, 'name': 'fake', 'region': None } # Check that correct arguments are passed self.environment_mock.create.assert_called_with(expected_call) # Check that columns are correct expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('1234', 'fake', 'ready', '2015-12-16T17:31:54', '2015-12-16T17:31:54')] self.assertEqual(expected_data, data) @mock.patch('osc_lib.utils.get_item_properties') def test_environment_create_with_subnet_option(self, mock_util): arglist = ['fake', '--join-subnet-id', 'x1y2z3'] verifylist = [('name', 'fake'), ('join_subnet_id', 'x1y2z3')] mock_util.return_value = ('1234', 'fake', 'ready', '2015-12-16T17:31:54', '2015-12-16T17:31:54') parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) expected_call = { 'defaultNetworks': { 'environment': { 'internalSubnetworkName': 'x1y2z3', '?': { 'type': 'io.murano.resources.ExistingNeutronNetwork', 'id': mock.ANY } }, 'flat': None }, 'name': 'fake', 'region': None } # Check that correct arguments are passed self.environment_mock.create.assert_called_with(expected_call) # Check that columns are correct expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('1234', 'fake', 'ready', '2015-12-16T17:31:54', '2015-12-16T17:31:54')] self.assertEqual(expected_data, data) class TestEnvironmentDelete(TestEnvironment): def setUp(self): super(TestEnvironmentDelete, self).setUp() self.environment_mock.delete.return_value = None self.environment_mock.list.return_value = [api_env.Environment(None, ENV_INFO)] # Command to test self.cmd = osc_env.EnvironmentDelete(self.app, None) @mock.patch('osc_lib.utils.get_item_properties') def test_environment_delete(self, mock_util): arglist = ['fake1', 'fake2'] verifylist = [('id', ['fake1', 'fake2'])] mock_util.return_value = ('1234', 'Environment of all tenants', 'fake deployed', '2015-12-16T17:31:54', '2015-12-16T17:31:54' ) parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = [('1234', 'Environment of all tenants', 'fake deployed', '2015-12-16T17:31:54', '2015-12-16T17:31:54')] self.assertEqual(expected_data, data) class TestEnvironmentDeploy(TestEnvironment): def setUp(self): super(TestEnvironmentDeploy, self).setUp() mock_to_dict = self.environment_mock.get.return_value.to_dict mock_to_dict.return_value = ENV_INFO # Command to test self.cmd = osc_env.EnvironmentDeploy(self.app, None) @mock.patch('oslo_serialization.jsonutils.dumps') def test_environment_deploy(self, mock_json): arglist = ['fake', '--session-id', 'abc123'] verifylist = [('id', 'fake'), ('session_id', 'abc123')] mock_json.return_value = ['fake services'] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ('created', 'id', 'name', 'networking', 'services', 'status', 'tenant_id', 'updated', 'version') self.assertEqual(expected_columns, columns) # Check that data is correct expected_data = ('2015-12-16T17:31:54', '1234', 'Fake Environment', {}, ['fake services'], 'fake deployed', 'xyz123', '2015-12-16T17:31:54', '1') self.assertEqual(expected_data, data) class TestEnvironmentAppsEdit(TestEnvironment): def setUp(self): super(TestEnvironmentAppsEdit, self).setUp() # Command to test self.cmd = osc_env.EnvironmentAppsEdit(self.app, None) def test_environment_apps_edit(self): fake = collections.namedtuple('fakeEnv', 'services') self.environment_mock.get.side_effect = [ fake(services=[ {'?': {'name': "foo"}} ]), ] temp_file = tempfile.NamedTemporaryFile(prefix="murano-test", mode='w') json.dump([ {'op': 'replace', 'path': '/0/?/name', 'value': "dummy" } ], temp_file) temp_file.file.flush() arglist = ['fake', '--session-id', 'abc123', temp_file.name] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.services_mock.put.assert_called_once_with( 'fake', session_id='abc123', path='/', data=[{'?': {'name': 'dummy'}}] ) class TestEnvironmentModelShow(TestEnvironment): def setUp(self): super(TestEnvironmentModelShow, self).setUp() self.env_mock = \ self.app.client_manager.application_catalog.environments self.env_mock.get_model.return_value = ENV_MODEL # Command to test self.cmd = osc_env.EnvironmentModelShow(self.app, None) def test_environment_model_show_basic(self): arglist = ['env-id'] verifylist = [('id', 'env-id')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ('?', 'defaultNetworks', 'name', 'region') self.assertEqual(expected_columns, columns) # Check that data is correct self.assertCountEqual(ENV_MODEL.values(), data) def test_environment_model_show_full(self): arglist = ['env-id', '--path', '/path', '--session-id', 'sess-id'] verifylist = [('id', 'env-id'), ('path', '/path'), ('session_id', 'sess-id')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ('?', 'defaultNetworks', 'name', 'region') self.assertEqual(expected_columns, columns) # Check that data is correct self.assertCountEqual(ENV_MODEL.values(), data) class TestEnvironmentModelEdit(TestEnvironment): def setUp(self): super(TestEnvironmentModelEdit, self).setUp() self.env_mock = \ self.app.client_manager.application_catalog.environments self.env_mock.update_model.return_value = ENV_MODEL # Command to test self.cmd = osc_env.EnvironmentModelEdit(self.app, None) def test_environment_model_edit(self): temp_file = tempfile.NamedTemporaryFile(prefix="murano-test", mode='w') patch = [{'op': 'replace', 'path': '/name', 'value': 'dummy'}] json.dump(patch, temp_file) temp_file.file.flush() arglist = ['env-id', temp_file.name, '--session-id', 'sess-id'] verifylist = [('id', 'env-id'), ('filename', temp_file.name), ('session_id', 'sess-id')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ('?', 'defaultNetworks', 'name', 'region') self.assertEqual(expected_columns, columns) # Check that data is correct self.assertCountEqual(ENV_MODEL.values(), data) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/test_package.py0000664000175000017500000006734700000000000026543 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import io import json import os import shutil import sys import tempfile from unittest import mock from testtools import matchers from muranoclient.common import exceptions as common_exceptions from muranoclient.common import utils as mc_utils from muranoclient.osc.v1 import package as osc_pkg from muranoclient.tests.unit.osc.v1 import fakes from muranoclient.tests.unit import test_utils from muranoclient.v1 import packages from osc_lib import exceptions as exc from osc_lib import utils import requests_mock make_pkg = test_utils.make_pkg FIXTURE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), 'fixture_data')) COLUMNS = ['Id', 'Name', 'Fully_qualified_name', 'Author', 'Active', 'Is public', 'Type', 'Version'] DATA = { 'class_definitions': ['com.example.apache.ApacheHttpServer'], 'updated': '2016-09-20T06:23:45.000000', 'description': 'Test description.\n', 'created': '2016-09-20T06:23:15.000000', 'author': 'Mirantis, Inc', 'enabled': True, 'owner_id': 'a203405ea871484a940850d6c0b8dfd9', 'tags': ['Server', 'WebServer', 'Apache', 'HTTP', 'HTML'], 'is_public': False, 'fully_qualified_name': 'com.example.apache.ApacheHttpServer', 'type': 'Application', 'id': '46860070-5f8a-4936-96e8-d7b89e5187d7', 'categories': [], 'name': 'Apache HTTP Server' } class TestPackage(fakes.TestApplicationCatalog): def setUp(self): super(TestPackage, self).setUp() self.package_mock = self.app.client_manager.application_catalog.\ packages self.package_mock.reset_mock() class TestCreatePackage(TestPackage): def setUp(self): super(TestCreatePackage, self).setUp() # Command to test self.cmd = osc_pkg.CreatePackage(self.app, None) def test_create_package_without_args(self): arglist = [] parsed_args = self.check_parser(self.cmd, arglist, []) error = self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) self.assertEqual('Provide --template for a HOT-based package, OR at ' 'least --classes-dir for a MuranoPL-based package', str(error)) def test_create_package_template_and_classes_args(self): heat_template = os.path.join(FIXTURE_DIR, 'heat-template.yaml') classes_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Classes') arglist = ['--template', heat_template, '--classes-dir', classes_dir] parsed_args = self.check_parser(self.cmd, arglist, []) error = self.assertRaises(exc.CommandError, self.cmd.take_action, parsed_args) self.assertEqual('Provide --template for a HOT-based package, OR' ' --classes-dir for a MuranoPL-based package', str(error)) def test_create_hot_based_package(self): with tempfile.NamedTemporaryFile() as f: RESULT_PACKAGE = f.name heat_template = os.path.join(FIXTURE_DIR, 'heat-template.yaml') logo = os.path.join(FIXTURE_DIR, 'logo.png') arglist = ['--template', heat_template, '--output', RESULT_PACKAGE, '-l', logo] parsed_args = self.check_parser(self.cmd, arglist, []) orig = sys.stdout try: sys.stdout = io.StringIO() self.cmd.take_action(parsed_args) finally: stdout = sys.stdout.getvalue() sys.stdout.close() sys.stdout = orig matchers.MatchesRegex(stdout, "Application package " "is available at {0}".format(RESULT_PACKAGE)) def test_create_mpl_package(self): with tempfile.NamedTemporaryFile() as f: RESULT_PACKAGE = f.name classes_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Classes') resources_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Resources') ui = os.path.join(FIXTURE_DIR, 'test-app', 'ui.yaml') arglist = ['-c', classes_dir, '-r', resources_dir, '-u', ui, '-o', RESULT_PACKAGE] parsed_args = self.check_parser(self.cmd, arglist, []) orig = sys.stdout try: sys.stdout = io.StringIO() self.cmd.take_action(parsed_args) finally: stdout = sys.stdout.getvalue() sys.stdout.close() sys.stdout = orig matchers.MatchesRegex(stdout, "Application package " "is available at {0}".format(RESULT_PACKAGE)) class TestPackageList(TestPackage): def setUp(self): super(TestPackageList, self).setUp() self.cmd = osc_pkg.ListPackages(self.app, None) self.package_mock.filter.return_value = \ [packages.Package(None, DATA)] utils.get_dict_properties = mock.MagicMock(return_value='') def test_package_list_defaults(self): arglist = [] parsed_args = self.check_parser(self.cmd, arglist, []) columns, data = self.cmd.take_action(parsed_args) self.package_mock.filter.assert_called_with( include_disabled=False, owned=False) self.assertEqual(COLUMNS, columns) def test_package_list_with_limit(self): arglist = ['--limit', '10'] parsed_args = self.check_parser(self.cmd, arglist, []) columns, data = self.cmd.take_action(parsed_args) self.package_mock.filter.assert_called_with( include_disabled=False, limit=10, owned=False) def test_package_list_with_marker(self): arglist = ['--marker', '12345'] parsed_args = self.check_parser(self.cmd, arglist, []) columns, data = self.cmd.take_action(parsed_args) self.package_mock.filter.assert_called_with( include_disabled=False, marker='12345', owned=False) def test_package_list_with_name(self): arglist = ['--name', 'mysql'] parsed_args = self.check_parser(self.cmd, arglist, []) columns, data = self.cmd.take_action(parsed_args) self.package_mock.filter.assert_called_with( include_disabled=False, name='mysql', owned=False) def test_package_list_with_fqn(self): arglist = ['--fqn', 'mysql'] parsed_args = self.check_parser(self.cmd, arglist, []) columns, data = self.cmd.take_action(parsed_args) self.package_mock.filter.assert_called_with( include_disabled=False, fqn='mysql', owned=False) class TestPackageDelete(TestPackage): def setUp(self): super(TestPackageDelete, self).setUp() self.package_mock.delete.return_value = None self.package_mock.filter.return_value = \ [packages.Package(None, DATA)] # Command to test self.cmd = osc_pkg.DeletePackage(self.app, None) @mock.patch('osc_lib.utils.get_item_properties') def test_package_delete(self, mock_util): arglist = ['fake1'] verifylist = [('id', ['fake1'])] mock_util.return_value = ('1234', 'Core library', 'io.murano', 'murano.io', '', 'True', 'Library', '0.0.0' ) parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct self.assertEqual(COLUMNS, columns) # Check that data is correct expected_data = [('1234', 'Core library', 'io.murano', 'murano.io', '', 'True', 'Library', '0.0.0')] self.assertEqual(expected_data, data) class TestPackageImport(TestPackage): def setUp(self): super(TestPackageImport, self).setUp() self.package_mock.filter.return_value = \ [packages.Package(None, DATA)] # Command to test self.cmd = osc_pkg.ImportPackage(self.app, None) @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import(self, from_file): with tempfile.NamedTemporaryFile() as f: RESULT_PACKAGE = f.name categories = ['Cat1', 'Cat2 with space'] pkg = make_pkg({'FullName': RESULT_PACKAGE}) from_file.return_value = mc_utils.Package(mc_utils.File(pkg)) arglist = [RESULT_PACKAGE, '--categories', categories, '--is-public'] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.package_mock.create.assert_called_once_with({ 'categories': [categories], 'is_public': True }, {RESULT_PACKAGE: mock.ANY},) def _test_conflict(self, packages, from_file, raw_input_mock, input_action, exists_action=''): packages.create = mock.MagicMock( side_effect=[common_exceptions.HTTPConflict("Conflict"), None]) packages.filter.return_value = [mock.Mock(id='test_id')] raw_input_mock.return_value = input_action with tempfile.NamedTemporaryFile() as f: pkg = make_pkg({'FullName': f.name}) from_file.return_value = mc_utils.Package(mc_utils.File(pkg)) if exists_action: arglist = [f.name, '--exists-action', exists_action] else: arglist = [f.name] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) return f.name @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_skip(self, from_file, raw_input_mock): name = self._test_conflict( self.package_mock, from_file, raw_input_mock, 's', ) self.package_mock.create.assert_called_once_with({ 'is_public': False, }, {name: mock.ANY},) @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_skip_ea(self, from_file, raw_input_mock): name = self._test_conflict( self.package_mock, from_file, raw_input_mock, '', exists_action='s', ) self.package_mock.create.assert_called_once_with({ 'is_public': False, }, {name: mock.ANY},) self.assertFalse(raw_input_mock.called) @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_abort(self, from_file, raw_input_mock): self.assertRaises(SystemExit, self._test_conflict, self.package_mock, from_file, raw_input_mock, 'a', ) self.package_mock.create.assert_called_once_with({ 'is_public': False, }, mock.ANY,) @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_abort_ea(self, from_file, raw_input_mock): self.assertRaises(SystemExit, self._test_conflict, self.package_mock, from_file, raw_input_mock, '', exists_action='a', ) self.package_mock.create.assert_called_once_with({ 'is_public': False, }, mock.ANY,) self.assertFalse(raw_input_mock.called) @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_update(self, from_file, raw_input_mock): name = self._test_conflict( self.package_mock, from_file, raw_input_mock, 'u', ) self.assertEqual(2, self.package_mock.create.call_count) self.package_mock.delete.assert_called_once_with('test_id') self.package_mock.create.assert_has_calls( [ mock.call({'is_public': False}, {name: mock.ANY},), mock.call({'is_public': False}, {name: mock.ANY},) ], any_order=True, ) @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_update_ea(self, from_file, raw_input_mock): name = self._test_conflict( self.package_mock, from_file, raw_input_mock, '', exists_action='u', ) self.assertEqual(2, self.package_mock.create.call_count) self.package_mock.delete.assert_called_once_with('test_id') self.package_mock.create.assert_has_calls( [ mock.call({'is_public': False}, {name: mock.ANY},), mock.call({'is_public': False}, {name: mock.ANY},) ], any_order=True, ) self.assertFalse(raw_input_mock.called) def _test_conflict_dep(self, packages, from_file, dep_exists_action=''): packages.create = mock.MagicMock( side_effect=[common_exceptions.HTTPConflict("Conflict"), common_exceptions.HTTPConflict("Conflict"), None]) packages.filter.return_value = [mock.Mock(id='test_id')] pkg1 = make_pkg( {'FullName': 'first_app', 'Require': {'second_app': '1.0'}, }) pkg2 = make_pkg({'FullName': 'second_app', }) def side_effect(name): if 'first_app' in name: return mc_utils.Package(mc_utils.File(pkg1)) if 'second_app' in name: return mc_utils.Package(mc_utils.File(pkg2)) from_file.side_effect = side_effect arglist = ['first_app', '--exists-action', 's', '--dep-exists-action', dep_exists_action] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_dep_skip_ea(self, from_file): self._test_conflict_dep( self.package_mock, from_file, dep_exists_action='s', ) self.assertEqual(2, self.package_mock.create.call_count) self.package_mock.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), ], any_order=True, ) @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_dep_abort_ea(self, from_file): self.assertRaises(SystemExit, self._test_conflict_dep, self.package_mock, from_file, dep_exists_action='a', ) self.package_mock.create.assert_called_with({ 'is_public': False, }, {'second_app': mock.ANY},) @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_dep_update_ea(self, from_file): self._test_conflict_dep( self.package_mock, from_file, dep_exists_action='u', ) self.assertGreater(self.package_mock.create.call_count, 2) self.assertLess(self.package_mock.create.call_count, 5) self.assertTrue(self.package_mock.delete.called) self.package_mock.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), ], any_order=True, ) @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_no_categories(self, from_file): with tempfile.NamedTemporaryFile() as f: RESULT_PACKAGE = f.name pkg = make_pkg({'FullName': RESULT_PACKAGE}) from_file.return_value = mc_utils.Package(mc_utils.File(pkg)) arglist = [RESULT_PACKAGE] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.package_mock.create.assert_called_once_with( {'is_public': False}, {RESULT_PACKAGE: mock.ANY}, ) @requests_mock.mock() @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_url(self, rm, from_file): filename = "http://127.0.0.1/test_package.zip" pkg = make_pkg({'FullName': 'test_package'}) from_file.return_value = mc_utils.Package(mc_utils.File(pkg)) rm.get(filename, body=make_pkg({'FullName': 'test_package'})) arglist = [filename] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.package_mock.create.assert_called_once_with( {'is_public': False}, {'test_package': mock.ANY}, ) @requests_mock.mock() @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_by_name(self, rm, from_file): filename = "io.test.apps.test_application" murano_repo_url = "http://127.0.0.1" pkg = make_pkg({'FullName': filename}) from_file.return_value = mc_utils.Package(mc_utils.File(pkg)) rm.get(murano_repo_url + '/apps/' + filename + '.zip', body=make_pkg({'FullName': 'first_app'})) arglist = [filename, '--murano-repo-url', murano_repo_url] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.assertTrue(self.package_mock.create.called) self.package_mock.create.assert_called_once_with( {'is_public': False}, {filename: mock.ANY}, ) @requests_mock.mock() def test_package_import_multiple(self, rm): filename = ["io.test.apps.test_application", "http://127.0.0.1/test_app2.zip", ] murano_repo_url = "http://127.0.0.1" rm.get(murano_repo_url + '/apps/' + filename[0] + '.zip', body=make_pkg({'FullName': 'first_app'})) rm.get(filename[1], body=make_pkg({'FullName': 'second_app'})) arglist = [filename[0], filename[1], '--murano-repo-url', murano_repo_url] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.assertEqual(2, self.package_mock.create.call_count) self.package_mock.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), ], any_order=True, ) class TestBundleImport(TestPackage): def setUp(self): super(TestBundleImport, self).setUp() # Command to test self.cmd = osc_pkg.ImportBundle(self.app, None) @requests_mock.mock() def test_import_bundle_by_name(self, m): """Asserts bundle import calls packages create once for each pkg.""" pkg1 = make_pkg({'FullName': 'first_app'}) pkg2 = make_pkg({'FullName': 'second_app'}) murano_repo_url = "http://127.0.0.1" m.get(murano_repo_url + '/apps/first_app.zip', body=pkg1) m.get(murano_repo_url + '/apps/second_app.1.0.zip', body=pkg2) s = io.StringIO() bundle_contents = {'Packages': [ {'Name': 'first_app'}, {'Name': 'second_app', 'Version': '1.0'} ]} json.dump(bundle_contents, s) s = io.BytesIO(s.getvalue().encode('ascii')) m.get(murano_repo_url + '/bundles/test_bundle.bundle', body=s) arglist = ["test_bundle", '--murano-repo-url', murano_repo_url] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.package_mock.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), ], any_order=True, ) @requests_mock.mock() def test_import_bundle_wrong_url(self, m): url = 'http://127.0.0.2/test_bundle.bundle' m.get(url, status_code=404) arglist = [url] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.assertFalse(self.package_mock.packages.create.called) @requests_mock.mock() def test_import_bundle_no_bundle(self, m): url = 'http://127.0.0.1/bundles/test_bundle.bundle' m.get(url, status_code=404) arglist = ["test_bundle"] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.assertFalse(self.package_mock.packages.create.called) @requests_mock.mock() def test_import_bundle_by_url(self, m): """Asserts bundle import calls packages create once for each pkg.""" pkg1 = make_pkg({'FullName': 'first_app'}) pkg2 = make_pkg({'FullName': 'second_app'}) murano_repo_url = 'http://127.0.0.1' m.get(murano_repo_url + '/apps/first_app.zip', body=pkg1) m.get(murano_repo_url + '/apps/second_app.1.0.zip', body=pkg2) s = io.StringIO() bundle_contents = {'Packages': [ {'Name': 'first_app'}, {'Name': 'second_app', 'Version': '1.0'} ]} json.dump(bundle_contents, s) s = io.BytesIO(s.getvalue().encode('ascii')) url = 'http://127.0.0.2/test_bundle.bundle' m.get(url, body=s) arglist = [url, '--murano-repo-url', murano_repo_url] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.package_mock.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), ], any_order=True, ) @requests_mock.mock() def test_import_local_bundle(self, m): """Asserts local bundles are first searched locally.""" tmp_dir = tempfile.mkdtemp() bundle_file = os.path.join(tmp_dir, 'bundle.bundle') with open(os.path.join(tmp_dir, 'bundle.bundle'), 'w') as f: bundle_contents = {'Packages': [ {'Name': 'first_app'}, {'Name': 'second_app', 'Version': '1.0'} ]} json.dump(bundle_contents, f) pkg1 = make_pkg({'FullName': 'first_app', 'Require': {'third_app': None}}) pkg2 = make_pkg({'FullName': 'second_app'}) pkg3 = make_pkg({'FullName': 'third_app'}) with open(os.path.join(tmp_dir, 'first_app'), 'wb') as f: f.write(pkg1.read()) with open(os.path.join(tmp_dir, 'third_app'), 'wb') as f: f.write(pkg3.read()) murano_repo_url = "http://127.0.0.1" m.get(murano_repo_url + '/apps/first_app.zip', status_code=404) m.get(murano_repo_url + '/apps/second_app.1.0.zip', body=pkg2) m.get(murano_repo_url + '/apps/third_app.zip', status_code=404) arglist = [bundle_file, '--murano-repo-url', murano_repo_url] parsed_args = self.check_parser(self.cmd, arglist, []) self.cmd.take_action(parsed_args) self.package_mock.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), mock.call({'is_public': False}, {'third_app': mock.ANY}), ], any_order=True, ) shutil.rmtree(tmp_dir) class TestShowPackage(TestPackage): def setUp(self): super(TestShowPackage, self).setUp() # Command to test self.cmd = osc_pkg.ShowPackage(self.app, None) def test_package_show(self): arglist = ['fake'] verifylist = [('id', 'fake')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) # Check that columns are correct expected_columns = ('categories', 'class_definitions', 'description', 'enabled', 'fully_qualified_name', 'id', 'is_public', 'name', 'owner_id', 'tags', 'type') self.assertEqual(expected_columns, columns) self.package_mock.get.assert_called_with('fake') class TestUpdatePackage(TestPackage): def setUp(self): super(TestUpdatePackage, self).setUp() self.package_mock.update.return_value = \ (mock.MagicMock(), mock.MagicMock()) # Command to test self.cmd = osc_pkg.UpdatePackage(self.app, None) def test_package_update(self): arglist = ['123', '--is-public', 'true'] verifylist = [('id', '123'), ('is_public', True)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.package_mock.update.assert_called_with('123', {'is_public': True}) arglist = ['123', '--enabled', 'true'] verifylist = [('id', '123'), ('enabled', True)] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.package_mock.update.assert_called_with('123', {'enabled': True}) arglist = ['123', '--name', 'foo', '--description', 'bar'] verifylist = [('id', '123'), ('name', 'foo'), ('description', 'bar')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.package_mock.update.assert_called_with( '123', {'name': 'foo', 'description': 'bar'}) arglist = ['123', '--tags', 'foo'] verifylist = [('id', '123'), ('tags', ['foo'])] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.package_mock.update.assert_called_with( '123', {'tags': ['foo']}) class TestDownloadPackage(TestPackage): def setUp(self): super(TestDownloadPackage, self).setUp() self.package_mock.download.return_value = \ b'This is a fake package buffer' # Command to test self.cmd = osc_pkg.DownloadPackage(self.app, None) def test_package_download(self): arglist = ['1234', '/tmp/foo.zip'] verifylist = [('id', '1234'), ('filename', '/tmp/foo.zip')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) self.cmd.take_action(parsed_args) self.package_mock.download.assert_called_with('1234') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/osc/v1/test_schema.py0000664000175000017500000000406000000000000026367 0ustar00zuulzuul00000000000000# Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from muranoclient.osc.v1 import schema as osc_schema from muranoclient.tests.unit.osc.v1 import fakes from muranoclient.v1 import schemas as api_schemas SAMPLE_CLASS_SCHEMA = { '': { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": {} }, 'modelBuilder': { "$schema": "http://json-schema.org/draft-04/schema#", "type": "object", "properties": {} } } class TestSchema(fakes.TestApplicationCatalog): def setUp(self): super(TestSchema, self).setUp() self.schemas_mock = \ self.app.client_manager.application_catalog.schemas self.schemas_mock.get.return_value = api_schemas.Schema( None, SAMPLE_CLASS_SCHEMA) self.cmd = osc_schema.ShowSchema(self.app, None) def test_query_class_schema(self): arglist = ['class.name', 'methodName1', '--package-name', 'package.name', '--class-version', '>1'] verifylist = [('class_name', 'class.name'), ('method_names', ['methodName1']), ('package_name', 'package.name'), ('class_version', '>1')] parsed_args = self.check_parser(self.cmd, arglist, verifylist) columns, data = self.cmd.take_action(parsed_args) expected_columns = ['', 'modelBuilder'] self.assertCountEqual(expected_columns, columns) self.assertCountEqual(tuple(SAMPLE_CLASS_SCHEMA.values()), data) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/test_base.py0000664000175000017500000000350300000000000024730 0ustar00zuulzuul00000000000000# Copyright 2015 Huawei. # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools from muranoclient.common import base from muranoclient.v1 import packages class BaseTest(testtools.TestCase): def test_two_resources_with_same_id_are_not_equal(self): # Two resources with same ID: never equal if their info is not equal r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) self.assertNotEqual(r1, r2) def test_two_resources_with_same_id_and_info_are_equal(self): # Two resources with same ID: equal if their info is equal r1 = base.Resource(None, {'id': 1, 'name': 'hello'}) r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) self.assertEqual(r1, r2) def test_two_resources_with_diff_type_are_not_equal(self): # Two resources of different types: never equal r1 = base.Resource(None, {'id': 1}) r2 = packages.Package(None, {'id': 1}) self.assertNotEqual(r1, r2) def test_two_resources_with_no_id_are_equal(self): # Two resources with no ID: equal if their info is equal r1 = base.Resource(None, {'name': 'joe', 'age': 12}) r2 = base.Resource(None, {'name': 'joe', 'age': 12}) self.assertEqual(r1, r2) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/test_common_http.py0000664000175000017500000004444600000000000026360 0ustar00zuulzuul00000000000000# -*- coding:utf-8 -*- # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import socket from unittest import mock import testtools from muranoclient.common import exceptions as exc from muranoclient.common import http from muranoclient.tests.unit import fakes @mock.patch('muranoclient.common.http.requests.request') class HttpClientTest(testtools.TestCase): def test_http_raw_request(self, mock_request): headers = {'User-Agent': 'python-muranoclient'} mock_request.return_value = \ fakes.FakeHTTPResponse( 200, 'OK', {}, '') client = http.HTTPClient('http://example.com:8082') resp = client.request('', 'GET') self.assertEqual(200, resp.status_code) self.assertEqual('', ''.join([x for x in resp.content])) mock_request.assert_called_with('GET', 'http://example.com:8082', allow_redirects=False, headers=headers) def test_token_or_credentials(self, mock_request): # Record a 200 fake200 = fakes.FakeHTTPResponse( 200, 'OK', {}, '') mock_request.side_effect = [fake200, fake200, fake200] # Replay, create client, assert client = http.HTTPClient('http://example.com:8082') resp = client.request('', 'GET') self.assertEqual(200, resp.status_code) client.username = 'user' client.password = 'pass' resp = client.request('', 'GET') self.assertEqual(200, resp.status_code) client.auth_token = 'abcd1234' resp = client.request('', 'GET') self.assertEqual(200, resp.status_code) # no token or credentials mock_request.assert_has_calls([ mock.call('GET', 'http://example.com:8082', allow_redirects=False, headers={'User-Agent': 'python-muranoclient'}), mock.call('GET', 'http://example.com:8082', allow_redirects=False, headers={'User-Agent': 'python-muranoclient', 'X-Auth-Key': 'pass', 'X-Auth-User': 'user'}), mock.call('GET', 'http://example.com:8082', allow_redirects=False, headers={'User-Agent': 'python-muranoclient', 'X-Auth-Token': 'abcd1234'}) ]) def test_region_name(self, mock_request): # Record a 200 fake200 = fakes.FakeHTTPResponse( 200, 'OK', {}, '') mock_request.return_value = fake200 client = http.HTTPClient('http://example.com:8082') client.region_name = 'RegionOne' resp = client.request('', 'GET') self.assertEqual(200, resp.status_code) mock_request.assert_called_once_with( 'GET', 'http://example.com:8082', allow_redirects=False, headers={'X-Region-Name': 'RegionOne', 'User-Agent': 'python-muranoclient'}) def test_http_json_request(self, mock_request): # Record a 200 mock_request.return_value = \ fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, '{}') client = http.HTTPClient('http://example.com:8082') resp, body = client.json_request('', 'GET') self.assertEqual(200, resp.status_code) self.assertEqual({}, body) mock_request.assert_called_once_with( 'GET', 'http://example.com:8082', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) def test_http_json_request_argument_passed_to_requests(self, mock_request): """Check that we have sent the proper arguments to requests.""" # Record a 200 mock_request.return_value = \ fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, '{}') client = http.HTTPClient('http://example.com:8082') client.verify_cert = True client.cert_file = 'RANDOM_CERT_FILE' client.key_file = 'RANDOM_KEY_FILE' client.auth_url = 'http://AUTH_URL' resp, body = client.json_request('', 'GET', data='text') self.assertEqual(200, resp.status_code) self.assertEqual({}, body) mock_request.assert_called_once_with( 'GET', 'http://example.com:8082', allow_redirects=False, cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'), verify=True, data='"text"', headers={'Content-Type': 'application/json', 'X-Auth-Url': 'http://AUTH_URL', 'User-Agent': 'python-muranoclient'}) def test_http_json_request_w_req_body(self, mock_request): # Record a 200 mock_request.return_value = \ fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, '{}') client = http.HTTPClient('http://example.com:8082') resp, body = client.json_request('', 'GET', data='test-body') self.assertEqual(200, resp.status_code) self.assertEqual({}, body) mock_request.assert_called_once_with( 'GET', 'http://example.com:8082', data='"test-body"', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) def test_http_json_request_non_json_resp_cont_type(self, mock_request): # Record a 200 mock_request.return_value = \ fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'not/json'}, '{}') client = http.HTTPClient('http://example.com:8082') resp, body = client.json_request('', 'GET', data='test-data') self.assertEqual(200, resp.status_code) self.assertIsNone(body) mock_request.assert_called_once_with( 'GET', 'http://example.com:8082', data='"test-data"', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) def test_http_json_request_invalid_json(self, mock_request): # Record a 200 mock_request.return_value = \ fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, 'invalid-json') client = http.HTTPClient('http://example.com:8082') resp, body = client.json_request('', 'GET') self.assertEqual(200, resp.status_code) self.assertEqual('invalid-json', body) mock_request.assert_called_once_with( 'GET', 'http://example.com:8082', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) def test_http_manual_redirect_delete(self, mock_request): mock_request.side_effect = [ fakes.FakeHTTPResponse( 302, 'Found', {'location': 'http://example.com:8082/foo/bar'}, ''), fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, '{}')] client = http.HTTPClient('http://example.com:8082/foo') resp, body = client.json_request('', 'DELETE') self.assertEqual(200, resp.status_code) mock_request.assert_has_calls([ mock.call('DELETE', 'http://example.com:8082/foo', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}), mock.call('DELETE', 'http://example.com:8082/foo/bar', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) ]) def test_http_manual_redirect_post(self, mock_request): mock_request.side_effect = [ fakes.FakeHTTPResponse( 302, 'Found', {'location': 'http://example.com:8082/foo/bar'}, ''), fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, '{}')] client = http.HTTPClient('http://example.com:8082/foo') resp, body = client.json_request('', 'POST') self.assertEqual(200, resp.status_code) mock_request.assert_has_calls([ mock.call('POST', 'http://example.com:8082/foo', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}), mock.call('POST', 'http://example.com:8082/foo/bar', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) ]) def test_http_manual_redirect_put(self, mock_request): mock_request.side_effect = [ fakes.FakeHTTPResponse( 302, 'Found', {'location': 'http://example.com:8082/foo/bar'}, ''), fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, '{}')] client = http.HTTPClient('http://example.com:8082/foo') resp, body = client.json_request('', 'PUT') self.assertEqual(200, resp.status_code) mock_request.assert_has_calls([ mock.call('PUT', 'http://example.com:8082/foo', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}), mock.call('PUT', 'http://example.com:8082/foo/bar', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) ]) def test_http_manual_redirect_prohibited(self, mock_request): mock_request.return_value = \ fakes.FakeHTTPResponse( 302, 'Found', {'location': 'http://example.com:8082/'}, '') client = http.HTTPClient('http://example.com:8082/foo') self.assertRaises(exc.InvalidEndpoint, client.json_request, '', 'DELETE') mock_request.assert_called_once_with( 'DELETE', 'http://example.com:8082/foo', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) def test_http_manual_redirect_error_without_location(self, mock_request): mock_request.return_value = \ fakes.FakeHTTPResponse( 302, 'Found', {}, '') client = http.HTTPClient('http://example.com:8082/foo') self.assertRaises(exc.InvalidEndpoint, client.json_request, '', 'DELETE') mock_request.assert_called_once_with( 'DELETE', 'http://example.com:8082/foo', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) def test_http_json_request_redirect(self, mock_request): # Record the 302 mock_request.side_effect = [ fakes.FakeHTTPResponse( 302, 'Found', {'location': 'http://example.com:8082'}, ''), fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, '{}')] client = http.HTTPClient('http://example.com:8082') resp, body = client.json_request('', 'GET') self.assertEqual(200, resp.status_code) self.assertEqual({}, body) mock_request.assert_has_calls([ mock.call('GET', 'http://example.com:8082', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}), mock.call('GET', 'http://example.com:8082', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) ]) def test_http_404_json_request(self, mock_request): mock_request.return_value = \ fakes.FakeHTTPResponse( 404, 'Not Found', {'content-type': 'application/json'}, '{}') client = http.HTTPClient('http://example.com:8082') e = self.assertRaises(exc.HTTPNotFound, client.json_request, '', 'GET') # Assert that the raised exception can be converted to string self.assertIsNotNone(str(e)) # Record a 404 mock_request.assert_called_once_with( 'GET', 'http://example.com:8082', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) def test_http_300_json_request(self, mock_request): mock_request.return_value = \ fakes.FakeHTTPResponse( 300, 'OK', {'content-type': 'application/json'}, '{}') client = http.HTTPClient('http://example.com:8082') e = self.assertRaises( exc.HTTPMultipleChoices, client.json_request, '', 'GET') # Assert that the raised exception can be converted to string self.assertIsNotNone(str(e)) # Record a 300 mock_request.assert_called_once_with( 'GET', 'http://example.com:8082', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}) def test_fake_json_request(self, mock_request): headers = {'User-Agent': 'python-muranoclient'} mock_request.side_effect = [socket.gaierror] client = http.HTTPClient('fake://example.com:8082') self.assertRaises(exc.InvalidEndpoint, client.request, "/", "GET") mock_request.assert_called_once_with('GET', 'fake://example.com:8082/', allow_redirects=False, headers=headers) def test_http_request_socket_error(self, mock_request): headers = {'User-Agent': 'python-muranoclient'} mock_request.side_effect = [socket.gaierror] client = http.HTTPClient('http://example.com:8082') self.assertRaises(exc.InvalidEndpoint, client.request, "/", "GET") mock_request.assert_called_once_with('GET', 'http://example.com:8082/', allow_redirects=False, headers=headers) def test_http_request_socket_timeout(self, mock_request): headers = {'User-Agent': 'python-muranoclient'} mock_request.side_effect = [socket.timeout] client = http.HTTPClient('http://example.com:8082') self.assertRaises(exc.CommunicationError, client.request, "/", "GET") mock_request.assert_called_once_with('GET', 'http://example.com:8082/', allow_redirects=False, headers=headers) def test_http_request_specify_timeout(self, mock_request): mock_request.return_value = \ fakes.FakeHTTPResponse( 200, 'OK', {'content-type': 'application/json'}, '{}') client = http.HTTPClient('http://example.com:8082', timeout='123') resp, body = client.json_request('', 'GET') self.assertEqual(200, resp.status_code) self.assertEqual({}, body) mock_request.assert_called_once_with( 'GET', 'http://example.com:8082', allow_redirects=False, headers={'Content-Type': 'application/json', 'User-Agent': 'python-muranoclient'}, timeout=float(123)) def test_get_system_ca_file(self, mock_request): chosen = '/etc/ssl/certs/ca-certificates.crt' with mock.patch('os.path.exists') as mock_os: mock_os.return_value = chosen ca = http.get_system_ca_file() self.assertEqual(chosen, ca) mock_os.assert_called_once_with(chosen) def test_insecure_verify_cert_None(self, mock_request): client = http.HTTPClient('https://foo', insecure=True) self.assertFalse(client.verify_cert) def test_passed_cert_to_verify_cert(self, mock_request): client = http.HTTPClient('https://foo', cacert="NOWHERE") self.assertEqual("NOWHERE", client.verify_cert) with mock.patch('muranoclient.common.http.get_system_ca_file') as gsf: gsf.return_value = "SOMEWHERE" client = http.HTTPClient('https://foo') self.assertEqual("SOMEWHERE", client.verify_cert) # def test_curl_log_i18n_headers(self, mock_request): # self.m.StubOutWithMock(logging.Logger, 'debug') # kwargs = {'headers': {'Key': b'foo\xe3\x8a\x8e'}} # # mock_logging_debug = logging.Logger.debug( # u"curl -i -X GET -H 'Key: foo㊎' http://somewhere" # ) # mock_logging_debug.AndReturn(None) # # self.m.ReplayAll() # # client = http.HTTPClient('http://somewhere') # client.log_curl_request('', "GET", kwargs=kwargs) # # self.m.VerifyAll() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/test_exc.py0000664000175000017500000000406600000000000024602 0ustar00zuulzuul00000000000000# Copyright 2012 OpenStack Foundation # All Rights Reserved. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools from unittest import mock from muranoclient.common import exceptions as exc HTML_MSG = """ 403 Forbidden

403 Forbidden

Access was denied to this resource.

""" class TestHTTPExceptions(testtools.TestCase): def test_handles_json(self): """exc.from_response should not print JSON.""" mock_resp = mock.Mock() mock_resp.status_code = 413 mock_resp.json.return_value = { "overLimit": { "code": 413, "message": "OverLimit Retry...", "details": "Error Details...", "retryAt": "2015-08-31T21:21:06Z" } } mock_resp.headers = { "content-type": "application/json" } err = exc.from_response(mock_resp) self.assertIsInstance(err, exc.HTTPOverLimit) self.assertEqual("OverLimit Retry...", err.details) def test_handles_html(self): """exc.from_response should not print HTML.""" mock_resp = mock.Mock() mock_resp.status_code = 403 mock_resp.text = HTML_MSG mock_resp.headers = { "content-type": "text/html" } err = exc.from_response(mock_resp) self.assertIsInstance(err, exc.HTTPForbidden) self.assertEqual("403 Forbidden: Access was denied to this resource.", err.details) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/test_methods.py0000664000175000017500000002772200000000000025472 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import testtools from unittest import mock from muranoclient import client from muranoclient.v1 import actions from muranoclient.v1 import deployments import muranoclient.v1.environments as environments from muranoclient.v1 import packages import muranoclient.v1.sessions as sessions from muranoclient.v1 import static_actions import muranoclient.v1.templates as templates def my_mock(*a, **b): return [a, b] api = mock.MagicMock(json_request=my_mock) class UnitTestsForClassesAndFunctions(testtools.TestCase): def test_create_client_instance(self): endpoint = 'http://no-resolved-host:8001' test_client = client.Client('1', endpoint=endpoint, token='1', timeout=10) self.assertIsNotNone(test_client.environments) self.assertIsNotNone(test_client.sessions) self.assertIsNotNone(test_client.services) def test_env_manager_list(self): manager = environments.EnvironmentManager(api) result = manager.list() self.assertEqual([], result) def test_env_manager_create(self): manager = environments.EnvironmentManager(api) result = manager.create({'name': 'test'}) self.assertEqual({'name': 'test'}, result.data) def test_env_manager_create_with_named_parameters(self): manager = environments.EnvironmentManager(api) result = manager.create(data={'name': 'test'}) self.assertEqual({'name': 'test'}, result.data) def test_env_manager_create_negative_without_parameters(self): manager = environments.EnvironmentManager(api) self.assertRaises(TypeError, manager.create) def test_env_manager_delete(self): manager = environments.EnvironmentManager(api) result = manager.delete('test') self.assertIsNone(result) def test_env_manager_delete_with_named_parameters(self): manager = environments.EnvironmentManager(api) result = manager.delete(environment_id='1') self.assertIsNone(result) def test_env_manager_delete_negative_without_parameters(self): manager = environments.EnvironmentManager(api) self.assertRaises(TypeError, manager.delete) def test_env_manager_update(self): manager = environments.EnvironmentManager(api) result = manager.update('1', 'test') self.assertEqual({'name': 'test'}, result.data) def test_env_manager_update_with_named_parameters(self): manager = environments.EnvironmentManager(api) result = manager.update(environment_id='1', name='test') self.assertEqual({'name': 'test'}, result.data) def test_env_manager_update_negative_with_one_parameter(self): manager = environments.EnvironmentManager(api) self.assertRaises(TypeError, manager.update, 'test') def test_env_manager_update_negative_without_parameters(self): manager = environments.EnvironmentManager(api) self.assertRaises(TypeError, manager.update) def test_env_manager_get(self): manager = environments.EnvironmentManager(api) result = manager.get('test') self.assertIsNotNone(result.manager) def test_env(self): environment = environments.Environment(api, api) self.assertIsNotNone(environment.data()) def test_session_manager_delete(self): manager = sessions.SessionManager(api) result = manager.delete('datacenter1', 'session1') self.assertIsNone(result) def test_session_manager_delete_with_named_parameters(self): manager = sessions.SessionManager(api) result = manager.delete(environment_id='datacenter1', session_id='session1') self.assertIsNone(result) def test_session_manager_delete_negative_with_one_parameter(self): manager = sessions.SessionManager(api) self.assertRaises(TypeError, manager.delete, 'datacenter1') def test_session_manager_delete_negative_without_parameters(self): manager = sessions.SessionManager(api) self.assertRaises(TypeError, manager.delete) def test_session_manager_get(self): manager = sessions.SessionManager(api) result = manager.get('datacenter1', 'session1') # WTF? self.assertIsNotNone(result.manager) def test_session_manager_configure(self): manager = sessions.SessionManager(api) result = manager.configure('datacenter1') self.assertIsNotNone(result) def test_session_manager_configure_with_named_parameter(self): manager = sessions.SessionManager(api) result = manager.configure(environment_id='datacenter1') self.assertIsNotNone(result) def test_session_manager_configure_negative_without_parameters(self): manager = sessions.SessionManager(api) self.assertRaises(TypeError, manager.configure) def test_session_manager_deploy(self): manager = sessions.SessionManager(api) result = manager.deploy('datacenter1', '1') self.assertIsNone(result) def test_session_manager_deploy_with_named_parameters(self): manager = sessions.SessionManager(api) result = manager.deploy(environment_id='datacenter1', session_id='1') self.assertIsNone(result) def test_session_manager_deploy_negative_with_one_parameter(self): manager = sessions.SessionManager(api) self.assertRaises(TypeError, manager.deploy, 'datacenter1') def test_session_manager_deploy_negative_without_parameters(self): manager = sessions.SessionManager(api) self.assertRaises(TypeError, manager.deploy) def test_action_manager_call(self): api_mock = mock.MagicMock( json_request=lambda *args, **kwargs: (None, {'task_id': '1234'})) manager = actions.ActionManager(api_mock) result = manager.call('testEnvId', 'testActionId', ['arg1', 'arg2']) self.assertEqual('1234', result) def test_package_filter_pagination_next_marker(self): # ``PackageManager.filter`` handles `next_marker` parameter related # to pagination in API correctly. responses = [ {'next_marker': 'test_marker', 'packages': [{'name': 'test_package_1'}]}, {'packages': [{'name': 'test_package_2'}]} ] def json_request(url, method, *args, **kwargs): self.assertIn('/v1/catalog/packages', url) return mock.MagicMock(), responses.pop(0) api = mock.MagicMock() api.configure_mock(**{'json_request.side_effect': json_request}) manager = packages.PackageManager(api) list(manager.filter()) self.assertEqual(2, api.json_request.call_count) def test_package_filter_encoding_good(self): responses = [ {'next_marker': 'test_marker', 'packages': [{'name': 'test_package_1'}]}, {'packages': [{'name': 'test_package_2'}]} ] def json_request(url, method, *args, **kwargs): self.assertIn('category=%D0%BF%D0%B8%D0%B2%D0%BE', url) return mock.MagicMock(), responses.pop(0) api = mock.MagicMock() api.configure_mock(**{'json_request.side_effect': json_request}) manager = packages.PackageManager(api) category = b'\xd0\xbf\xd0\xb8\xd0\xb2\xd0\xbe' kwargs = {'category': category.decode('utf-8')} list(manager.filter(**kwargs)) self.assertEqual(2, api.json_request.call_count) def test_action_manager_get_result(self): api_mock = mock.MagicMock( json_request=lambda *args, **kwargs: (None, {'a': 'b'})) manager = actions.ActionManager(api_mock) result = manager.get_result('testEnvId', '1234') self.assertEqual({'a': 'b'}, result) def test_static_action_manager_call(self): api_mock = mock.MagicMock( json_request=lambda *args, **kwargs: (None, 'result')) manager = static_actions.StaticActionManager(api_mock) args = {'className': 'cls', 'methodName': 'method'} result = manager.call(args).get_result() self.assertEqual('result', result) def test_env_template_manager_list(self): """Tests the list of environment templates.""" manager = templates.EnvTemplateManager(api) result = manager.list() self.assertEqual([], result) def test_env_template_manager_create(self): manager = templates.EnvTemplateManager(api) result = manager.create({'name': 'test'}) self.assertEqual({'name': 'test'}, result.data) def test_env_template_manager_create_with_named_parameters(self): manager = templates.EnvTemplateManager(api) result = manager.create(data={'name': 'test'}) self.assertEqual({'name': 'test'}, result.data) def test_env_template_manager_create_negative_without_parameters(self): manager = templates.EnvTemplateManager(api) self.assertRaises(TypeError, manager.create) def test_env_template_manager_delete(self): manager = templates.EnvTemplateManager(api) result = manager.delete('test') self.assertIsNone(result) def test_env_template_manager_delete_with_named_parameters(self): manager = templates.EnvTemplateManager(api) result = manager.delete(env_template_id='1') self.assertIsNone(result) def test_env_template_manager_delete_negative_without_parameters(self): manager = templates.EnvTemplateManager(api) self.assertRaises(TypeError, manager.delete) def test_env_template_manager_update(self): manager = templates.EnvTemplateManager(api) result = manager.update('1', 'test') self.assertEqual({'name': 'test'}, result.data) def test_env_template_manager_update_with_named_parameters(self): manager = templates.EnvTemplateManager(api) result = manager.update(env_template_id='1', name='test') self.assertEqual({'name': 'test'}, result.data) def test_env_template_manager_update_negative_with_one_parameter(self): manager = templates.EnvTemplateManager(api) self.assertRaises(TypeError, manager.update, 'test') def test_env_template_manager_update_negative_without_parameters(self): manager = templates.EnvTemplateManager(api) self.assertRaises(TypeError, manager.update) def test_env_template_manager_get(self): manager = templates.EnvTemplateManager(api) result = manager.get('test') self.assertIsNotNone(result.manager) def test_deployment_manager_list(self): manager = deployments.DeploymentManager(api) manager._list = mock.Mock(return_value=mock.sentinel.deployments) result = manager.list(mock.sentinel.environment_id) self.assertEqual(mock.sentinel.deployments, result) manager._list.assert_called_once_with( '/v1/environments/sentinel.environment_id/deployments', 'deployments') def test_deployment_manager_list_all_environments(self): manager = deployments.DeploymentManager(api) manager._list = mock.Mock(return_value=mock.sentinel.deployments) result = manager.list(mock.sentinel.environment_id, all_environments=True) self.assertEqual(mock.sentinel.deployments, result) manager._list.assert_called_once_with('/v1/deployments', 'deployments') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/test_package_creator.py0000664000175000017500000001731400000000000027135 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import shutil from muranoclient.apiclient import exceptions from muranoclient.tests.unit import base from muranoclient.v1.package_creator import hot_package from muranoclient.v1.package_creator import mpl_package FIXTURE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), 'fixture_data')) TEMPLATE = os.path.join(FIXTURE_DIR, 'heat-template.yaml') CLASSES_DIR = os.path.join(FIXTURE_DIR, 'test-app', 'Classes') RESOURCES_DIR = os.path.join(FIXTURE_DIR, 'test-app', 'Resources') UI = os.path.join(FIXTURE_DIR, 'test-app', 'ui.yaml') LOGO = os.path.join(FIXTURE_DIR, 'logo.png') class TestArgs(object): pass class PackageCreatorTest(base.TestAdditionalAsserts): def test_generate_hot_manifest(self): args = TestArgs() args.template = TEMPLATE args.name = 'test_name' args.author = 'TestAuthor' args.full_name = None args.tags = None args.description = None expected_manifest = { 'Format': 'Heat.HOT/1.0', 'Type': 'Application', 'FullName': 'io.murano.apps.generated.TestName', 'Name': 'test_name', 'Description': 'Heat-defined application ' 'for a template "heat-template.yaml"', 'Author': 'TestAuthor', 'Tags': ['Heat-generated'] } result_manifest = hot_package.generate_manifest(args) self.check_dict_is_subset(expected_manifest, result_manifest) def test_generate_hot_manifest_nonexistent_template(self): args = TestArgs() args.template = '/home/this/path/does/not/exist' self.assertRaises(exceptions.CommandError, hot_package.generate_manifest, args) def test_generate_hot_manifest_with_all_parameters(self): args = TestArgs() args.template = TEMPLATE args.name = 'test_name' args.author = 'TestAuthor' args.full_name = 'test.full.name.TestName' args.tags = ['test', 'tag', 'Heat'] args.description = 'Test description' expected_manifest = { 'Format': 'Heat.HOT/1.0', 'Type': 'Application', 'FullName': 'test.full.name.TestName', 'Name': 'test_name', 'Description': 'Test description', 'Author': 'TestAuthor', 'Tags': ['test', 'tag', 'Heat'] } result_manifest = hot_package.generate_manifest(args) self.check_dict_is_subset(expected_manifest, result_manifest) def test_generate_hot_manifest_template_not_yaml(self): args = TestArgs() args.template = LOGO args.name = None args.full_name = None self.assertRaises(exceptions.CommandError, hot_package.generate_manifest, args) def test_prepare_hot_package(self): args = TestArgs() args.template = TEMPLATE args.name = 'test_name' args.author = 'TestAuthor' args.full_name = 'test.full.name.TestName' args.tags = 'test, tag, Heat' args.description = 'Test description' args.resources_dir = RESOURCES_DIR args.logo = LOGO package_dir = hot_package.prepare_package(args) prepared_files = ['manifest.yaml', 'logo.png', 'template.yaml', 'Resources'] self.assertEqual(sorted(prepared_files), sorted(os.listdir(package_dir))) shutil.rmtree(package_dir) def test_generate_mpl_manifest(self): args = TestArgs() args.template = TEMPLATE args.classes_dir = CLASSES_DIR args.resources_dir = RESOURCES_DIR args.type = 'Application' args.author = 'TestAuthor' args.name = None args.full_name = None args.tags = None args.description = None expected_manifest = { 'Format': 'MuranoPL/1.0', 'Type': 'Application', 'Classes': {'io.murano.apps.test.APP': 'testapp.yaml'}, 'FullName': 'io.murano.apps.test.APP', 'Name': 'APP', 'Description': 'Description for the application is not provided', 'Author': 'TestAuthor', } result_manifest = mpl_package.generate_manifest(args) self.check_dict_is_subset(expected_manifest, result_manifest) def test_generate_mpl_manifest_with_all_parameters(self): args = TestArgs() args.template = TEMPLATE args.classes_dir = CLASSES_DIR args.resources_dir = RESOURCES_DIR args.type = 'Application' args.name = 'test_name' args.author = 'TestAuthor' args.full_name = 'test.full.name.TestName' args.tags = ['test', 'tag', 'Heat'] args.description = 'Test description' expected_manifest = { 'Format': 'MuranoPL/1.0', 'Type': 'Application', 'Classes': {'io.murano.apps.test.APP': 'testapp.yaml'}, 'FullName': 'test.full.name.TestName', 'Name': 'test_name', 'Description': 'Test description', 'Author': 'TestAuthor', 'Tags': ['test', 'tag', 'Heat'] } result_manifest = mpl_package.generate_manifest(args) self.check_dict_is_subset(expected_manifest, result_manifest) def test_generate_mpl_wrong_classes_dir(self): args = TestArgs() args.classes_dir = '/home/this/path/does/not/exist' expected = ("'--classes-dir' parameter should be a directory", ) try: mpl_package.generate_manifest(args) except exceptions.CommandError as message: self.assertEqual(expected, message.args) def test_prepare_mpl_wrong_resources_dir(self): args = TestArgs() args.template = TEMPLATE args.classes_dir = CLASSES_DIR args.resources_dir = '/home/this/path/does/not/exist' args.type = 'Application' args.name = 'Test' args.tags = '' args.ui = UI args.logo = LOGO args.full_name = 'test.full.name.TestName' args.author = 'TestAuthor' args.description = 'Test description' expected = ("'--resources-dir' parameter should be a directory", ) try: mpl_package.prepare_package(args) except exceptions.CommandError as message: self.assertEqual(expected, message.args) def test_prepare_mpl_package(self): args = TestArgs() args.template = TEMPLATE args.classes_dir = CLASSES_DIR args.resources_dir = RESOURCES_DIR args.type = 'Application' args.name = 'test_name' args.author = 'TestAuthor' args.full_name = 'test.full.name.TestName' args.tags = 'test, tag, Heat' args.description = 'Test description' args.ui = UI args.logo = LOGO prepared_files = ['UI', 'Classes', 'manifest.yaml', 'Resources', 'logo.png'] package_dir = mpl_package.prepare_package(args) self.assertEqual(sorted(prepared_files), sorted(os.listdir(package_dir))) shutil.rmtree(package_dir) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/test_shell.py0000664000175000017500000017765700000000000025153 0ustar00zuulzuul00000000000000# # Copyright (c) 2013 Mirantis, Inc. # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import filecmp import io import json import logging import os import re import shutil import sys import tempfile from unittest import mock import fixtures from keystoneclient import fixture from keystoneclient.fixture import v2 as ks_v2_fixture from keystoneclient.fixture import v3 as ks_v3_fixture from oslo_log import handlers from oslo_log import log import requests_mock from testtools import matchers from muranoclient.apiclient import exceptions from muranoclient.common import exceptions as common_exceptions from muranoclient.common import utils import muranoclient.shell from muranoclient.tests.unit import base from muranoclient.tests.unit import test_utils from muranoclient.v1 import shell as v1_shell make_pkg = test_utils.make_pkg FIXTURE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), 'fixture_data')) # RESULT_PACKAGE = os.path.join(FIXTURE_DIR, 'test-app.zip') FAKE_ENV = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_NAME': 'tenant_name', 'OS_AUTH_URL': 'http://no.where/v2.0'} FAKE_ENV2 = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', 'OS_AUTH_URL': 'http://no.where/v2.0'} FAKE_ENV_v3 = {'OS_USERNAME': 'username', 'OS_PASSWORD': 'password', 'OS_TENANT_ID': 'tenant_id', 'OS_USER_DOMAIN_NAME': 'domain_name', 'OS_AUTH_URL': 'http://no.where/v3'} def _create_ver_list(versions): return {'versions': {'values': versions}} class TestArgs(object): package_version = '' murano_repo_url = 'http://127.0.0.1' exists_action = '' dep_exists_action = '' is_public = False categories = [] class ShellTest(base.TestCaseShell): def make_env(self, exclude=None, fake_env=FAKE_ENV): env = dict((k, v) for k, v in fake_env.items() if k != exclude) self.useFixture(fixtures.MonkeyPatch('os.environ', env)) class ShellCommandTest(ShellTest): _msg_no_tenant_project = ('You must provide a project name or project' ' id via --os-project-name, --os-project-id,' ' env[OS_PROJECT_ID] or env[OS_PROJECT_NAME].' ' You may use os-project and os-tenant' ' interchangeably.',) def setUp(self): super(ShellCommandTest, self).setUp() def get_auth_endpoint(bound_self, args): return ('test', {}) self.useFixture(fixtures.MonkeyPatch( 'muranoclient.shell.MuranoShell._get_endpoint_and_kwargs', get_auth_endpoint)) self.client = mock.MagicMock() # To prevent log descriptors from being closed during # shell tests set a custom StreamHandler self.logger = log.getLogger(None).logger self.logger.level = logging.DEBUG self.color_handler = handlers.ColorHandler(sys.stdout) self.logger.addHandler(self.color_handler) def tearDown(self): super(ShellTest, self).tearDown() self.logger.removeHandler(self.color_handler) def shell(self, argstr, exitcodes=(0,)): orig = sys.stdout orig_stderr = sys.stderr try: sys.stdout = io.StringIO() sys.stderr = io.StringIO() _shell = muranoclient.shell.MuranoShell() _shell.main(argstr.split()) except SystemExit: exc_type, exc_value, exc_traceback = sys.exc_info() self.assertIn(exc_value.code, exitcodes) finally: stdout = sys.stdout.getvalue() sys.stdout.close() sys.stdout = orig stderr = sys.stderr.getvalue() sys.stderr.close() sys.stderr = orig_stderr return (stdout, stderr) def register_keystone_discovery_fixture(self, mreq): v2_url = "http://no.where/v2.0" v2_version = fixture.V2Discovery(v2_url) mreq.register_uri('GET', v2_url, json=_create_ver_list([v2_version]), status_code=200) def register_keystone_token_fixture(self, mreq): v2_token = ks_v2_fixture.Token(token_id='token') service = v2_token.add_service('application-catalog') service.add_endpoint('http://no.where', region='RegionOne') mreq.register_uri('POST', 'http://no.where/v2.0/tokens', json=v2_token, status_code=200) def test_help_unknown_command(self): self.assertRaises(exceptions.CommandError, self.shell, 'help foofoo') def test_help(self): required = [ r'.*?^usage: murano', r'.*?^\s+package-create\s+Create an application package.', r'.*?^See "murano help COMMAND" for help on a specific command', ] stdout, stderr = self.shell('help') for r in required: self.assertThat((stdout + stderr), matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_help_on_subcommand(self): required = [ '.*?^usage: murano package-create', '.*?^Create an application package.', ] stdout, stderr = self.shell('help package-create') for r in required: self.assertThat((stdout + stderr), matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_help_no_options(self): required = [ r'.*?^usage: murano', r'.*?^\s+package-create\s+Create an application package', r'.*?^See "murano help COMMAND" for help on a specific command', ] stdout, stderr = self.shell('') for r in required: self.assertThat((stdout + stderr), matchers.MatchesRegex(r, re.DOTALL | re.MULTILINE)) def test_no_username(self): required = ('You must provide a username via either --os-username or ' 'env[OS_USERNAME] or a token via --os-auth-token or ' 'env[OS_AUTH_TOKEN]',) self.make_env(exclude='OS_USERNAME') try: self.shell('package-list') except exceptions.CommandError as message: self.assertEqual(required, message.args) else: self.fail('CommandError not raised') def test_no_tenant_name(self): required = self._msg_no_tenant_project self.make_env(exclude='OS_TENANT_NAME') try: self.shell('package-list') except exceptions.CommandError as message: self.assertEqual(required, message.args) else: self.fail('CommandError not raised') def test_no_tenant_id(self): required = self._msg_no_tenant_project self.make_env(exclude='OS_TENANT_ID', fake_env=FAKE_ENV2) try: self.shell('package-list') except exceptions.CommandError as message: self.assertEqual(required, message.args) else: self.fail('CommandError not raised') def test_no_auth_url(self): required = ('You must provide an auth url' ' via either --os-auth-url or via env[OS_AUTH_URL]',) self.make_env(exclude='OS_AUTH_URL') try: self.shell('package-list') except exceptions.CommandError as message: self.assertEqual(required, message.args) else: self.fail('CommandError not raised') @mock.patch('muranoclient.v1.packages.PackageManager') @requests_mock.mock() def test_package_list(self, mock_package_manager, m_requests): self.client.packages = mock_package_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('package-list') self.client.packages.filter.assert_called_once_with( include_disabled=False, owned=False) @mock.patch('muranoclient.v1.packages.PackageManager') @requests_mock.mock() def test_package_list_with_limit(self, mock_package_manager, m_requests): self.client.packages = mock_package_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('package-list --limit 10') self.client.packages.filter.assert_called_once_with( include_disabled=False, limit=10, owned=False) @mock.patch('muranoclient.v1.packages.PackageManager') @requests_mock.mock() def test_package_list_with_marker(self, mock_package_manager, m_requests): self.client.packages = mock_package_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('package-list --marker 12345') self.client.packages.filter.assert_called_once_with( include_disabled=False, marker='12345', owned=False) @mock.patch('muranoclient.v1.packages.PackageManager') @requests_mock.mock() def test_package_list_with_name(self, mock_package_manager, m_requests): self.client.packages = mock_package_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('package-list --name mysql') self.client.packages.filter.assert_called_once_with( name='mysql', include_disabled=False, owned=False) @mock.patch('muranoclient.v1.packages.PackageManager') @requests_mock.mock() def test_package_list_with_fqn(self, mock_package_manager, m_requests): self.client.packages = mock_package_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('package-list --fqn mysql') self.client.packages.filter.assert_called_once_with( fqn='mysql', include_disabled=False, owned=False) @mock.patch('muranoclient.v1.packages.PackageManager') @requests_mock.mock() def test_package_show(self, mock_package_manager, m_requests): self.client.packages = mock_package_manager() mock_package = mock.MagicMock() mock_package.class_definitions = '' mock_package.categories = '' mock_package.tags = '' mock_package.description = '' self.client.packages.get.return_value = mock_package self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('package-show 1234') self.client.packages.get.assert_called_with('1234') @mock.patch('muranoclient.v1.packages.PackageManager') @requests_mock.mock() def test_package_update(self, mock_package_manager, m_requests): self.client.packages = mock_package_manager() mock_package = mock.MagicMock() mock_package.class_definitions = '' mock_package.categories = '' mock_package.tags = '' mock_package.description = '' self.client.packages.get.return_value = mock_package self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('package-update 123 --is-public true') self.shell('package-update 123 --is-public false') self.shell('package-update 123 --is-public false --enabled t') self.shell('package-update 123 --name foo --description bar') self.shell('package-update 123 --tags a') self.shell('package-update 123 --tags a ' + '--is-public f --enabled f ' + '--name foo ' + '--description bar',) self.client.packages.update.assert_has_calls([ mock.call('123', {'is_public': True}), mock.call('123', {'is_public': False}), mock.call('123', {'enabled': True, 'is_public': False}), mock.call('123', {'name': 'foo', 'description': 'bar'}), mock.call('123', {'tags': ['a']}), mock.call('123', { 'tags': ['a'], 'is_public': False, 'enabled': False, 'name': 'foo', 'description': 'bar', }), ]) @mock.patch('muranoclient.v1.packages.PackageManager') @requests_mock.mock() def test_package_delete(self, mock_package_manager, m_requests): self.client.packages = mock_package_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('package-delete 1234 4321') self.client.packages.delete.assert_has_calls([ mock.call('1234'), mock.call('4321')]) self.assertEqual(2, self.client.packages.delete.call_count) @mock.patch('muranoclient.v1.sessions.SessionManager') @requests_mock.mock() def test_environment_session_create(self, mock_manager, m_requests): self.client.sessions = mock_manager() self.client.sessions.configure.return_value.id = '123' self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-session-create 1234') self.client.sessions.configure.assert_has_calls([ mock.call('1234')]) @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_create(self, mock_manager, m_requests): self.client.environments = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-create foo') self.client.environments.create.assert_has_calls( [mock.call({'name': 'foo', 'region': None})]) self.client.environments.create.reset_mock() self.shell('environment-create --join-net 123 foo --region RegionOne') cc = self.client.environments.create expected_call = mock.call({ 'defaultNetworks': { 'environment': { 'internalNetworkName': '123', '?': { 'type': 'io.murano.resources.ExistingNeutronNetwork', 'id': mock.ANY } }, 'flat': None }, 'name': 'foo', 'region': 'RegionOne' }) self.assertEqual(expected_call, cc.call_args) @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_list(self, mock_manager, m_requests): self.client.environments = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-list') self.client.environments.list.assert_called_once_with(False, None) self.client.environments.list.reset_mock() self.shell('environment-list --all-tenants') self.client.environments.list.assert_called_once_with(True, None) self.client.environments.list.reset_mock() self.shell('environment-list --tenant ABC') self.client.environments.list.assert_called_once_with(False, 'ABC') @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_delete(self, mock_manager, m_requests): self.client.environments = mock_manager() self.client.environments.find.return_value.id = '123' self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-delete env1') self.client.environments.find.assert_has_calls([ mock.call(name='env1') ]) self.client.environments.delete.assert_has_calls([ mock.call('123', False) ]) @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_delete_with_abandon(self, mock_manager, m_requests): self.client.environments = mock_manager() self.client.environments.find.return_value.id = '123' self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-delete env1 --abandon') self.client.environments.find.assert_has_calls([ mock.call(name='env1') ]) self.client.environments.delete.assert_has_calls([ mock.call('123', True) ]) @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_rename(self, mock_manager, m_requests): self.client.environments = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-rename old-name-or-id new-name') self.client.environments.find.assert_called_once_with( name='old-name-or-id') self.assertEqual(1, self.client.environments.update.call_count) @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_show(self, mock_manager, m_requests): self.client.environments = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-show env-id-or-name') self.client.environments.find.assert_called_once_with( name='env-id-or-name') @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_model_show_basic(self, mock_manager, m_requests): self.client.environments = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-model-show env-id') self.client.environments.get_model.assert_called_once_with( 'env-id', '/', None) @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_model_show_full(self, mock_manager, m_requests): self.client.environments = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-model-show env-id ' '--path /path ' '--session-id sess-id') self.client.environments.get_model.assert_called_once_with( 'env-id', '/path', 'sess-id') @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_model_show_path_with_q(self, mock_manager, m_requests): self.client.environments = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-model-show env-id ' '--path /?/path') self.client.environments.get_model.assert_called_once_with( 'env-id', '/%3F/path', None) @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_model_edit(self, mock_manager, m_requests): self.client.environments = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.client.environments.get_model.return_value = {'name': "foo"} temp_file = tempfile.NamedTemporaryFile(prefix="murano-test", mode='w') patch = [{'op': 'replace', 'path': '/name', 'value': 'dummy'}] json.dump(patch, temp_file) temp_file.file.flush() self.shell('environment-model-edit env-id {0} ' '--session-id sess-id'.format(temp_file.name)) self.client.environments.update_model.assert_called_once_with( 'env-id', patch, 'sess-id') @mock.patch('muranoclient.v1.environments.EnvironmentManager') @mock.patch('muranoclient.v1.sessions.SessionManager') @requests_mock.mock() def test_environment_deploy(self, mock_manager, env_manager, m_requests): self.client.sessions = mock_manager() self.client.environments = env_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-deploy 12345 --session-id 54321') self.client.sessions.deploy.assert_called_once_with( '12345', '54321') @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_show_session(self, mock_manager, m_requests): self.client.environments = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-show 12345 --session-id 12345') self.client.environments.get.assert_called_once_with( 12345, session_id='12345') @mock.patch('muranoclient.v1.actions.ActionManager') @requests_mock.mock() def test_environment_action_call(self, mock_manager, m_requests): self.client.actions = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-action-call 12345 --action-id 54321') self.client.actions.call.assert_called_once_with( '12345', '54321', arguments={}) @mock.patch('muranoclient.v1.actions.ActionManager') @requests_mock.mock() def test_environment_action_call_args(self, mock_manager, m_requests): self.client.actions = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell("""environment-action-call 12345 --action-id 54321 --arguments foo=bar dictArg={"key1":"value1","key2":"value2"} listArg=["item1","item2","item3"] nullArg=null stringArg="null" intArg=5 compoundArg=["foo",14,{"key1":null,"key2":8}]""") self.client.actions.call.assert_called_once_with( '12345', '54321', arguments={ 'foo': 'bar', 'dictArg': {u'key1': u'value1', u'key2': u'value2'}, 'listArg': [u'item1', u'item2', u'item3'], 'nullArg': None, 'stringArg': u'null', 'intArg': 5, 'compoundArg': [u'foo', 14, {u'key1': None, u'key2': 8}] }) @mock.patch('muranoclient.v1.actions.ActionManager') @requests_mock.mock() def test_environment_action_get_result(self, mock_manager, m_requests): self.client.actions = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-action-get-result 12345 --task-id 54321') self.client.actions.get_result.assert_called_once_with( '12345', '54321') @mock.patch('muranoclient.v1.static_actions.StaticActionManager') @requests_mock.mock() def test_static_action_call_basic(self, mock_manager, m_requests): self.client.static_actions = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('static-action-call class.name method.name') self.client.static_actions.call.assert_called_once_with({ "className": 'class.name', "methodName": 'method.name', "packageName": None, "classVersion": '=0', "parameters": {} }) @mock.patch('muranoclient.v1.static_actions.StaticActionManager') @requests_mock.mock() def test_static_action_call_full(self, mock_manager, m_requests): self.client.static_actions = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('static-action-call class.name method.name ' '--package-name package.name --class-version ">1"') self.client.static_actions.call.assert_called_once_with({ "className": 'class.name', "methodName": 'method.name', "packageName": 'package.name', "classVersion": '">1"', "parameters": {} }) @mock.patch('muranoclient.v1.static_actions.StaticActionManager') @requests_mock.mock() def test_static_action_call_string_args(self, mock_manager, m_requests): self.client.static_actions = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('static-action-call class.name method.name ' '--arguments food=spam parrot=dead') self.client.static_actions.call.assert_called_once_with({ "className": 'class.name', "methodName": 'method.name', "packageName": None, "classVersion": '=0', "parameters": {'food': 'spam', 'parrot': 'dead'} }) @mock.patch('muranoclient.v1.static_actions.StaticActionManager') @requests_mock.mock() def test_static_action_call_json_args(self, mock_manager, m_requests): self.client.static_actions = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell("""static-action-call class.name method.name --arguments dictArg={"key1":"value1","key2":"value2"} listArg=["item1","item2","item3"] nullArg=null stringArg="null" intArg=5 compoundArg=["foo",14,{"key1":null,"key2":8}]""") self.client.static_actions.call.assert_called_once_with({ "className": 'class.name', "methodName": 'method.name', "packageName": None, "classVersion": '=0', "parameters": { 'dictArg': {u'key1': u'value1', u'key2': u'value2'}, 'listArg': [u'item1', u'item2', u'item3'], 'nullArg': None, 'stringArg': u'null', 'intArg': 5, 'compoundArg': [u'foo', 14, {u'key1': None, u'key2': 8}] } }) @mock.patch('muranoclient.v1.schemas.SchemaManager') @requests_mock.mock() def test_class_schema(self, mock_manager, m_requests): self.client.schemas = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('class-schema class.name') self.client.schemas.get.assert_called_once_with( 'class.name', [], package_name=None, class_version='=0' ) @mock.patch('muranoclient.v1.schemas.SchemaManager') @requests_mock.mock() def test_class_schema_with_methods(self, mock_manager, m_requests): self.client.schemas = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('class-schema class.name method1 method2') self.client.schemas.get.assert_called_once_with( 'class.name', ['method1', 'method2'], package_name=None, class_version='=0' ) @mock.patch('muranoclient.v1.schemas.SchemaManager') @requests_mock.mock() def test_class_schema_full(self, mock_manager, m_requests): self.client.schemas = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('class-schema class.name method1 method2 ' '--class-version >1.2.3 --package-name foo.bar') self.client.schemas.get.assert_called_once_with( 'class.name', ['method1', 'method2'], package_name='foo.bar', class_version='>1.2.3' ) @mock.patch('muranoclient.v1.templates.EnvTemplateManager') @requests_mock.mock() def test_env_template_delete(self, mock_manager, m_requests): self.client.env_templates = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('env-template-delete env1 env2') self.client.env_templates.delete.assert_has_calls([ mock.call('env1'), mock.call('env2')]) @mock.patch('muranoclient.v1.templates.EnvTemplateManager') @requests_mock.mock() def test_env_template_create(self, mock_manager, m_requests): self.client.env_templates = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('env-template-create env-name') self.client.env_templates.create.assert_called_once_with( {'name': 'env-name', 'is_public': False}) @mock.patch('muranoclient.v1.templates.EnvTemplateManager') @requests_mock.mock() def test_env_template_create_public(self, mock_manager, m_requests): self.client.env_templates = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('env-template-create --is-public env-name') self.client.env_templates.create.assert_called_once_with( {'name': 'env-name', 'is_public': True}) @mock.patch('muranoclient.v1.templates.EnvTemplateManager') @requests_mock.mock() def test_env_template_show(self, mock_manager, m_requests): self.client.env_templates = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('env-template-show env-id') self.client.env_templates.get.assert_called_once_with('env-id') @mock.patch('muranoclient.v1.templates.EnvTemplateManager') @requests_mock.mock() def test_env_template_create_env(self, mock_manager, m_requests): self.client.env_templates = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('env-template-create-env env-id env-name') self.client.env_templates.create_env.\ assert_called_once_with('env-id', {"name": 'env-name'}) @mock.patch('muranoclient.v1.templates.EnvTemplateManager') @requests_mock.mock() def test_env_template_create_env_with_region(self, mock_manager, m_requests): self.client.env_templates = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('env-template-create-env env-id env-name --region Region') self.client.env_templates.create_env.\ assert_called_once_with('env-id', {"name": 'env-name', "region": 'Region'}) @mock.patch('muranoclient.v1.templates.EnvTemplateManager') @requests_mock.mock() def test_env_template_clone(self, mock_manager, m_requests): self.client.env_templates = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('env-template-clone env-id env-name') self.client.env_templates.clone.assert_called_once_with( 'env-id', 'env-name') @mock.patch('muranoclient.v1.environments.EnvironmentManager') @mock.patch('muranoclient.v1.deployments.DeploymentManager') @requests_mock.mock() def test_deployments_show(self, mock_deployment_manager, mock_env_manager, m_requests): self.client.deployments = mock_deployment_manager() self.client.environments = mock_env_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('deployment-list env-id-or-name') self.client.environments.find.assert_called_once_with( name='env-id-or-name') self.assertEqual(1, self.client.deployments.list.call_count) @mock.patch('muranoclient.v1.deployments.DeploymentManager') @requests_mock.mock() def test_deployments_list_all_environments(self, mock_deployment_manager, m_requests): self.client.deployments = mock_deployment_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('deployment-list --all-environments') self.client.deployments.list.assert_called_once_with( None, True) @mock.patch('muranoclient.v1.deployments.DeploymentManager') @requests_mock.mock() def test_deployments_list_negative(self, mock_deployment_manager, m_requests): self.client.deployments = mock_deployment_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) e = self.assertRaises( exceptions.CommandError, self.shell, 'deployment-list env-id --all-environments') self.assertIn( 'Environment ID and all-environments flag cannot both be set.', e.__str__()) self.assertFalse(self.client.deployments.list.called) e = self.assertRaises( exceptions.CommandError, self.shell, 'deployment-list') self.assertIn( 'Either environment ID or all-environments flag must be set.', e.__str__()) self.assertFalse(self.client.deployments.list.called) @mock.patch('muranoclient.v1.services.ServiceManager') @mock.patch('muranoclient.v1.environments.EnvironmentManager') @requests_mock.mock() def test_environment_apps_edit(self, mock_env_manager, mock_services, m_requests): self.client.environments = mock_env_manager() self.client.services = mock_services() fake = collections.namedtuple('fakeEnv', 'services') self.client.environments.get.side_effect = [ fake(services=[ {'?': {'name': "foo"}} ]), ] temp_file = tempfile.NamedTemporaryFile(prefix="murano-test", mode='w') json.dump([ {'op': 'replace', 'path': '/0/?/name', 'value': "dummy" } ], temp_file) temp_file.file.flush() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) self.shell('environment-apps-edit 12345 {0} --session-id 4321'.format( temp_file.name)) self.client.services.put.assert_called_once_with( '12345', session_id='4321', path='/', data=[{'?': {'name': 'dummy'}}] ) @mock.patch('muranoclient.v1.services.ServiceManager') @requests_mock.mock() def test_app_show(self, mock_services, m_requests): self.client.services = mock_services() mock_app = mock.MagicMock() mock_app.name = "app_name" setattr(mock_app, '?', {'type': 'app_type', 'id': 'app_id'}) self.client.services.list.return_value = [mock_app] self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) result = self.shell('app-show env-id') required = ['Id', 'Name', 'Type', 'app_id', 'app_name', 'app_type'] for r in required: self.assertIn(r, result[0]) self.client.services.list.assert_called_once_with('env-id') @mock.patch('muranoclient.v1.services.ServiceManager') @requests_mock.mock() def test_app_show_empty_list(self, mock_services, m_requests): self.client.services = mock_services() self.client.services.list.return_value = [] self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) result = self.shell('app-show env-id') required = ['Id', 'Name', 'Type'] for r in required: self.assertIn(r, result[0]) self.client.services.list.assert_called_once_with('env-id') @mock.patch('muranoclient.v1.services.ServiceManager') @requests_mock.mock() def test_app_show_with_path(self, mock_services, m_requests): self.client.services = mock_services() mock_app = mock.MagicMock() mock_app.name = "app_name" setattr(mock_app, '?', {'type': 'app_type', 'id': 'app_id'}) self.client.services.get.return_value = mock_app self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) result = self.shell('app-show env-id --path app-id') required = ['Property', 'Value'] for r in required: self.assertIn(r, result[0]) self.client.services.get.assert_called_once_with('env-id', '/app-id') @mock.patch('muranoclient.v1.categories.CategoryManager') @requests_mock.mock() def test_category_list(self, mock_manager, m_requests): self.client.categories = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) result = self.shell('category-list') required = ['ID', 'Name'] for r in required: self.assertIn(r, result[0]) self.client.categories.list.assert_called_once_with() @mock.patch('muranoclient.v1.packages.PackageManager') @mock.patch('muranoclient.v1.categories.CategoryManager') @requests_mock.mock() def test_category_show(self, category_manager, pkg_manager, m_requests): self.client.packages = pkg_manager() self.client.categories = category_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) result = self.shell('category-show category-id') required = ['Property', 'Value', 'id', 'name', 'packages'] for r in required: self.assertIn(r, result[0]) self.client.categories.get.assert_called_once_with('category-id') @mock.patch('muranoclient.v1.categories.CategoryManager') @requests_mock.mock() def test_category_create(self, mock_manager, m_requests): self.client.categories = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) result = self.shell('category-create category-name') required = ['ID', 'Name'] for r in required: self.assertIn(r, result[0]) self.client.categories.add.assert_called_once_with( {'name': 'category-name'}) @mock.patch('muranoclient.v1.categories.CategoryManager') @requests_mock.mock() def test_category_delete(self, mock_manager, m_requests): self.client.categories = mock_manager() self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) result = self.shell('category-delete category-id') required = ['ID', 'Name'] for r in required: self.assertIn(r, result[0]) self.client.categories.delete.assert_called_once_with('category-id') self.client.categories.delete.side_effect =\ common_exceptions.HTTPNotFound() ex = self.assertRaises(exceptions.CommandError, self.shell, 'category-delete category-id') expected = 'Unable to find and delete any of the specified categories.' self.assertEqual(expected, str(ex)) class ShellPackagesOperations(ShellCommandTest): @requests_mock.mock() def test_create_hot_based_package(self, m_requests): self.useFixture(fixtures.MonkeyPatch( 'muranoclient.v1.client.Client', mock.MagicMock)) heat_template = os.path.join(FIXTURE_DIR, 'heat-template.yaml') logo = os.path.join(FIXTURE_DIR, 'logo.png') self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) with tempfile.NamedTemporaryFile() as f: RESULT_PACKAGE = f.name c = "package-create --template={0} --output={1} -l={2}".format( heat_template, RESULT_PACKAGE, logo) stdout, stderr = self.shell(c) matchers.MatchesRegex((stdout + stderr), "Application package " "is available at {0}".format(RESULT_PACKAGE)) @requests_mock.mock() def test_create_mpl_package(self, m_requests): self.useFixture(fixtures.MonkeyPatch( 'muranoclient.v1.client.Client', mock.MagicMock)) classes_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Classes') resources_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Resources') ui = os.path.join(FIXTURE_DIR, 'test-app', 'ui.yaml') self.make_env() self.register_keystone_discovery_fixture(m_requests) self.register_keystone_token_fixture(m_requests) with tempfile.NamedTemporaryFile() as f: RESULT_PACKAGE = f.name stdout, stderr = self.shell( "package-create -c={0} -r={1} -u={2} -o={3}".format( classes_dir, resources_dir, ui, RESULT_PACKAGE)) matchers.MatchesRegex((stdout + stderr), "Application package " "is available at {0}".format(RESULT_PACKAGE)) @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import(self, from_file): args = TestArgs() with tempfile.NamedTemporaryFile() as f: RESULT_PACKAGE = f.name args.filename = [RESULT_PACKAGE] args.categories = ['Cat1', 'Cat2 with space'] args.is_public = True pkg = make_pkg({'FullName': RESULT_PACKAGE}) from_file.return_value = utils.Package(utils.File(pkg)) v1_shell.do_package_import(self.client, args) self.client.packages.create.assert_called_once_with({ 'categories': ['Cat1', 'Cat2 with space'], 'is_public': True }, {RESULT_PACKAGE: mock.ANY},) def _test_conflict(self, packages, from_file, raw_input_mock, input_action, exists_action=''): packages.create = mock.MagicMock( side_effect=[common_exceptions.HTTPConflict("Conflict"), None]) packages.filter.return_value = [mock.Mock(id='test_id')] raw_input_mock.return_value = input_action args = TestArgs() args.exists_action = exists_action with tempfile.NamedTemporaryFile() as f: args.filename = [f.name] pkg = make_pkg({'FullName': f.name}) from_file.return_value = utils.Package(utils.File(pkg)) v1_shell.do_package_import(self.client, args) return f.name @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_skip(self, from_file, raw_input_mock): name = self._test_conflict( self.client.packages, from_file, raw_input_mock, 's', ) self.client.packages.create.assert_called_once_with({ 'is_public': False, }, {name: mock.ANY},) @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_skip_ea(self, from_file, raw_input_mock): name = self._test_conflict( self.client.packages, from_file, raw_input_mock, '', exists_action='s', ) self.client.packages.create.assert_called_once_with({ 'is_public': False, }, {name: mock.ANY},) self.assertFalse(raw_input_mock.called) @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_abort(self, from_file, raw_input_mock): self.assertRaises(SystemExit, self._test_conflict, self.client.packages, from_file, raw_input_mock, 'a', ) self.client.packages.create.assert_called_once_with({ 'is_public': False, }, mock.ANY,) @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_abort_ea(self, from_file, raw_input_mock): self.assertRaises(SystemExit, self._test_conflict, self.client.packages, from_file, raw_input_mock, '', exists_action='a', ) self.client.packages.create.assert_called_once_with({ 'is_public': False, }, mock.ANY,) self.assertFalse(raw_input_mock.called) @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_update(self, from_file, raw_input_mock): name = self._test_conflict( self.client.packages, from_file, raw_input_mock, 'u', ) self.client.packages.delete.assert_called_once_with('test_id') self.client.packages.create.assert_has_calls( [ mock.call({'is_public': False}, {name: mock.ANY},), mock.call({'is_public': False}, {name: mock.ANY},) ], any_order=True, ) self.assertEqual(2, self.client.packages.create.call_count) @mock.patch('builtins.input') @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_update_ea(self, from_file, raw_input_mock): name = self._test_conflict( self.client.packages, from_file, raw_input_mock, '', exists_action='u', ) self.client.packages.delete.assert_called_once_with('test_id') self.client.packages.create.assert_has_calls( [ mock.call({'is_public': False}, {name: mock.ANY},), mock.call({'is_public': False}, {name: mock.ANY},) ], any_order=True, ) self.assertEqual(2, self.client.packages.create.call_count) self.assertFalse(raw_input_mock.called) def _test_conflict_dep(self, packages, from_file, dep_exists_action=''): packages.create = mock.MagicMock( side_effect=[common_exceptions.HTTPConflict("Conflict"), common_exceptions.HTTPConflict("Conflict"), None]) packages.filter.return_value = [mock.Mock(id='test_id')] args = TestArgs() args.exists_action = 's' args.dep_exists_action = dep_exists_action args.filename = ['first_app'] pkg1 = make_pkg( {'FullName': 'first_app', 'Require': {'second_app': '1.0'}, }) pkg2 = make_pkg({'FullName': 'second_app', }) def side_effect(name): if 'first_app' in name: return utils.Package(utils.File(pkg1)) if 'second_app' in name: return utils.Package(utils.File(pkg2)) from_file.side_effect = side_effect v1_shell.do_package_import(self.client, args) @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_dep_skip_ea(self, from_file): self._test_conflict_dep( self.client.packages, from_file, dep_exists_action='s', ) self.client.packages.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), ], any_order=True, ) self.assertEqual(2, self.client.packages.create.call_count) @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_dep_abort_ea(self, from_file): self.assertRaises(SystemExit, self._test_conflict_dep, self.client.packages, from_file, dep_exists_action='a', ) self.client.packages.create.assert_called_with({ 'is_public': False, }, {'second_app': mock.ANY},) @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_conflict_dep_update_ea(self, from_file): self.assertRaises(SystemExit, self._test_conflict_dep, self.client.packages, from_file, dep_exists_action='u', ) self.assertTrue(self.client.packages.delete.called) self.client.packages.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), ], any_order=True, ) self.assertGreater(self.client.packages.create.call_count, 2) self.assertLess(self.client.packages.create.call_count, 5) @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_no_categories(self, from_file): args = TestArgs() with tempfile.NamedTemporaryFile() as f: RESULT_PACKAGE = f.name pkg = make_pkg({'FullName': RESULT_PACKAGE}) from_file.return_value = utils.Package(utils.File(pkg)) args.filename = [RESULT_PACKAGE] args.categories = None args.is_public = False v1_shell.do_package_import(self.client, args) self.client.packages.create.assert_called_once_with( {'is_public': False}, {RESULT_PACKAGE: mock.ANY}, ) @requests_mock.mock() @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_url(self, rm, from_file): args = TestArgs() args.filename = ["http://127.0.0.1/test_package.zip"] args.categories = None args.is_public = False pkg = make_pkg({'FullName': 'test_package'}) from_file.return_value = utils.Package(utils.File(pkg)) rm.get(args.filename[0], body=make_pkg({'FullName': 'test_package'})) v1_shell.do_package_import(self.client, args) self.client.packages.create.assert_called_once_with( {'is_public': False}, {'test_package': mock.ANY}, ) @requests_mock.mock() @mock.patch('muranoclient.common.utils.Package.from_file') def test_package_import_by_name(self, rm, from_file): args = TestArgs() args.filename = ["io.test.apps.test_application"] args.categories = None args.is_public = False args.murano_repo_url = "http://127.0.0.1" pkg = make_pkg({'FullName': args.filename[0]}) from_file.return_value = utils.Package(utils.File(pkg)) rm.get(args.murano_repo_url + '/apps/' + args.filename[0] + '.zip', body=make_pkg({'FullName': 'first_app'})) v1_shell.do_package_import(self.client, args) self.assertTrue(self.client.packages.create.called) self.client.packages.create.assert_called_once_with( {'is_public': False}, {args.filename[0]: mock.ANY}, ) @requests_mock.mock() def test_package_import_multiple(self, rm): args = TestArgs() args.filename = ["io.test.apps.test_application", "http://127.0.0.1/test_app2.zip", ] args.categories = None args.is_public = False args.murano_repo_url = "http://127.0.0.1" rm.get(args.murano_repo_url + '/apps/' + args.filename[0] + '.zip', body=make_pkg({'FullName': 'first_app'})) rm.get(args.filename[1], body=make_pkg({'FullName': 'second_app'})) v1_shell.do_package_import(self.client, args) self.assertTrue(self.client.packages.create.called) self.client.packages.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), ], any_order=True, ) @requests_mock.mock() def test_import_bundle_by_name(self, m): """Asserts bundle import calls packages create once for each pkg.""" pkg1 = make_pkg({'FullName': 'first_app'}) pkg2 = make_pkg({'FullName': 'second_app'}) m.get(TestArgs.murano_repo_url + '/apps/first_app.zip', body=pkg1) m.get(TestArgs.murano_repo_url + '/apps/second_app.1.0.zip', body=pkg2) s = io.StringIO() bundle_contents = {'Packages': [ {'Name': 'first_app'}, {'Name': 'second_app', 'Version': '1.0'} ]} json.dump(bundle_contents, s) s = io.BytesIO(s.getvalue().encode('ascii')) m.get(TestArgs.murano_repo_url + '/bundles/test_bundle.bundle', body=s) args = TestArgs() args.filename = ["test_bundle"] v1_shell.do_bundle_import(self.client, args) self.client.packages.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), ], any_order=True, ) @requests_mock.mock() def test_import_bundle_dependencies(self, m): """Test bundle import calls Asserts bundle import calls packages create once for each pkg, including dependencies. """ pkg1 = make_pkg( {'FullName': 'first_app', 'Require': {'second_app': '1.0'}, }) pkg2 = make_pkg({'FullName': 'second_app'}) m.get(TestArgs.murano_repo_url + '/apps/first_app.zip', body=pkg1) m.get(TestArgs.murano_repo_url + '/apps/second_app.1.0.zip', body=pkg2) s = io.StringIO() # bundle only contains 1st package bundle_contents = {'Packages': [ {'Name': 'first_app'}, ]} json.dump(bundle_contents, s) s = io.BytesIO(s.getvalue().encode('ascii')) m.get(TestArgs.murano_repo_url + '/bundles/test_bundle.bundle', body=s) args = TestArgs() args.filename = ["test_bundle"] v1_shell.do_bundle_import(self.client, args) self.client.packages.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), ], any_order=True, ) @requests_mock.mock() def test_import_bundle_by_url(self, m): """Asserts bundle import calls packages create once for each pkg.""" pkg1 = make_pkg({'FullName': 'first_app'}) pkg2 = make_pkg({'FullName': 'second_app'}) m.get(TestArgs.murano_repo_url + '/apps/first_app.zip', body=pkg1) m.get(TestArgs.murano_repo_url + '/apps/second_app.1.0.zip', body=pkg2) s = io.StringIO() bundle_contents = {'Packages': [ {'Name': 'first_app'}, {'Name': 'second_app', 'Version': '1.0'} ]} json.dump(bundle_contents, s) s = io.BytesIO(s.getvalue().encode('ascii')) url = 'http://127.0.0.2/test_bundle.bundle' m.get(url, body=s) args = TestArgs() args.filename = [url] v1_shell.do_bundle_import(self.client, args) self.client.packages.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), ], any_order=True, ) @requests_mock.mock() def test_import_bundle_wrong_url(self, m): url = 'http://127.0.0.2/test_bundle.bundle' m.get(url, status_code=404) args = TestArgs() args.filename = [url] v1_shell.do_bundle_import(self.client, args) self.assertFalse(self.client.packages.create.called) @requests_mock.mock() def test_import_bundle_no_bundle(self, m): url = 'http://127.0.0.1/bundles/test_bundle.bundle' m.get(url, status_code=404) args = TestArgs() args.filename = ["test_bundle"] v1_shell.do_bundle_import(self.client, args) self.assertFalse(self.client.packages.create.called) @requests_mock.mock() def test_import_local_bundle(self, m): """Asserts local bundles are first searched locally.""" tmp_dir = tempfile.mkdtemp() bundle_file = os.path.join(tmp_dir, 'bundle.bundle') with open(os.path.join(tmp_dir, 'bundle.bundle'), 'w') as f: bundle_contents = {'Packages': [ {'Name': 'first_app'}, {'Name': 'second_app', 'Version': '1.0'} ]} json.dump(bundle_contents, f) pkg1 = make_pkg({'FullName': 'first_app', 'Require': {'third_app': None}}) pkg2 = make_pkg({'FullName': 'second_app'}) pkg3 = make_pkg({'FullName': 'third_app'}) with open(os.path.join(tmp_dir, 'first_app'), 'wb') as f: f.write(pkg1.read()) with open(os.path.join(tmp_dir, 'third_app'), 'wb') as f: f.write(pkg3.read()) m.get(TestArgs.murano_repo_url + '/apps/first_app.zip', status_code=404) m.get(TestArgs.murano_repo_url + '/apps/second_app.1.0.zip', body=pkg2) m.get(TestArgs.murano_repo_url + '/apps/third_app.zip', status_code=404) args = TestArgs() args.filename = [bundle_file] v1_shell.do_bundle_import(self.client, args) self.client.packages.create.assert_has_calls( [ mock.call({'is_public': False}, {'first_app': mock.ANY}), mock.call({'is_public': False}, {'second_app': mock.ANY}), mock.call({'is_public': False}, {'third_app': mock.ANY}), ], any_order=True, ) shutil.rmtree(tmp_dir) @requests_mock.mock() def test_save_bundle(self, m): tmp_dir = tempfile.mkdtemp() pkg = make_pkg({'FullName': 'test_app'}) expected_pkg = tempfile.NamedTemporaryFile(delete=False) shutil.copyfileobj(pkg, expected_pkg) pkg.seek(0) m.get(TestArgs.murano_repo_url + '/apps/test_app.zip', body=pkg) s = io.StringIO() expected_bundle = {'Packages': [ {'Name': 'test_app'}, ]} json.dump(expected_bundle, s) s = io.BytesIO(s.getvalue().encode('ascii')) m.get(TestArgs.murano_repo_url + '/bundles/test_bundle.bundle', body=s) args = TestArgs() args.filename = "test_bundle" args.path = tmp_dir v1_shell.do_bundle_save(self.client, args) expected_pkg.seek(0) result_bundle = json.load(open(os.path.join( tmp_dir, 'test_bundle.bundle'))) result_pkg = os.path.join(tmp_dir, 'test_app.zip') self.assertEqual(expected_bundle, result_bundle) self.assertTrue(filecmp.cmp(expected_pkg.name, result_pkg)) os.remove(expected_pkg.name) shutil.rmtree(tmp_dir) @requests_mock.mock() def test_package_save(self, m): args = TestArgs() tmp_dir = tempfile.mkdtemp() args.package = ["test_app1", "http://127.0.0.1/test_app2.zip"] args.path = tmp_dir pkgs = [ make_pkg( {'FullName': 'test_app1', 'Require': {'test_app3': '1.0'}}), make_pkg({'FullName': 'test_app2'}), make_pkg({'FullName': 'test_app3'}) ] m.get(TestArgs.murano_repo_url + '/apps/' + args.package[0] + '.zip', body=pkgs[0]) m.get(args.package[1], body=pkgs[1]) m.get(TestArgs.murano_repo_url + '/apps/' + 'test_app3.1.0.zip', body=pkgs[2]) expected_pkgs = [] for i in range(0, 3): expected_pkgs.append(tempfile.NamedTemporaryFile(delete=False)) shutil.copyfileobj(pkgs[i], expected_pkgs[i]) pkgs[i].seek(0) v1_shell.do_package_save(self.client, args) file_names = ['test_app1.zip', 'test_app2.zip', 'test_app3.1.0.zip'] for i in range(0, 3): expected_pkgs[i].seek(0) result_pkg = os.path.join(tmp_dir, file_names[i]) self.assertTrue(filecmp.cmp(expected_pkgs[i].name, result_pkg)) os.remove(expected_pkgs[i].name) shutil.rmtree(tmp_dir) class ShellPackagesOperationsV3(ShellPackagesOperations): def make_env(self, exclude=None, fake_env=FAKE_ENV): if 'OS_AUTH_URL' in fake_env: fake_env.update({'OS_AUTH_URL': 'http://no.where/v3'}) env = dict((k, v) for k, v in fake_env.items() if k != exclude) self.useFixture(fixtures.MonkeyPatch('os.environ', env)) def register_keystone_discovery_fixture(self, mreq): v3_url = "http://no.where/v3" v3_version = fixture.V3Discovery(v3_url) mreq.register_uri('GET', v3_url, json=_create_ver_list([v3_version]), status_code=200) def register_keystone_token_fixture(self, mreq): v3_token = ks_v3_fixture.Token() service = v3_token.add_service('application-catalog') service.add_standard_endpoints(public='http://no.where') mreq.register_uri('POST', 'http://no.where/v3/auth/tokens', json=v3_token, headers={'X-Subject-Token': 'tokenid'}, status_code=200) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/tests/unit/test_utils.py0000664000175000017500000004436200000000000025166 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import io import json import os.path import tempfile from unittest import mock import zipfile import requests import requests_mock import testtools import yaml from muranoclient.common import utils class FileTest(testtools.TestCase): def test_file_object_from_file(self): f_obj = tempfile.NamedTemporaryFile(delete=True) new_f_obj = utils.File(f_obj).open() self.assertTrue(hasattr(new_f_obj, 'read')) new_f_obj = utils.File(f_obj.name).open() self.assertTrue(hasattr(new_f_obj, 'read')) def test_file_object_file_fails(self): f_obj = utils.File('') self.assertRaises(ValueError, f_obj.open) def test_file_object_url_fails(self): resp = requests.Response() resp.status_code = 400 resp.raw = io.BytesIO(b"123") with mock.patch( 'requests.get', mock.Mock(side_effect=lambda k, *args, **kwargs: resp)): f = utils.File("http://127.0.0.1") self.assertRaises(ValueError, f.open) def test_file_object_url(self): resp = requests.Response() resp.raw = io.BytesIO(b"123") resp.status_code = 200 with mock.patch( 'requests.get', mock.Mock(side_effect=lambda k, *args, **kwargs: resp)): new_f_obj = utils.File('http://127.0.0.1/').open() self.assertTrue(hasattr(new_f_obj, 'read')) def make_pkg(manifest_override, image_dicts=None): manifest = { 'Author': '', 'Classes': {'foo': 'foo.yaml'}, 'Description': '', 'Format': 1.0, 'FullName': 'org.foo', 'Name': 'Apache HTTP Server', 'Type': 'Application'} manifest.update(manifest_override) file_obj = io.BytesIO() zfile = zipfile.ZipFile(file_obj, "a") zfile.writestr('manifest.yaml', yaml.dump(manifest)) zfile.writestr('Classes/foo.yaml', yaml.dump({})) if image_dicts: images_list = [] default_image_spec = { 'ContainerFormat': 'bare', 'DiskFormat': 'qcow2', 'Name': '', } for image_dict in image_dicts: image_spec = default_image_spec.copy() image_spec.update(image_dict) images_list.append(image_spec) images = {'Images': images_list, } zfile.writestr('images.lst', yaml.dump(images)) zfile.close() file_obj.seek(0) return file_obj class PackageTest(testtools.TestCase): base_url = "http://127.0.0.1" @requests_mock.mock() def test_from_location_local_file(self, m): temp = tempfile.NamedTemporaryFile() pkg = make_pkg({'FullName': 'single_app'}) temp.write(pkg.read()) temp.flush() path, name = os.path.split(temp.name) # ensure we do not go to base url m.get(self.base_url + '/apps/{0}.zip'.format(name), status_code=404) self.assertEqual('single_app', utils.Package.from_location( name=name, base_url=self.base_url, path=path, ).manifest['FullName']) def test_package_from_directory(self): path = os.path.join(os.path.dirname(os.path.realpath(__file__)), "fixture_data/empty-app") pkg = utils.Package(utils.File(path)) self.assertEqual('empty', pkg.manifest['FullName']) pkg = utils.Package.from_location('', path=path) self.assertEqual('empty', pkg.manifest['FullName']) @requests_mock.mock() def test_from_location_url(self, m): """Test that url overrides name specification.""" pkg = make_pkg({'FullName': 'single_app'}) m.get('http://127.0.0.2/apps/single_app.zip', body=pkg) m.get(self.base_url + '/apps/single_app.zip', status_code=404) self.assertEqual('single_app', utils.Package.from_location( name='single_app', base_url=self.base_url, url="http://127.0.0.2/apps/single_app.zip", ).manifest['FullName']) @requests_mock.mock() def test_from_location(self, m): """Test from location url requesting mechanism.""" pkg = make_pkg({'FullName': 'single_app'}) pkg_ver = make_pkg({'FullName': 'single_app'}) m.get(self.base_url + '/apps/single_app.zip', body=pkg) m.get(self.base_url + '/apps/single_app.1.0.zip', body=pkg_ver) m.get(self.base_url + '/apps/single_app.2.0.zip', status_code=404) self.assertEqual('single_app', utils.Package.from_location( name='single_app', base_url=self.base_url).manifest['FullName']) self.assertEqual('single_app', utils.Package.from_location( name='single_app', version='1.0', base_url=self.base_url).manifest['FullName']) self.assertRaises( ValueError, utils.Package.from_location, name='single_app', version='2.0', base_url=self.base_url) def test_no_requirements(self): pkg = make_pkg({'FullName': 'single_app'}) app = utils.Package.fromFile(pkg) reqs = app.requirements(base_url=self.base_url) self.assertEqual({'single_app': app}, reqs) @requests_mock.mock() def test_requirements(self, m): """Test that dependencies are parsed correctly.""" pkg3 = make_pkg({'FullName': 'dep_of_dep'}) pkg2 = make_pkg({'FullName': 'dep_app', 'Require': { 'dep_of_dep': "1.0"}, }) pkg1 = make_pkg({'FullName': 'main_app', 'Require': { 'dep_app': None}, }) m.get(self.base_url + '/apps/main_app.zip', body=pkg1) m.get(self.base_url + '/apps/dep_app.zip', body=pkg2) m.get(self.base_url + '/apps/dep_of_dep.1.0.zip', body=pkg3) app = utils.Package.fromFile(pkg1) reqs = app.requirements(base_url=self.base_url) self.assertEqual( {'main_app': app, 'dep_app': mock.ANY, 'dep_of_dep': mock.ANY}, reqs) @mock.patch('muranoclient.common.utils.Package.from_file') def test_requirements_order(self, from_file): """Test that dependencies are parsed in correct order.""" pkg5 = make_pkg({'FullName': 'd4', }) pkg4 = make_pkg({'FullName': 'd3', 'Require': {'d4': None}, }) pkg3 = make_pkg({'FullName': 'd2', 'Require': {'d3': None}, }) pkg2 = make_pkg({'FullName': 'd1', 'Require': {'d3': None}, }) pkg1 = make_pkg({'FullName': 'M', 'Require': {'d1': None, 'd2': None, 'd4': None}, }) def side_effect(name): if 'M' in name: return utils.Package(utils.File(pkg1)) if 'd1' in name: return utils.Package(utils.File(pkg2)) if 'd2' in name: return utils.Package(utils.File(pkg3)) if 'd3' in name: return utils.Package(utils.File(pkg4)) if 'd4' in name: return utils.Package(utils.File(pkg5)) from_file.side_effect = side_effect app = from_file('M') reqs = app.requirements(base_url=self.base_url) def key_position(key): keys = list(iter(reqs.keys())) return keys.index(key) self.assertTrue( key_position('d4') < key_position('d3') and key_position('d4') < key_position('M') and key_position('d3') < key_position('d1') and key_position('d3') < key_position('d2') < key_position('M') ) @mock.patch('muranoclient.common.utils.Package.from_file') def test_requirements_order2(self, from_file): """Test that dependencies are parsed in correct order.""" pkg5 = make_pkg({'FullName': 'd4', 'Require': {'d6': None}, }) pkg4 = make_pkg({'FullName': 'd3', 'Require': {'d4': None}, }) pkg3 = make_pkg({'FullName': 'd1', 'Require': {'d3': None, 'd7': None}, }) pkg2 = make_pkg({'FullName': 'd2', 'Require': {'d3': None}, }) pkg6 = make_pkg({'FullName': 'd6', }) pkg7 = make_pkg({'FullName': 'd7', 'Require': {'d8': None}, }) pkg8 = make_pkg({'FullName': 'd8', }) pkg1 = make_pkg({'FullName': 'M', 'Require': {'d1': None, 'd2': None, 'd4': None, }, }) def side_effect(name): if 'M' in name: return utils.Package(utils.File(pkg1)) if 'd1' in name: return utils.Package(utils.File(pkg2)) if 'd2' in name: return utils.Package(utils.File(pkg3)) if 'd3' in name: return utils.Package(utils.File(pkg4)) if 'd4' in name: return utils.Package(utils.File(pkg5)) if 'd6' in name: return utils.Package(utils.File(pkg6)) if 'd7' in name: return utils.Package(utils.File(pkg7)) if 'd8' in name: return utils.Package(utils.File(pkg8)) from_file.side_effect = side_effect app = from_file('M') reqs = app.requirements(base_url=self.base_url) def key_position(key): keys = list(iter(reqs.keys())) return keys.index(key) self.assertTrue( key_position('d6') < key_position('d4') < key_position('d3') < key_position('d1') and key_position('d3') < key_position('d2') and key_position('d1') < key_position('M') and key_position('d2') < key_position('M') and key_position('d8') < key_position('d7') < key_position('d1') ) @mock.patch('muranoclient.common.utils.Package.from_file') def test_cyclic_requirements(self, from_file): """Test that a cyclic dependency would be handled correctly.""" pkg3 = make_pkg({'FullName': 'dep_of_dep', 'Require': { 'main_app': None, 'dep_app': None}, }) pkg2 = make_pkg({'FullName': 'dep_app', 'Require': { 'dep_of_dep': None, 'main_app': None}, }) pkg1 = make_pkg({'FullName': 'main_app', 'Require': { 'dep_app': None, 'dep_of_dep': None}, }) def side_effect(name): if 'main_app' in name: return utils.Package(utils.File(pkg1)) if 'dep_app' in name: return utils.Package(utils.File(pkg2)) if 'dep_of_dep' in name: return utils.Package(utils.File(pkg3)) from_file.side_effect = side_effect app = from_file('main_app') reqs = app.requirements(base_url=self.base_url) self.assertEqual( {'main_app': app, 'dep_app': mock.ANY, 'dep_of_dep': mock.ANY}, reqs) @mock.patch('muranoclient.common.utils.Package.from_file') def test_order_with_cyclic_requirements2(self, from_file): """Test that dependencies are parsed in correct order.""" pkg6 = make_pkg({'FullName': 'd5', 'Require': {'d6': None}, }) pkg7 = make_pkg({'FullName': 'd6', }) pkg5 = make_pkg({'FullName': 'd4', 'Require': {'d3': None, 'd5': None}}) pkg4 = make_pkg({'FullName': 'd3', 'Require': {'d4': None}, }) pkg3 = make_pkg({'FullName': 'd2', 'Require': {'d1': None, 'd5': None, 'd6': None}, }) pkg2 = make_pkg({'FullName': 'd1', 'Require': {'d2': None}, }) pkg1 = make_pkg({'FullName': 'M', 'Require': {'d1': None, 'd3': None}, }) def side_effect(name): if 'M' in name: return utils.Package(utils.File(pkg1)) if 'd1' in name: return utils.Package(utils.File(pkg2)) if 'd2' in name: return utils.Package(utils.File(pkg3)) if 'd3' in name: return utils.Package(utils.File(pkg4)) if 'd4' in name: return utils.Package(utils.File(pkg5)) if 'd5' in name: return utils.Package(utils.File(pkg6)) if 'd6' in name: return utils.Package(utils.File(pkg7)) from_file.side_effect = side_effect app = from_file('M') reqs = app.requirements(base_url=self.base_url) def key_position(key): keys = list(iter(reqs.keys())) return keys.index(key) self.assertTrue( key_position('d5') < key_position('d4') and key_position('d5') < key_position('d2') and key_position('d5') < key_position('d3') < key_position('M') and key_position('d5') < key_position('d1') < key_position('M') ) @mock.patch('muranoclient.common.utils.Package.from_file') def test_order_with_cyclic_requirements3(self, from_file): """Test that dependencies are parsed in correct order.""" pkg5 = make_pkg({'FullName': 'd4', }) pkg4 = make_pkg({'FullName': 'd3', 'Require': {'M': None}, }) pkg3 = make_pkg({'FullName': 'd2', 'Require': {'d3': None, 'd4': None}, }) pkg2 = make_pkg({'FullName': 'd1', 'Require': {'d2': None}, }) pkg1 = make_pkg({'FullName': 'M', 'Require': {'d1': None}, }) def side_effect(name): if 'M' in name: return utils.Package(utils.File(pkg1)) if 'd1' in name: return utils.Package(utils.File(pkg2)) if 'd2' in name: return utils.Package(utils.File(pkg3)) if 'd3' in name: return utils.Package(utils.File(pkg4)) if 'd4' in name: return utils.Package(utils.File(pkg5)) from_file.side_effect = side_effect app = from_file('M') reqs = app.requirements(base_url=self.base_url) def key_position(key): keys = list(iter(reqs.keys())) return keys.index(key) self.assertTrue( key_position('d4') < key_position('M') and key_position('d4') < key_position('d1') and key_position('d4') < key_position('d2') and key_position('d4') < key_position('d3') ) def test_images(self): pkg = make_pkg({}) app = utils.Package.fromFile(pkg) self.assertEqual([], app.images()) pkg = make_pkg( {}, [{'Name': 'test.qcow2'}, {'Name': 'test2.qcow2'}]) app = utils.Package.fromFile(pkg) self.assertEqual( set(['test.qcow2', 'test2.qcow2']), set([img['Name'] for img in app.images()])) def test_file_object_repo_fails(self): resp = requests.Response() resp.raw = io.BytesIO(b"123") resp.status_code = 400 with mock.patch( 'requests.get', mock.Mock(side_effect=lambda k, *args, **kwargs: resp)): self.assertRaises( ValueError, utils.Package.from_location, name='foo.bar.baz', base_url='http://127.0.0.1') def test_no_repo_url_fails(self): self.assertRaises(ValueError, utils.Package.from_location, name='foo.bar.baz', base_url='') @mock.patch.object(utils.Package, 'validate') def test_file_object_repo(self, m_validate): resp = requests.Response() resp.raw = io.BytesIO(b"123") resp.status_code = 200 m_validate.return_value = None with mock.patch( 'requests.get', mock.Mock(side_effect=lambda k, *args, **kwargs: resp)): new_f_obj = utils.Package.from_location( name='foo.bar.baz', base_url='http://127.0.0.1').file() self.assertTrue(hasattr(new_f_obj, 'read')) class BundleTest(testtools.TestCase): base_url = "http://127.0.0.1" @requests_mock.mock() def test_packages(self, m): s = io.StringIO() bundle_contents = {'Packages': [ {'Name': 'first_app'}, {'Name': 'second_app', 'Version': '1.0'} ]} json.dump(bundle_contents, s) s.seek(0) bundle = utils.Bundle.from_file(s) self.assertEqual( set(['first_app', 'second_app']), set([p['Name'] for p in bundle.package_specs()]) ) # setup packages pkg1 = make_pkg({'FullName': 'first_app'}) pkg2 = make_pkg({'FullName': 'second_app'}) m.get(self.base_url + '/apps/first_app.zip', body=pkg1) m.get(self.base_url + '/apps/second_app.1.0.zip', body=pkg2) self.assertEqual( set(['first_app', 'second_app']), set([p.manifest['FullName'] for p in bundle.packages(base_url=self.base_url)]) ) class TraverseTest(testtools.TestCase): def test_traverse_and_replace(self): obj = [ {'id': '===id1==='}, {'id': '===id2===', 'x': [{'bar': '===id1==='}]}, ['===id1===', '===id2==='], '===id3===', '===nonid0===', '===id3===', ] utils.traverse_and_replace(obj) self.assertNotEqual('===id1===', obj[0]['id']) self.assertNotEqual('===id2===', obj[1]['id']) self.assertNotEqual('===id1===', obj[1]['x'][0]['bar']) self.assertNotEqual('===id1===', obj[2][0]) self.assertNotEqual('===id2===', obj[2][1]) self.assertNotEqual('===id3===', obj[3]) self.assertEqual('===nonid0===', obj[4]) self.assertNotEqual('===id3===', obj[5]) self.assertEqual(obj[0]['id'], obj[1]['x'][0]['bar']) self.assertEqual(obj[0]['id'], obj[2][0]) self.assertEqual(obj[1]['id'], obj[2][1]) self.assertEqual(obj[3], obj[5]) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1709301724.6060848 python-muranoclient-2.8.0/muranoclient/v1/0000775000175000017500000000000000000000000020611 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/__init__.py0000664000175000017500000000000000000000000022710 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/actions.py0000664000175000017500000000263700000000000022633 0ustar00zuulzuul00000000000000# Copyright (c) 2014 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. # Not a true manager yet; should be changed to be one if CRUD # functionality becomes available for actions. class ActionManager(object): def __init__(self, api): self.api = api def call(self, environment_id, action_id, arguments=None): if arguments is None: arguments = {} url = '/v1/environments/{environment_id}/actions/{action_id}'.format( environment_id=environment_id, action_id=action_id) resp, body = self.api.json_request(url, 'POST', body=arguments) return body['task_id'] def get_result(self, environment_id, task_id): url = '/v1/environments/{environment_id}/actions/{task_id}'.format( environment_id=environment_id, task_id=task_id) resp, body = self.api.json_request(url, 'GET') return body or None ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/artifact_packages.py0000664000175000017500000003100200000000000024612 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections from glanceclient import exc as glance_exc import yaml from muranoclient.common import exceptions as exc from muranoclient.common import utils from muranoclient.i18n import _ def rewrap_http_exceptions(func): def inner(*args, **kwargs): try: return func(*args, **kwargs) except glance_exc.HTTPException as e: raise exc.from_code(e.code) return inner class ArtifactRepo(object): def __init__(self, client, tenant=None): self.tenant = tenant self.client = client def create(self, fqn, data, **kwargs): package = utils.Package.from_file(data) manifest = package.manifest package_draft = { 'name': manifest.get('FullName', fqn), 'version': manifest.get('Version', '0.0.0'), 'description': manifest.get('Description'), 'display_name': manifest.get('Name', fqn), 'type': manifest.get('Type', 'Application'), 'author': manifest.get('Author'), 'tags': manifest.get('Tags', []), 'class_definitions': package.classes.keys() } for k, v in kwargs.items(): package_draft[k] = v inherits = self._get_local_inheritance(package.classes, package.resolvers) # check for global inheritance ancestor_queue = collections.deque(inherits.keys()) while ancestor_queue: ancestor_name = ancestor_queue.popleft() child_classes = inherits[ancestor_name] ancestors = self.list(class_definitions=ancestor_name) for ancestor in ancestors: # check if ancestor inherits anything ancestor_inherits = \ ancestor.type_specific_properties.get('inherits', {}) for name, value in ancestor_inherits.items(): # check if this is the class we actually inherit if ancestor_name in value: ancestor_queue.append(name) inherits[name] = child_classes package_draft['inherits'] = inherits keywords = self._keywords_from_display_name( package_draft['display_name']) keywords.extend(package_draft['tags']) package_draft['keywords'] = keywords # NOTE(ativelkov): this is very racy, but until we have a chance to # enforce uniqueness right in glance this is the only way to do it visibility = package_draft.get('visibility', 'private') if visibility == 'public': filters = {} else: filters = {'owner': self.tenant} existing = self.list(name=package_draft['name'], version=package_draft['version'], **filters) try: next(existing) raise exc.HTTPConflict("Package already exists") except StopIteration: pass res = self.client.artifacts.create(**package_draft) app_id = res.id self.client.artifacts.upload_blob(app_id, 'archive', package.file()) if package.logo is not None: self.client.artifacts.upload_blob(app_id, 'logo', package.logo) if package.ui is not None: self.client.artifacts.upload_blob(app_id, 'ui_definition', package.ui) package.file().close() self.client.artifacts.active(app_id) return self.client.artifacts.get(app_id) @staticmethod def _get_local_inheritance(classes, resolvers): result = {} for class_name, klass in classes.items(): if 'Extends' not in klass: continue ns = klass.get('Namespaces') if ns: resolver = utils.NamespaceResolver(ns) else: resolver = resolvers.get(class_name) if isinstance(klass['Extends'], list): bases = klass['Extends'] else: bases = [klass['Extends']] for base_class in bases: if resolver: base_fqn = resolver.resolve_name(base_class) else: base_fqn = base_class result.setdefault(base_fqn, []).append(class_name) return result @staticmethod def _keywords_from_display_name(display_name): return display_name.split()[:10] def list(self, sort_field='name', sort_dir='asc', type=None, tags=None, limit=None, page_size=None, **filters): sort = "%s:%s" % (sort_field, sort_dir) if type is not None: filters['type'] = type if tags is not None: filters['tag'] = tags return self.client.artifacts.list(sort=sort, limit=limit, page_size=page_size, filters=filters) def get(self, app_id): return self.client.artifacts.get(app_id) def delete(self, app_id): return self.client.artifacts.delete(app_id) def update(self, app_id, props_to_remove=None, **new_props): new_keywords = [] new_name = new_props.get('display_name') new_tags = new_props.get('tags') if new_name: new_keywords.extend(self._keywords_from_display_name(new_name)) if new_tags: new_keywords.extend(new_tags) if new_keywords: new_props['keywords'] = new_keywords visibility = new_props.get('visibility') if visibility == 'public': package = self.client.artifacts.get(app_id) # NOTE(ativelkov): this is very racy, but until we have a chance to # enforce uniqueness right in glance this is the only way to do it existing = self.list(name=package.name, version=package.version, visibility='public') try: while True: package = next(existing) if package.id == app_id: continue else: raise exc.HTTPConflict("Package already exists") except StopIteration: pass return self.client.artifacts.update(app_id, remove_props=props_to_remove, **new_props) def toggle_active(self, app_id): old_val = self.get(app_id).type_specific_properties['enabled'] return self.update(app_id, enabled=(not old_val)) def toggle_public(self, app_id): visibility = self.get(app_id).visibility if visibility == 'public': return self.update(app_id, visibility='private') else: return self.update(app_id, visibility='public') def download(self, app_id): return self.client.artifacts.download_blob(app_id, 'archive') def get_ui(self, app_id, loader_cls=None): ui_stream = "".join( self.client.artifacts.download_blob(app_id, 'ui_definition')) if loader_cls is None: loader_cls = yaml.SafeLoader return yaml.load(ui_stream, loader_cls) def get_logo(self, app_id): return self.client.artifacts.download_blob(app_id, 'logo') class PackageManagerAdapter(object): def __init__(self, legacy, glare): self.legacy = legacy self.glare = glare def categories(self): return self.legacy.categories() @rewrap_http_exceptions def create(self, data, files): is_public = data.pop('is_public', None) if is_public is not None: data['visibility'] = 'public' if is_public else 'private' fqn = list(files.keys())[0] pkg = self.glare.create(fqn, files[fqn], **data) return PackageWrapper(pkg) @rewrap_http_exceptions def filter(self, **kwargs): kwargs.pop('catalog', None) # NOTE(ativelkov): Glare ignores 'catalog' include_disabled = kwargs.pop('include_disabled', False) order_by = kwargs.pop('order_by', None) search = kwargs.pop('search', None) category = kwargs.pop('category', None) fqn = kwargs.pop('fqn', None) class_name = kwargs.pop('class_name', None) name = kwargs.pop('name', None) if category: kwargs['categories'] = category if search: kwargs['keywords'] = search if order_by: kwargs['sort_field'] = order_by if not include_disabled: kwargs['enabled'] = True if fqn: kwargs['name'] = fqn if class_name: kwargs['class_definitions'] = class_name if name: kwargs['display_name'] = name # if 'owned' is used there should be a filter with 'owner' parameter if kwargs.pop('owned', None): kwargs['owner'] = self.glare.tenant for pkg in self.glare.list(**kwargs): yield PackageWrapper(pkg) @rewrap_http_exceptions def list(self, include_disabled=False): return self.filter(include_disabled=include_disabled) @rewrap_http_exceptions def delete(self, app_id): return self.glare.delete(app_id) @rewrap_http_exceptions def get(self, app_id): return PackageWrapper(self.glare.get(app_id)) @rewrap_http_exceptions def update(self, app_id, body, operation='replace'): is_public = body.pop('is_public', None) name = body.pop('name', None) if is_public is not None: body['visibility'] = 'public' if is_public else 'private' if name is not None: body['display_name'] = name if operation == 'replace': return PackageWrapper(self.glare.update(app_id, None, **body)) @rewrap_http_exceptions def toggle_active(self, app_id): return self.glare.toggle_active(app_id) @rewrap_http_exceptions def toggle_public(self, app_id): return self.glare.toggle_public(app_id) @rewrap_http_exceptions def download(self, app_id): return "".join(self.glare.download(app_id)) @rewrap_http_exceptions def get_logo(self, app_id): return "".join(self.glare.get_logo(app_id)) @rewrap_http_exceptions def get_ui(self, app_id, loader_cls=None): return self.legacy.get_ui(app_id, loader_cls) class PackageWrapper(object): def __init__(self, item): self._item = item @property def updated(self): return self._item.updated_at @property def created(self): return self._item.created_at @property def is_public(self): return self._item.visibility == 'public' @property def name(self): return self._item.type_specific_properties['display_name'] @property def fully_qualified_name(self): return self._item.name @property def owner_id(self): return self._item.owner def __getstate__(self): return {"item": self._item} def __setstate__(self, state): self._item = state['item'] def __getattr__(self, name): if name in self._item.type_specific_properties: return self._item.type_specific_properties.get(name) else: return getattr(self._item, name) def to_dict(self): keys = ('author', 'categories', 'class_definitions', 'created', 'description', 'enabled', 'fully_qualified_name', 'id', 'is_public', 'name', 'owner_id', 'tags', 'type', 'updated') missing_keys = [key for key in keys if not hasattr(self, key)] if missing_keys: raise KeyError(_("Some attributes are missing in " "%(pkg_name)s: %(attrs)s.") % {'pkg_name': self.name, 'attrs': ", ".join(missing_keys)}) return {key: getattr(self, key) for key in keys} ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/categories.py0000664000175000017500000000361200000000000023312 0ustar00zuulzuul00000000000000# Copyright (c) 2015 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import urllib from muranoclient.common import base class Category(base.Resource): def __repr__(self): return "" % self._info def data(self, **kwargs): return self.manager.data(self, **kwargs) class CategoryManager(base.Manager): resource_class = Category def list(self, **kwargs): """Get category list with pagination support. :param sort_keys: an array of fields used to sort the list (string) :param sort_dir: 'asc' or 'desc' for ascending or descending sort :param limit: maximum number of categories to return :param marker: begin returning categories that appear later in the category list than that represented by this marker id """ params = {} for key, value in kwargs.items(): if value: params[key] = value url = '/v1/catalog/categories?{0}'.format( urllib.parse.urlencode(params, True)) return self._list(url, response_key='categories') def get(self, id): return self._get('/v1/catalog/categories/{0}'.format(id)) def add(self, data): return self._create('/v1/catalog/categories', data) def delete(self, id): return self._delete('/v1/catalog/categories/{0}'.format(id)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/client.py0000664000175000017500000000605300000000000022445 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from muranoclient.common import http from muranoclient.v1 import actions from muranoclient.v1 import artifact_packages from muranoclient.v1 import categories from muranoclient.v1 import deployments from muranoclient.v1 import environments from muranoclient.v1 import instance_statistics from muranoclient.v1 import packages from muranoclient.v1 import request_statistics from muranoclient.v1 import schemas from muranoclient.v1 import services from muranoclient.v1 import sessions from muranoclient.v1 import static_actions from muranoclient.v1 import templates class Client(object): """Client for the Murano v1 API. :param string endpoint: A user-supplied endpoint URL for the service. :param string token: Token for authentication. :param integer timeout: Allows customization of the timeout for client http requests. (optional) """ def __init__(self, *args, **kwargs): """Initialize a new client for the Murano v1 API.""" self.glance_client = kwargs.pop('glance_client', None) tenant = kwargs.pop('tenant', None) artifacts_client = kwargs.pop('artifacts_client', None) self.http_client = http._construct_http_client(*args, **kwargs) self.environments = environments.EnvironmentManager(self.http_client) self.env_templates = templates.EnvTemplateManager(self.http_client) self.sessions = sessions.SessionManager(self.http_client) self.services = services.ServiceManager(self.http_client) self.deployments = deployments.DeploymentManager(self.http_client) self.schemas = schemas.SchemaManager(self.http_client) self.request_statistics = \ request_statistics.RequestStatisticsManager(self.http_client) self.instance_statistics = \ instance_statistics.InstanceStatisticsManager(self.http_client) pkg_mgr = packages.PackageManager(self.http_client) if artifacts_client: artifact_repo = artifact_packages.ArtifactRepo(artifacts_client, tenant) self.packages = artifact_packages.PackageManagerAdapter( pkg_mgr, artifact_repo) else: self.packages = pkg_mgr self.actions = actions.ActionManager(self.http_client) self.static_actions = static_actions.StaticActionManager( self.http_client) self.categories = categories.CategoryManager(self.http_client) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/deployments.py0000664000175000017500000000355400000000000023535 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from muranoclient.common import base class Deployment(base.Resource): def __repr__(self): return '' % self._info def data(self, **kwargs): return self.manager.data(self, **kwargs) class Status(base.Resource): def __repr__(self): return '' % self._info def data(self, **kwargs): return self.manager.data(self, **kwargs) class DeploymentManager(base.Manager): resource_class = Deployment def list(self, environment_id, all_environments=False): if all_environments: return self._list('/v1/deployments', 'deployments') else: return self._list('/v1/environments/{id}/deployments'. format(id=environment_id), 'deployments') def reports(self, environment_id, deployment_id, *service_ids): path = '/v1/environments/{id}/deployments/{deployment_id}' path = path.format(id=environment_id, deployment_id=deployment_id) if service_ids: for service_id in service_ids: path += '?service_id={0}'.format(service_id) resp, body = self.api.json_request(path, 'GET') data = body.get('reports', []) return [Status(self, res, loaded=True) for res in data if res] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/environments.py0000664000175000017500000000656400000000000023725 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import urllib from muranoclient.common import base class Environment(base.Resource): def __repr__(self): return "" % self._info def data(self, **kwargs): return self.manager.data(self, **kwargs) class Status(base.Resource): def __repr__(self): return '' % self._info def data(self, **kwargs): return self.manager.data(self, **kwargs) class EnvironmentManager(base.ManagerWithFind): resource_class = Environment def list(self, all_tenants=False, tenant_id=None): params = {'all_tenants': all_tenants} if tenant_id: params['tenant'] = tenant_id path = '/v1/environments?{query}'.format( query=urllib.parse.urlencode(params)) return self._list(path, 'environments') def create(self, data): return self._create('/v1/environments', data) def update(self, environment_id, name): return self._update('/v1/environments/{id}'.format(id=environment_id), data={'name': name}) def delete(self, environment_id, abandon=False): path = '/v1/environments/{id}?{query}'.format( id=environment_id, query=urllib.parse.urlencode({'abandon': abandon})) return self._delete(path) def get(self, environment_id, session_id=None): if session_id: headers = {'X-Configuration-Session': session_id} else: headers = {} return self._get("/v1/environments/{id}".format(id=environment_id), headers=headers) def last_status(self, environment_id, session_id): headers = {'X-Configuration-Session': session_id} path = '/v1/environments/{id}/lastStatus' path = path.format(id=environment_id) status_dict = self._get(path, return_raw=True, response_key='lastStatuses', headers=headers) result = {} for k, v in status_dict.items(): if v: result[k] = Status(self, v, loaded=True) return result def get_model(self, environment_id, path, session_id=None): headers = {'X-Configuration-Session': session_id} url = '/v1/environments/{id}/model/{path}' url = url.format(id=environment_id, path=path) return self._get(url, return_raw=True, headers=headers) def update_model(self, environment_id, data, session_id): headers = {'X-Configuration-Session': session_id} url = '/v1/environments/{id}/model/' url = url.format(id=environment_id) return self._update(url, data, return_raw=True, headers=headers, method='PATCH', content_type='application/env-model-json-patch') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/instance_statistics.py0000664000175000017500000000304600000000000025244 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from muranoclient.common import base class InstanceStatistics(base.Resource): def __repr__(self): return "" % self._info def data(self, **kwargs): return self.manager.data(self, **kwargs) class InstanceStatisticsManager(base.Manager): resource_class = InstanceStatistics def get(self, environment_id, instance_id=None): if instance_id: path = '/v1/environments/{id}/instance-statistics/raw/' \ '{instance_id}'.format(id=environment_id, instance_id=instance_id) else: path = '/v1/environments/{id}/instance-statistics/raw'.format( id=environment_id) return self._list(path, None) def get_aggregated(self, environment_id): path = '/v1/environments/{id}/instance-statistics/aggregated'.format( id=environment_id) return self._list(path, None) ././@PaxHeader0000000000000000000000000000003300000000000011451 xustar000000000000000027 mtime=1709301724.610086 python-muranoclient-2.8.0/muranoclient/v1/package_creator/0000775000175000017500000000000000000000000023723 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/package_creator/__init__.py0000664000175000017500000000000000000000000026022 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/package_creator/hot_package.py0000664000175000017500000000676600000000000026561 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import shutil import tempfile import yaml import muranoclient from muranoclient.apiclient import exceptions def generate_manifest(args): """Generates application manifest file. If some parameters are missed - they we be generated automatically. :param args: :returns: dictionary, contains manifest file data """ if not os.path.isfile(args.template): raise exceptions.CommandError( "Template '{0}' doesn`t exist".format(args.template)) filename = os.path.basename(args.template) if not args.name: args.name = os.path.splitext(filename)[0] if not args.full_name: prefix = 'io.murano.apps.generated' normalized_name = args.name.replace('_', ' ').replace('-', ' ') normalized_name = normalized_name.title().replace(' ', '') args.full_name = '{0}.{1}'.format(prefix, normalized_name) try: with open(args.template, 'rb') as heat_file: yaml_content = yaml.safe_load(heat_file) if not args.description: args.description = yaml_content.get( 'description', 'Heat-defined application for a template "{0}"'.format( filename)) except yaml.YAMLError: raise exceptions.CommandError( "Heat template, represented by --'template' parameter" " should be a valid yaml file") if not args.author: args.author = args.os_username if not args.tags: args.tags = ['Heat-generated'] manifest = { 'Format': 'Heat.HOT/1.0', 'Type': 'Application', 'FullName': args.full_name, 'Name': args.name, 'Description': args.description, 'Author': args.author, 'Tags': args.tags } return manifest def prepare_package(args): """Compose required files for murano application package. :param args: list of command line arguments :returns: absolute path to directory with prepared files """ manifest = generate_manifest(args) temp_dir = tempfile.mkdtemp() manifest_file = os.path.join(temp_dir, 'manifest.yaml') template_file = os.path.join(temp_dir, 'template.yaml') if args.resources_dir: if not os.path.isdir(args.resources_dir): raise exceptions.CommandError( "'--resources-dir' parameter should be a directory") resource_directory = os.path.join(temp_dir, 'Resources') shutil.copytree(args.resources_dir, resource_directory) logo_file = os.path.join(temp_dir, 'logo.png') if not args.logo: shutil.copyfile(muranoclient.get_resource('heat_logo.png'), logo_file) else: if os.path.isfile(args.logo): shutil.copyfile(args.logo, logo_file) with open(manifest_file, 'w') as f: f.write(yaml.dump(manifest, default_flow_style=False)) shutil.copyfile(args.template, template_file) return temp_dir ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/package_creator/mpl_package.py0000664000175000017500000001767700000000000026562 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import os import shutil import tempfile import yaml import muranoclient from muranoclient.apiclient import exceptions from muranoclient.common import utils def prepare_package(args): """Prepare for application package Prepare all files and directories for that application package. Generates manifest file and all required parameters for that. :param args: list of command line arguments :returns: absolute path to directory with prepared files """ if args.type and args.type not in ['Application', 'Library']: raise exceptions.CommandError( "--type should be set to 'Application' or 'Library'") manifest = generate_manifest(args) if args.type == 'Application': if not args.ui: raise exceptions.CommandError("'--ui' is required parameter") if not os.path.exists(args.ui) or not os.path.isfile(args.ui): raise exceptions.CommandError( "{0} is not a file or doesn`t exist".format(args.ui)) temp_dir = tempfile.mkdtemp() manifest_file = os.path.join(temp_dir, 'manifest.yaml') classes_directory = os.path.join(temp_dir, 'Classes') resource_directory = os.path.join(temp_dir, 'Resources') with open(manifest_file, 'w') as f: f.write(yaml.dump(manifest, default_flow_style=False)) logo_file = os.path.join(temp_dir, 'logo.png') if not args.logo or (args.logo and not os.path.isfile(args.logo)): shutil.copyfile(muranoclient.get_resource('mpl_logo.png'), logo_file) else: shutil.copyfile(args.logo, logo_file) shutil.copytree(args.classes_dir, classes_directory) if args.resources_dir: if not os.path.isdir(args.resources_dir): raise exceptions.CommandError( "'--resources-dir' parameter should be a directory") shutil.copytree(args.resources_dir, resource_directory) if args.ui: ui_directory = os.path.join(temp_dir, 'UI') os.mkdir(ui_directory) shutil.copyfile(args.ui, os.path.join(ui_directory, 'ui.yaml')) return temp_dir def generate_manifest(args): """Generates application manifest file. If some parameters are missed - they we be generated automatically. :param args: :returns: dictionary, contains manifest file data """ if not os.path.isdir(args.classes_dir): raise exceptions.CommandError( "'--classes-dir' parameter should be a directory") args = update_args(args) if not args.type: raise exceptions.CommandError( "Too few arguments: --type and --full-name is required") if not args.author: args.author = args.os_username if not args.description: args.description = "Description for the application is not provided" if not args.full_name: raise exceptions.CommandError( "Please, provide --full-name parameter") manifest = { 'Format': 'MuranoPL/1.0', 'Type': args.type, 'FullName': args.full_name, 'Name': args.name, 'Description': args.description, 'Author': args.author, 'Classes': args.classes } if args.tags: manifest['Tags'] = args.tags return manifest def update_args(args): """Add and update arguments if possible. Some parameters are not required and would be guessed from muranoPL classes: thus, if class extends system application class fully qualified and require names could be calculated. Also, in that case type of a package could be set to 'Application'. """ classes = {} extends_from_application = False for root, dirs, files in os.walk(args.classes_dir): for class_file in files: class_file_path = os.path.join(root, class_file) try: with open(class_file_path) as f: content = yaml.load(f, utils.YaqlYamlLoader) if not content.get('Name'): raise exceptions.CommandError( "Error in class definition: 'Name' " "section is required") class_name = get_fqn_for_name(content.get('Namespaces'), content['Name']) if root == args.classes_dir: relative_path = class_file else: relative_path = os.path.join( root.replace(args.classes_dir, "")[1:], class_file) classes[class_name] = relative_path extends_from_application = check_derived_from_application( content, extends_from_application) if extends_from_application: if not args.type: args.type = 'Application' if not args.name: args.name = class_name.split('.')[-1] if not args.full_name: args.full_name = class_name except yaml.YAMLError: raise exceptions.CommandError( "MuranoPL class {0} should be" " a valid yaml file".format(class_file_path)) except IOError: raise exceptions.CommandError( "Could not open file {0}".format(class_file_path)) if not classes: raise exceptions.CommandError("Application should have " "at least one class") args.classes = classes return args def get_fqn_for_name(namespaces, name): """Analyze name for namespace reference. If namespaces are used - return a full name :param namespaces: content of 'Namespaces' section of muranoPL class :param name: name that should be checked :returns: generated name according to namespaces """ values = name.split(':') if len(values) == 1: if '=' in namespaces: return namespaces['='] + '.' + values[0] return values[0] if len(values) > 2: raise exceptions.CommandError( "Error in class definition: Wrong usage of ':' is " "reserved for namespace referencing and could " "be used only once " "for each name") if not namespaces: raise exceptions.CommandError( "Error in {0} class definition: " "'Namespaces' section is missed") result = namespaces.get(values[0]) if not result: raise exceptions.CommandError( "Error in class definition: namespaces " "reference is not correct at the 'Extends'" " section") return result + '.' + values[1] def check_derived_from_application(content, extends_from_application): """Look up for system 'io.murano.Application' class in extends section""" if content.get('Extends'): extends = content['Extends'] if not isinstance(extends, list): extends = [extends] for name in extends: parent_class_name = get_fqn_for_name( content.get('Namespaces'), name) if parent_class_name == 'io.murano.Application': if not extends_from_application: return True else: raise exceptions.CommandError( "Murano package should have only one class" " extends 'io.murano.Application' class") return False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/packages.py0000664000175000017500000001304200000000000022741 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from oslo_serialization import jsonutils import urllib import yaml from muranoclient.common import base from muranoclient.common import exceptions from muranoclient.common import utils DEFAULT_PAGE_SIZE = 20 class Package(base.Resource): def __repr__(self): return "" % self._info def data(self, **kwargs): return self.manager.data(self, **kwargs) class PackageManager(base.Manager): resource_class = Package _tracked_packages = set() def create(self, data, files): for pkg_file in files.values(): utils.Package.from_file(pkg_file) pkg_file.seek(0) response = self.api.request( '/v1/catalog/packages', 'POST', data={'__metadata__': jsonutils.dumps(data)}, files=files ) if not response.ok: setattr(response, 'status', response.status_code) raise exceptions.from_response(response) body = jsonutils.loads(response.text) return self.resource_class(self, body) def get(self, app_id): return self._get('/v1/catalog/packages/{0}'.format(app_id)) def filter(self, **kwargs): def construct_url(params): for k, v in params.items(): if isinstance(v, str): v = v.encode('utf-8') params[k] = v return '?'.join( ['/v1/catalog/packages', urllib.parse.urlencode(params, doseq=True)] ) def paginate(_url): # code from Glance resp, body = self.api.json_request(_url, 'GET') for package in body['packages']: yield package try: m_kwargs = kwargs.copy() m_kwargs['marker'] = body['next_marker'] next_url = construct_url(m_kwargs) except KeyError: return else: for package in paginate(next_url): yield package if 'page_size' not in kwargs: kwargs['limit'] = kwargs.get('limit', DEFAULT_PAGE_SIZE) else: kwargs['limit'] = kwargs['page_size'] url = construct_url(kwargs) for package in paginate(url): yield self.resource_class(self, package, loaded=True) def list(self, include_disabled=False, limit=20): return self.filter(include_disabled=include_disabled, limit=limit) def delete(self, app_id): return self._delete('/v1/catalog/packages/{0}'.format(app_id)) def update(self, app_id, body, operation='replace'): """Translates dictionary to jsonpatch request :param app_id: string, id of updating application :param body: dictionary, mapping between keys and values for update :param operation: string, way of updating: replace, remove, add :returns: HTTP response """ url = '/v1/catalog/packages/{0}'.format(app_id) data = [] for key, value in body.items(): data.append({'op': operation, 'path': '/' + key, 'value': value}) return self.api.json_patch_request(url, data=data) def download(self, app_id): url = '/v1/catalog/packages/{0}/download'.format(app_id) response = self.api.request(url, 'GET', log=False) if response.status_code == 200: return response.content else: raise exceptions.from_response(response) def toggle_active(self, app_id): url = '/v1/catalog/packages/{0}'.format(app_id) enabled = self.get(app_id).enabled data = [{'op': 'replace', 'path': '/enabled', 'value': not enabled}] return self.api.json_patch_request(url, data=data) def toggle_public(self, app_id): url = '/v1/catalog/packages/{0}'.format(app_id) is_public = self.get(app_id).is_public data = [{'op': 'replace', 'path': '/is_public', 'value': not is_public}] return self.api.json_patch_request(url, body=data) def get_ui(self, app_id, loader_cls=None): if loader_cls is None: loader_cls = yaml.SafeLoader url = '/v1/catalog/packages/{0}/ui'.format(app_id) response = self.api.request(url, 'GET') if response.status_code == 200: return yaml.load(response.content, loader_cls) else: raise exceptions.from_response(response) def get_logo(self, app_id): url = '/v1/catalog/packages/{0}/logo'.format(app_id) response = self.api.request(url, 'GET') if response.status_code == 200: return response.content else: raise exceptions.from_response(response) def get_supplier_logo(self, app_id): url = '/v1/catalog/packages/{0}/supplier_logo'.format(app_id) response = self.api.request(url, 'GET') if response.status_code == 200: return response.content else: raise exceptions.from_response(response) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/request_statistics.py0000664000175000017500000000174600000000000025135 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from muranoclient.common import base class RequestStatistics(base.Resource): def __repr__(self): return "" % self._info def data(self, **kwargs): return self.manager.data(self, **kwargs) class RequestStatisticsManager(base.Manager): resource_class = RequestStatistics def list(self): return self._list('/v1/stats') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/schemas.py0000664000175000017500000000302100000000000022602 0ustar00zuulzuul00000000000000# Copyright (c) 2016 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import urllib from muranoclient.common import base class Schema(base.Resource): def __repr__(self): return "" % self._info @property def data(self): return self._info class SchemaManager(base.Manager): resource_class = Schema def get(self, class_name, method_names=None, class_version=None, package_name=None): """Get JSON-schema for class or method""" if isinstance(method_names, (list, tuple)): method_names = ','.join(method_names) base_url = '/v1/schemas/' + '/'.join( t for t in (class_name, method_names) if t) params = { key: value for key, value in ( ('classVersion', class_version), ('packageName', package_name)) if value } if len(params): base_url += '?' + urllib.parse.urlencode(params, True) return self._get(base_url) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/services.py0000664000175000017500000000641400000000000023013 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import functools import posixpath from muranoclient.common import base def normalize_path(f): @functools.wraps(f) def f_normalize_path(*args, **kwargs): path = args[2] if len(args) >= 3 else kwargs['path'] # path formally is just absolute unix path if not posixpath.isabs(path): raise ValueError("Parameter 'path' should start with '/'") args = list(args) if len(args) >= 3: args[2] = args[2][1:] else: kwargs['path'] = kwargs['path'][1:] return f(*args, **kwargs) return f_normalize_path class Service(base.Resource): def __repr__(self): return '' % self._info def data(self, **kwargs): return self.manager.data(self, **kwargs) def _add_details(self, info): if isinstance(info, dict): for k, v in info.items(): setattr(self, k, v) class ServiceManager(base.Manager): resource_class = Service def list(self, environment_id, session_id=None): if session_id: headers = {'X-Configuration-Session': session_id} else: headers = {} return self._list("/v1/environments/{0}/services". format(environment_id), headers=headers) @normalize_path def get(self, environment_id, path, session_id=None): if session_id: headers = {'X-Configuration-Session': session_id} else: headers = {} return self._get('/v1/environments/{0}/services/{1}'. format(environment_id, path), headers=headers) @normalize_path def post(self, environment_id, path, data, session_id): headers = {'X-Configuration-Session': session_id} result = self._create('/v1/environments/{0}/services/{1}'. format(environment_id, path), data, headers=headers, return_raw=True) if isinstance(result, list): return [self.resource_class(self, item) for item in result] else: return self.resource_class(self, result) @normalize_path def put(self, environment_id, path, data, session_id): headers = {'X-Configuration-Session': session_id} return self._update('/v1/environments/{0}/services/{1}'. format(environment_id, path), data, headers=headers) @normalize_path def delete(self, environment_id, path, session_id): headers = {'X-Configuration-Session': session_id} path = '/v1/environments/{0}/services/{1}'.format(environment_id, path) return self._delete(path, headers=headers) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/sessions.py0000664000175000017500000000326400000000000023036 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from muranoclient.common import base class Session(base.Resource): def __repr__(self): return '' % self._info def data(self, **kwargs): return self.manager.data(self, **kwargs) class SessionManager(base.Manager): resource_class = Session def get(self, environment_id, session_id): return self._get('/v1/environments/{id}/sessions/{session_id}'. format(id=environment_id, session_id=session_id)) def configure(self, environment_id): return self._create('/v1/environments/{id}/configure'. format(id=environment_id), None) def deploy(self, environment_id, session_id): path = '/v1/environments/{id}/sessions/{session_id}/deploy' self.api.json_request(path.format(id=environment_id, session_id=session_id), 'POST') def delete(self, environment_id, session_id): return self._delete("/v1/environments/{id}/sessions/{session_id}". format(id=environment_id, session_id=session_id)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/shell.py0000664000175000017500000014127100000000000022300 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import collections import functools import itertools import json import os import shutil import sys import tempfile import zipfile import jsonpatch from oslo_utils import strutils from oslo_utils import uuidutils import urllib from muranoclient.apiclient import exceptions from muranoclient.common import exceptions as common_exceptions from muranoclient.common import utils from muranoclient.v1.package_creator import hot_package from muranoclient.v1.package_creator import mpl_package _bool_from_str_strict = functools.partial( strutils.bool_from_string, strict=True) @utils.arg('--all-tenants', action='store_true', default=False, help='Allows to list environments from all tenants' ' (admin only).') @utils.arg('--tenant', metavar="", default=None, help='Allows to list environments for a given tenant' ' (admin only).') def do_environment_list(mc, args=None): """List the environments.""" if args is None: args = {} all_tenants = getattr(args, 'all_tenants', False) tenant = getattr(args, 'tenant', None) environments = mc.environments.list(all_tenants, tenant) _print_environment_list(environments) def _print_environment_list(environments): field_labels = ['ID', 'Name', 'Status', 'Created', 'Updated'] fields = ['id', 'name', 'status', 'created', 'updated'] utils.print_list(environments, fields, field_labels, sortby=0) def _generate_join_existing_net(net, subnet): res = { 'defaultNetworks': { 'environment': { '?': { 'id': uuidutils.generate_uuid(dashed=False), 'type': 'io.murano.resources.ExistingNeutronNetwork' }, }, 'flat': None } } if net: res['defaultNetworks']['environment']['internalNetworkName'] = net if subnet: res['defaultNetworks']['environment']['internalSubnetworkName'] = \ subnet return res @utils.arg("--join-net-id", metavar="", help="Network id to join.",) @utils.arg("--join-subnet-id", metavar="", help="Subnetwork id to join.",) @utils.arg("--region", metavar="", help="Name of the target OpenStack region.",) @utils.arg("name", metavar="", help="Environment name.") def do_environment_create(mc, args): """Create an environment.""" body = {"name": args.name, "region": args.region} if args.join_net_id or args.join_subnet_id: body.update(_generate_join_existing_net( args.join_net_id, args.join_subnet_id)) environment = mc.environments.create(body) _print_environment_list([environment]) @utils.arg("id", metavar="", nargs="+", help="Id or name of environment(s) to delete.") @utils.arg('--abandon', action='store_true', default=False, help='If set will abandon environment without deleting any' ' of its resources.') def do_environment_delete(mc, args): """Delete an environment.""" abandon = getattr(args, 'abandon', False) failure_count = 0 for environment_id in args.id: try: environment = utils.find_resource(mc.environments, environment_id) mc.environments.delete(environment.id, abandon) except exceptions.NotFound: failure_count += 1 print("Failed to delete '{0}'; environment not found". format(environment_id)) if failure_count == len(args.id): raise exceptions.CommandError("Unable to find and delete any of the " "specified environments.") do_environment_list(mc) @utils.arg("id", metavar="", help="Environment ID or name.") @utils.arg("name", metavar="", help="A name to which the environment will be renamed.") def do_environment_rename(mc, args): """Rename an environment.""" try: environment = utils.find_resource(mc.environments, args.id) environment = mc.environments.update(environment.id, args.name) except exceptions.NotFound: raise exceptions.CommandError("Environment %s not found" % args.id) else: _print_environment_list([environment]) @utils.arg("id", metavar="", help="Environment ID or name.") @utils.arg("--session-id", metavar="", default='', help="Id of a config session.") @utils.arg("--only-apps", action='store_true', help="Only print apps of the environment (useful for automation).") def do_environment_show(mc, args): """Display environment details.""" try: environment = utils.find_resource( mc.environments, args.id, session_id=args.session_id) except exceptions.NotFound: raise exceptions.CommandError("Environment %s not found" % args.id) else: if getattr(args, 'only_apps', False): print(utils.json_formatter(environment.services)) else: formatters = { "id": utils.text_wrap_formatter, "created": utils.text_wrap_formatter, "name": utils.text_wrap_formatter, "tenant_id": utils.text_wrap_formatter, "services": utils.json_formatter, } utils.print_dict(environment.to_dict(), formatters=formatters) @utils.arg("id", metavar="", help="ID of Environment to deploy.") @utils.arg("--session-id", metavar="", required=True, help="ID of configuration session to deploy.") def do_environment_deploy(mc, args): """Start deployment of a murano environment session.""" mc.sessions.deploy(args.id, args.session_id) do_environment_show(mc, args) @utils.arg("id", help="ID of Environment to call action against.") @utils.arg("--action-id", metavar="", required=True, help="ID of action to run.") @utils.arg("--arguments", metavar='', nargs='*', help="Action arguments.") def do_environment_action_call(mc, args): """Call action `ACTION` in environment `ID`. Returns id of an asynchronous task, that executes the action. Actions can only be called on a `deployed` environment. To view actions available in a given environment use `environment-show` command. """ arguments = {} for argument in args.arguments or []: if '=' not in argument: raise exceptions.CommandError( "Argument should be in form of KEY=VALUE. Found: {0}".format( argument)) k, v = argument.split('=', 1) try: v = json.loads(v) except ValueError: # treat value as a string if it doesn't load as json pass arguments[k] = v task_id = mc.actions.call( args.id, args.action_id, arguments=arguments) print("Created task, id: {0}".format(task_id)) @utils.arg("id", metavar="", help="ID of Environment where task is being executed.") @utils.arg("--task-id", metavar="", required=True, help="ID of action to run.") def do_environment_action_get_result(mc, args): """Get result of `TASK` in environment `ID`.""" result = mc.actions.get_result(args.id, args.task_id) print("Task id result: {0}".format(result)) @utils.arg("class_name", metavar='', help="FQN of the class with static method") @utils.arg("method_name", metavar='', help="Static method to run") @utils.arg("--arguments", metavar='', nargs='*', help="Method arguments. No arguments by default") @utils.arg("--package-name", metavar='', default='', help='Optional FQN of the package to look for the class in') @utils.arg("--class-version", default='', help='Optional version of the class, otherwise version =0 is ' 'used ') def do_static_action_call(mc, args): """Call static method `METHOD` of the class `CLASS` with `ARGUMENTS`. Returns the result of the method execution. `PACKAGE` and `CLASS_VERSION` can be specified optionally to find class in a particular package and to look for the specific version of a class respectively. """ arguments = {} for argument in args.arguments or []: if '=' not in argument: raise exceptions.CommandError( "Argument should be in form of KEY=VALUE. Found: {0}".format( argument)) key, value = argument.split('=', 1) try: value = json.loads(value) except ValueError: # treat value as a string if it doesn't load as json pass arguments[key] = value request_body = { "className": args.class_name, "methodName": args.method_name, "packageName": args.package_name or None, "classVersion": args.class_version or '=0', "parameters": arguments } print("Waiting for result...") try: result = mc.static_actions.call(request_body).get_result() print("Static action result: {0}".format(result)) except Exception as e: print(str(e)) @utils.arg("id", metavar="", help="ID of Environment to add session to.") def do_environment_session_create(mc, args): """Creates a new configuration session for environment ID.""" environment_id = args.id session_id = mc.sessions.configure(environment_id).id print("Created new session:") formatters = {"id": utils.text_wrap_formatter} utils.print_dict({"id": session_id}, formatters=formatters) @utils.arg("id", metavar="", help="ID of Environment to edit.") @utils.arg("filename", metavar="FILE", nargs="?", help="File to read jsonpatch from (defaults to stdin).") @utils.arg("--session-id", metavar="", required=True, help="Id of a config session.") def do_environment_apps_edit(mc, args): """Edit environment's object model. `FILE` is path to a file, that contains jsonpatch, that describes changes to be made to environment's object-model. [ { "op": "add", "path": "/-", "value": { ... your-app object model here ... } }, { "op": "replace", "path": "/0/?/name", "value": "new_name" }, ] NOTE: Values '===id1===', '===id2===', etc. in the resulting object-model will be substituted with uuids. For more info on jsonpatch see RFC 6902 """ jp_obj = None if not args.filename: jp_obj = json.load(sys.stdin) else: with open(args.filename) as fpatch: jp_obj = json.load(fpatch) jpatch = jsonpatch.JsonPatch(jp_obj) environment_id = args.id session_id = args.session_id environment = mc.environments.get(environment_id, session_id) object_model = jpatch.apply(environment.services) utils.traverse_and_replace(object_model) mc.services.put( environment_id, path='/', data=jpatch.apply(environment.services), session_id=session_id) @utils.arg("id", metavar="", help="ID of Environment to show.") @utils.arg("--path", metavar="", default='/', help="Path to Environment model section. Defaults to '/'.") @utils.arg("--session-id", metavar="", help="Id of a config session.") def do_environment_model_show(mc, args): """Display an environment's object model.""" session_id = args.session_id or None path = urllib.parse.quote(args.path) env_model = mc.environments.get_model(args.id, path, session_id) print(utils.json_formatter(env_model)) @utils.arg("id", metavar="", help="ID of Environment to edit.") @utils.arg("filename", metavar="", nargs="?", help="File to read JSON-patch from (defaults to stdin).") @utils.arg("--session-id", metavar="", required=True, help="Id of a config session.") def do_environment_model_edit(mc, args): """Edit an environment's object model.""" jp_obj = None if not args.filename: jp_obj = json.load(sys.stdin) else: with open(args.filename) as fpatch: jp_obj = json.load(fpatch) if not isinstance(jp_obj, list): raise exceptions.CommandError('JSON-patch must be a list of changes') for change in jp_obj: if 'op' not in change or 'path' not in change: raise exceptions.CommandError('Every change in JSON-patch must ' 'contain "op" and "path" keys') op = change['op'] if op not in ['add', 'replace', 'remove']: raise exceptions.CommandError('The value of "op" item must be ' '"add", "replace" or "remove", ' 'got {0}'.format(op)) if op != 'remove' and 'value' not in change: raise exceptions.CommandError('"add" or "replace" change in ' 'JSON-patch must contain "value" ' 'key') session_id = args.session_id new_model = mc.environments.update_model(args.id, jp_obj, session_id) print(utils.json_formatter(new_model)) def do_env_template_list(mc, args=None): """List the environments templates.""" if args is None: args = {} env_templates = mc.env_templates.list() _print_env_template_list(env_templates) def _print_env_template_list(env_templates): field_labels = ['ID', 'Name', 'Created', 'Updated', 'Is public'] fields = ['id', 'name', 'created', 'updated', 'is_public'] utils.print_list(env_templates, fields, field_labels, sortby=0) @utils.arg("name", metavar="", help="Environment template name.") @utils.arg("--is-public", action='store_true', default=False, help='Make the template available for users from other tenants.') def do_env_template_create(mc, args): """Create an environment template.""" env_template = mc.env_templates.create( {"name": args.name, "is_public": args.is_public}) _print_env_template_list([env_template]) @utils.arg("id", metavar="", help="Environment template ID.") @utils.arg("name", metavar="", help="New environment name.") @utils.arg("--region", metavar="", help="Name of the target OpenStack region.",) def do_env_template_create_env(mc, args): """Create a new environment from template.""" try: data = {} data["name"] = args.name if args.region: data["region"] = args.region template = mc.env_templates.create_env(args.id, data) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Environment template %s not found" % args.id) else: formatters = { "environment_id": utils.text_wrap_formatter, "session_id": utils.text_wrap_formatter } utils.print_dict(template.to_dict(), formatters=formatters) @utils.arg("id", metavar="", help="Environment template ID.") def do_env_template_show(mc, args): """Display environment template details.""" try: env_template = mc.env_templates.get(args.id) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Environment template %s not found" % args.id) else: formatters = { "id": utils.text_wrap_formatter, "created": utils.text_wrap_formatter, "name": utils.text_wrap_formatter, "tenant_id": utils.text_wrap_formatter, "services": utils.json_formatter, } utils.print_dict(env_template.to_dict(), formatters=formatters) @utils.arg("id", metavar="", help="Environment template ID.") @utils.arg('app_template_file', metavar='', help='Path to the template.') def do_env_template_add_app(mc, args): """Add application to the environment template.""" with open(args.app_template_file, "r") as app_file: app_templates = json.load(app_file) if not isinstance(app_templates, list): app_templates = [app_templates] for app_template in app_templates: mc.env_templates.create_app(args.id, app_template) do_env_template_show(mc, args) @utils.arg("id", metavar="", help="Environment template ID.") @utils.arg("app_id", metavar="", help="Application ID.") def do_env_template_del_app(mc, args): """Delete application from the environment template.""" mc.env_templates.delete_app(args.id, args.app_id) do_env_template_show(mc, args) @utils.arg("id", metavar="", help="Environment template ID.") @utils.arg("name", metavar="", help="Environment template name.") def do_env_template_update(mc, args): """Update an environment template.""" try: env_template = mc.env_templates.update(args.id, args.name) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Environment template %s not found" % args.id) _print_env_template_list([env_template]) @utils.arg("id", metavar="", nargs="+", help="ID of environment(s) template to delete.") def do_env_template_delete(mc, args): """Delete an environment template.""" failure_count = 0 for env_template_id in args.id: try: mc.env_templates.delete(env_template_id) except common_exceptions.HTTPNotFound: failure_count += 1 mns = "Failed to delete '{0}'; environment template not found".\ format(env_template_id) if failure_count == len(args.id): raise exceptions.CommandError(mns) do_env_template_list(mc) @utils.arg("id", metavar="", help="Environment template ID.") @utils.arg("name", metavar="", help="New environment template name.") def do_env_template_clone(mc, args): """Create a new template, cloned from template.""" try: env_template = mc.env_templates.clone(args.id, args.name) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Environment template %s not found" % args.id) else: formatters = { "id": utils.text_wrap_formatter, "created": utils.text_wrap_formatter, "updated": utils.text_wrap_formatter, "version": utils.text_wrap_formatter, "name": utils.text_wrap_formatter, "tenant_id": utils.text_wrap_formatter, "is_public": utils.text_wrap_formatter, "services": utils.json_formatter, } utils.print_dict(env_template.to_dict(), formatters=formatters) @utils.arg("id", metavar="", nargs='?', default=None, help="Environment ID for which to list deployments.") @utils.arg('--all-environments', action='store_true', default=False, help="Lists all deployments for all environments in user's tenant.") def do_deployment_list(mc, args): """List deployments for an environment or multiple environments.""" all_environments = getattr(args, 'all_environments', False) env_id = getattr(args, 'id', None) if env_id and all_environments: raise exceptions.CommandError( 'Environment ID and all-environments flag cannot both be set.') elif not env_id and not all_environments: raise exceptions.CommandError( 'Either environment ID or all-environments flag must be set.') try: if all_environments: deployments = mc.deployments.list(None, all_environments) else: environment = utils.find_resource(mc.environments, env_id) deployments = mc.deployments.list(environment.id) except exceptions.NotFound: if env_id: msg = "Environment %s not found" % env_id else: msg = "Environments not found" raise exceptions.CommandError(msg) else: field_labels = ["ID", "State", "Created", "Updated", "Finished"] fields = ["id", "state", "created", "updated", "finished"] utils.print_list(deployments, fields, field_labels, sortby=0) @utils.arg("--limit", type=int, default=0, help='Show limited number of packages') @utils.arg("--marker", default='', help='Show packages starting from package with id excluding it') @utils.arg("--include-disabled", default=False, action="store_true") @utils.arg("--owned", default=False, action="store_true") @utils.arg('--search', metavar='', dest='search', required=False, help='Show packages, that match search keys fuzzily') @utils.arg('--name', metavar='', dest='name', required=False, help='Show packages, whose name match parameter exactly') @utils.arg('--fqn', metavar="", dest='fqn', required=False, help='Show packages, ' 'whose fully qualified name match parameter exactly') @utils.arg('--type', metavar='', dest='type', required=False, help='Show packages, whose type match parameter exactly') @utils.arg('--category', metavar='', dest='category', required=False, help='Show packages, whose categories include parameter') @utils.arg('--class_name', metavar='', dest='class_name', required=False, help='Show packages, whose class name match parameter exactly') @utils.arg('--tag', metavar='', dest='tag', required=False, help='Show packages, whose tags include parameter') def do_package_list(mc, args=None): """List available packages.""" filter_args = { "include_disabled": getattr(args, 'include_disabled', False), "owned": getattr(args, 'owned', False), } if args: if args.limit < 0: raise exceptions.CommandError( '--limit parameter must be non-negative') if args.limit != 0: filter_args['limit'] = args.limit if args.marker: filter_args['marker'] = args.marker if args.search: filter_args['search'] = args.search if args.name: filter_args['name'] = args.name if args.fqn: filter_args['fqn'] = args.fqn if args.type: filter_args['type'] = args.type if args.category: filter_args['category'] = args.category if args.class_name: filter_args['class_name'] = args.class_name if args.tag: filter_args['tag'] = args.tag packages = mc.packages.filter(**filter_args) if not args or args.limit == 0: _print_package_list(packages) else: _print_package_list(itertools.islice(packages, args.limit)) def _print_package_list(packages): field_labels = ["ID", "Name", "FQN", "Author", "Active", "Is Public", "Type", "Version"] fields = ["id", "name", "fully_qualified_name", "author", "enabled", "is_public", "type", "version"] utils.print_list(packages, fields, field_labels, sortby=0) @utils.arg("id", metavar="", help="Package ID to download.") @utils.arg("filename", metavar="file", nargs="?", help="Filename to save package to. If it is not specified and " "there is no stdout redirection the package won't be saved.") def do_package_download(mc, args): """Download a package to a filename or stdout.""" def download_to_fh(package_id, fh): fh.write(mc.packages.download(package_id)) try: if args.filename: with open(args.filename, 'wb') as fh: download_to_fh(args.id, fh) print("Package downloaded to %s" % args.filename) elif not sys.stdout.isatty(): download_to_fh(args.id, sys.stdout) else: msg = ('No stdout redirection or local file specified for ' 'downloaded package. Please specify a local file to save ' 'downloaded package or redirect output to another source.') raise exceptions.CommandError(msg) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Package %s not found" % args.id) @utils.arg("id", metavar="", help="Package ID to show.") def do_package_show(mc, args): """Display details for a package.""" try: package = mc.packages.get(args.id) except common_exceptions.HTTPNotFound: raise exceptions.CommandError("Package %s not found" % args.id) else: to_display = dict( id=package.id, type=package.type, owner_id=package.owner_id, name=package.name, fully_qualified_name=package.fully_qualified_name, is_public=package.is_public, enabled=package.enabled, class_definitions=", ".join(package.class_definitions), categories=", ".join(package.categories), tags=", ".join(package.tags), description=package.description ) formatters = { 'class_definitions': utils.text_wrap_formatter, 'categories': utils.text_wrap_formatter, 'tags': utils.text_wrap_formatter, 'description': utils.text_wrap_formatter, } utils.print_dict(to_display, formatters) @utils.arg("id", metavar="", nargs='+', help="Package ID to delete.") def do_package_delete(mc, args): """Delete a package.""" failure_count = 0 for package_id in args.id: try: mc.packages.delete(package_id) print("Deleted package '{0}'".format(package_id)) except exceptions.NotFound: failure_count += 1 print("Failed to delete '{0}'; package not found". format(package_id)) if failure_count == len(args.id): raise exceptions.CommandError("Unable to find and delete any of the " "specified packages.") else: do_package_list(mc) def _handle_package_exists(mc, data, package, exists_action): name = package.manifest['FullName'] version = package.manifest.get('Version', '0') while True: print("Importing package {0}".format(name)) try: return mc.packages.create(data, {name: package.file()}) except common_exceptions.HTTPConflict: print("Importing package {0} failed. Package with the same" " name/classes is already registered.".format(name)) allowed_results = ['s', 'u', 'a'] res = exists_action if not res: while True: print("What do you want to do? (s)kip, (u)pdate, (a)bort") res = input() if res in allowed_results: break if res == 's': print("Skipping.") return None elif res == 'a': print("Exiting.") sys.exit() elif res == 'u': pkgs = list(mc.packages.filter(fqn=name, version=version, owned=True)) if not pkgs: msg = ( "Got a conflict response, but could not find the " "package '{0}' in the current tenant.\nThis probably " "means the conflicting package is in another tenant.\n" "Please delete it manually." ).format(name) raise exceptions.CommandError(msg) elif len(pkgs) > 1: msg = ( "Got {0} packages with name '{1}'.\nI do not trust " "myself, please delete the package manually." ).format(len(pkgs), name) raise exceptions.CommandError(msg) print("Deleting package {0}({1})".format(name, pkgs[0].id)) mc.packages.delete(pkgs[0].id) continue @utils.arg('filename', metavar='', nargs='+', help='URL of the murano zip package, FQPN, path to zip package' ' or path to directory with package.') @utils.arg('-c', '--categories', metavar='', nargs='*', help='Category list to attach.') @utils.arg('--is-public', action='store_true', default=False, help='Make the package available for users from other tenants.') @utils.arg('--package-version', default='', help='Version of the package to use from repository ' '(ignored when importing with multiple packages).') @utils.arg('--exists-action', default='', choices=['a', 's', 'u'], help='Default action when a package already exists: ' '(s)kip, (u)pdate, (a)bort.') @utils.arg('--dep-exists-action', default='', choices=['a', 's', 'u'], help='Default action when a dependency package already exists: ' '(s)kip, (u)pdate, (a)bort.') def do_package_import(mc, args): """Import a package. `FILE` can be either a path to a zip file, url or a FQPN. You can use `--` to separate `FILE`s from other arguments. Categories have to be separated with a space and have to be already present in murano. """ data = {"is_public": args.is_public} exception_occurred = False version = args.package_version if version and len(args.filename) >= 2: print("Requested to import more than one package, " "ignoring version.") version = '' if args.categories: data["categories"] = args.categories total_reqs = collections.OrderedDict() main_packages_names = [] for filename in args.filename: if os.path.isfile(filename) or os.path.isdir(filename): _file = filename else: print("Package file '{0}' does not exist, attempting to download" "".format(filename)) _file = utils.to_url( filename, version=version, base_url=args.murano_repo_url, extension='.zip', path='apps/', ) try: package = utils.Package.from_file(_file) except Exception as e: print("Failed to create package for '{0}', reason: {1}".format( filename, e)) exception_occurred = True continue total_reqs.update(package.requirements(base_url=args.murano_repo_url)) main_packages_names.append(package.manifest['FullName']) imported_list = [] dep_exists_action = args.dep_exists_action if dep_exists_action == '': dep_exists_action = args.exists_action for name, package in total_reqs.items(): image_specs = package.images() if image_specs: print("Inspecting required images") try: imgs = utils.ensure_images( glance_client=mc.glance_client, image_specs=image_specs, base_url=args.murano_repo_url, is_package_public=args.is_public) for img in imgs: print("Added {0}, {1} image".format( img['name'], img['id'])) except Exception as e: print("Error {0} occurred while installing " "images for {1}".format(e, name)) exception_occurred = True if name in main_packages_names: exists_action = args.exists_action else: exists_action = dep_exists_action try: imported_package = _handle_package_exists( mc, data, package, exists_action) if imported_package: imported_list.append(imported_package) except Exception as e: print("Error {0} occurred while installing package {1}".format( e, name)) exception_occurred = True if imported_list: _print_package_list(imported_list) if exception_occurred: # NOTE(jose-phillips) Leave a Warning to users in case some packages # can be uploaded successfully. if imported_list: print("Warning: there were some errors during the operation.") sys.exit(1) else: sys.exit(1) @utils.arg("id", metavar="", help="Package ID to update.") @utils.arg('--is-public', type=_bool_from_str_strict, metavar='{true|false}', help='Make package available to users from other tenants.') @utils.arg('--enabled', type=_bool_from_str_strict, metavar='{true|false}', help='Make package active and available for deployments.') @utils.arg('--name', default=None, help='New name for the package.') @utils.arg('--description', default=None, help='New package description.') @utils.arg('--tags', metavar='', nargs='*', default=None, help='A list of keywords connected to the application.') def do_package_update(mc, args): """Update an existing package.""" data = {} parameters = ('is_public', 'enabled', 'name', 'description', 'tags') for parameter in parameters: param_value = getattr(args, parameter, None) if param_value is not None: data[parameter] = param_value mc.packages.update(args.id, data) do_package_show(mc, args) @utils.arg('filename', metavar='', nargs='+', help='Bundle URL, bundle name, or path to the bundle file.') @utils.arg('--is-public', action='store_true', default=False, help='Make packages available to users from other tenants.') @utils.arg('--exists-action', default='', choices=['a', 's', 'u'], help='Default action when a package already exists.') def do_bundle_import(mc, args): """Import a bundle. `FILE` can be either a path to a zip file, URL, or name from repo. If `FILE` is a local file, treat names of packages in a bundle as file names, relative to location of the bundle file. Requirements are first searched in the same directory. """ total_reqs = collections.OrderedDict() for filename in args.filename: local_path = None if os.path.isfile(filename): _file = filename local_path = os.path.dirname(os.path.abspath(filename)) else: print("Bundle file '{0}' does not exist, attempting to download" "".format(filename)) _file = utils.to_url( filename, base_url=args.murano_repo_url, path='bundles/', extension='.bundle', ) try: bundle_file = utils.Bundle.from_file(_file) except Exception as e: print("Failed to create bundle for '{0}', reason: {1}".format( filename, e)) continue data = {"is_public": args.is_public} try: for package in bundle_file.packages( base_url=args.murano_repo_url, path=local_path): requirements = package.requirements( base_url=args.murano_repo_url, path=local_path, ) total_reqs.update(requirements) except Exception: print("Can't parse bundle contents") continue imported_list = [] for name, dep_package in total_reqs.items(): image_specs = dep_package.images() if image_specs: print("Inspecting required images") try: imgs = utils.ensure_images( glance_client=mc.glance_client, image_specs=image_specs, base_url=args.murano_repo_url, local_path=local_path, is_package_public=args.is_public) for img in imgs: print("Added {0}, {1} image".format( img['name'], img['id'])) except Exception as e: print("Error {0} occurred while installing " "images for {1}".format(e, name)) try: imported_package = _handle_package_exists( mc, data, dep_package, args.exists_action) if imported_package: imported_list.append(imported_package) except exceptions.CommandError: raise except Exception as e: print("Error {0} occurred while " "installing package {1}".format(e, name)) if imported_list: _print_package_list(imported_list) def _handle_save_packages(packages, dst, base_url, no_images): downloaded_images = [] for name, pkg in packages.items(): if not no_images: image_specs = pkg.images() for image_spec in image_specs: if not image_spec["Name"]: print("Invalid image.lst file for {0} package. " "'Name' section is absent.".format(name)) continue if image_spec["Name"] not in downloaded_images: print("Package {0} depends on image {1}. " "Downloading...".format(name, image_spec["Name"])) try: utils.save_image_local(image_spec, base_url, dst) downloaded_images.append(image_spec["Name"]) except Exception as e: print("Error {0} occurred while saving image {1}". format(e, image_spec["Name"])) try: pkg.save(dst) print("Package {0} has been successfully saved".format(name)) except Exception as e: print("Error {0} occurred while saving package {1}".format( e, name)) @utils.arg('filename', metavar='', help='Bundle URL, bundle name, or path to the bundle file.') @utils.arg('-p', '--path', metavar='', help='Path to the directory to store packages. If not set will use ' 'current directory.') @utils.arg('--no-images', action='store_true', default=False, help='If set will skip images downloading.') def do_bundle_save(mc, args): """Save a bundle. This will download a bundle of packages with all dependencies to specified path. If path doesn't exist it will be created. """ bundle = args.filename base_url = args.murano_repo_url if args.path: if not os.path.exists(args.path): os.makedirs(args.path) dst = args.path else: dst = os.getcwd() total_reqs = collections.OrderedDict() if os.path.isfile(bundle): _file = bundle else: print("Bundle file '{0}' does not exist, attempting to download" .format(bundle)) _file = utils.to_url( bundle, base_url=base_url, path='bundles/', extension='.bundle', ) try: bundle_file = utils.Bundle.from_file(_file) except Exception as e: msg = "Failed to create bundle for {0}, reason: {1}".format(bundle, e) raise exceptions.CommandError(msg) for package in bundle_file.packages(base_url=base_url): requirements = package.requirements(base_url=base_url) total_reqs.update(requirements) no_images = getattr(args, 'no_images', False) _handle_save_packages(total_reqs, dst, base_url, no_images) try: bundle_file.save(dst, binary=False) print("Bundle file {0} has been successfully saved".format(bundle)) except Exception as e: print("Error {0} occurred while saving bundle {1}".format(e, bundle)) @utils.arg('package', metavar='', nargs='+', help='Package URL or name.') @utils.arg('-p', '--path', metavar='', help='Path to the directory to store package. If not set will use ' 'current directory.') @utils.arg('--package-version', default='', help='Version of the package to use from repository ' '(ignored when saving with multiple packages).') @utils.arg('--no-images', action='store_true', default=False, help='If set will skip images downloading.') def do_package_save(mc, args): """Save a package. This will download package(s) with all dependencies to specified path. If path doesn't exist it will be created. """ base_url = args.murano_repo_url if args.path: if not os.path.exists(args.path): os.makedirs(args.path) dst = args.path else: dst = os.getcwd() version = args.package_version if version and len(args.filename) >= 2: print("Requested to save more than one package, " "ignoring version.") version = '' total_reqs = collections.OrderedDict() for package in args.package: _file = utils.to_url( package, version=version, base_url=base_url, extension='.zip', path='apps/', ) try: pkg = utils.Package.from_file(_file) except Exception as e: print("Failed to create package for '{0}', reason: {1}".format( package, e)) continue total_reqs.update(pkg.requirements(base_url=base_url)) no_images = getattr(args, 'no_images', False) _handle_save_packages(total_reqs, dst, base_url, no_images) @utils.arg('id', metavar='', help='Environment ID to show applications from.') @utils.arg('-p', '--path', metavar='', help='Level of detalization to show. ' 'Leave empty to browse all applications in the environment.', default='/') def do_app_show(mc, args): """List applications, added to specified environment.""" if args.path == '/': apps = mc.services.list(args.id) formatters = {'id': lambda x: getattr(x, '?')['id'], 'type': lambda x: getattr(x, '?')['type']} field_labels = ['Id', 'Name', 'Type'] fields = ['id', 'name', 'type'] utils.print_list(apps, fields, field_labels, formatters=formatters) else: if not args.path.startswith('/'): args.path = '/' + args.path app = mc.services.get(args.id, args.path) # If app with specified path is not found, it is empty. if hasattr(app, '?'): formatters = {} for key in app.to_dict().keys(): formatters[key] = utils.json_formatter utils.print_dict(app.to_dict(), formatters) else: raise exceptions.CommandError("Could not find application at path" " %s" % args.path) @utils.arg('-t', '--template', metavar='', help='Path to the Heat template to import as ' 'an Application Definition.') @utils.arg('-c', '--classes-dir', metavar='', help='Path to the directory containing application classes.') @utils.arg('-r', '--resources-dir', metavar='', help='Path to the directory containing application resources.') @utils.arg('-n', '--name', metavar='', help='Display name of the Application in Catalog.') @utils.arg('-f', '--full-name', metavar='', help='Fully-qualified name of the Application in Catalog.') @utils.arg('-a', '--author', metavar='', help='Name of the publisher.') @utils.arg('--tags', help='A list of keywords connected to the application.', metavar='', nargs='*') @utils.arg('-d', '--description', metavar='', help='Detailed description for the Application in Catalog.') @utils.arg('-o', '--output', metavar='', help='The name of the output file archive to save locally.') @utils.arg('-u', '--ui', metavar='', help='Dynamic UI form definition.') @utils.arg('--type', help='Package type. Possible values: Application or Library.') @utils.arg('-l', '--logo', metavar='', help='Path to the package logo.') def do_package_create(mc, args): """Create an application package.""" if args.template and args.classes_dir: raise exceptions.CommandError( "Provide --template for a HOT-based package, OR" " --classes-dir for a MuranoPL-based package") if not args.template and not args.classes_dir: raise exceptions.CommandError( "Provide --template for a HOT-based package, OR at least" " --classes-dir for a MuranoPL-based package") directory_path = None try: archive_name = args.output if args.output else None if args.template: directory_path = hot_package.prepare_package(args) if not archive_name: archive_name = os.path.basename(args.template) archive_name = os.path.splitext(archive_name)[0] + ".zip" else: directory_path = mpl_package.prepare_package(args) if not archive_name: archive_name = tempfile.mkstemp( prefix="murano_", dir=os.getcwd())[1] + ".zip" _make_archive(archive_name, directory_path) print("Application package is available at " + os.path.abspath(archive_name)) finally: if directory_path: shutil.rmtree(directory_path) def _make_archive(archive_name, path): zip_file = zipfile.ZipFile(archive_name, 'w') for root, dirs, files in os.walk(path): for f in files: zip_file.write(os.path.join(root, f), arcname=os.path.join(os.path.relpath(root, path), f)) def do_category_list(mc, args=None): """List all available categories.""" if args is None: args = {} categories = mc.categories.list() _print_category_list(categories) def _print_category_list(categories): field_labels = ["ID", "Name"] fields = ["id", "name"] utils.print_list(categories, fields, field_labels) @utils.arg("id", metavar="", help="ID of a category(s) to show.") def do_category_show(mc, args): """Display category details.""" try: category = mc.categories.get(args.id) packages = mc.packages.filter(category=category.name) to_display = dict(id=category.id, name=category.name, packages=', '.join(p.name for p in packages)) formatters = {'packages': utils.text_wrap_formatter} utils.print_dict(to_display, formatters) except common_exceptions.HTTPNotFound: print("Category id '{0}' not found". format(args.id)) @utils.arg("name", metavar="", help="Category name.") def do_category_create(mc, args): """Create a category.""" category = mc.categories.add({"name": args.name}) _print_category_list([category]) @utils.arg("id", metavar="", nargs="+", help="ID of a category(ies) to delete.") def do_category_delete(mc, args): """Delete a category.""" failure_count = 0 for category_id in args.id: try: mc.categories.delete(category_id) except common_exceptions.HTTPNotFound: failure_count += 1 print("Failed to delete '{0}'; category not found". format(category_id)) if failure_count == len(args.id): raise exceptions.CommandError("Unable to find and delete any of the " "specified categories.") do_category_list(mc) @utils.arg("class_name", metavar="", help="Class FQN") @utils.arg("method_names", metavar="", help="Method name", nargs='*') @utils.arg("--package-name", default=None, help="FQN of the package where the class is located") @utils.arg("--class-version", default='=0', help="Class version or version range (version spec)") def do_class_schema(mc, args): """Display class schema""" schema = mc.schemas.get(args.class_name, args.method_names, class_version=args.class_version, package_name=args.package_name) print(utils.json_formatter(schema.data)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/static_actions.py0000664000175000017500000000262600000000000024200 0ustar00zuulzuul00000000000000# Copyright (c) 2016 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. class StaticActionResult(object): def __init__(self, result, exception=None): self._result = result self._exception = exception def get_result(self): if self._exception: raise self._exception return self._result def check_result(self): return True # Not a true manager yet; should be changed to be one if CRUD # functionality becomes available for actions. class StaticActionManager(object): def __init__(self, api): self.api = api def call(self, arguments): url = '/v1/actions' try: resp, body = self.api.json_request(url, 'POST', data=arguments) return StaticActionResult(body) except Exception as e: if e.code >= 500: raise return StaticActionResult(None, exception=e) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1709301692.0 python-muranoclient-2.8.0/muranoclient/v1/templates.py0000664000175000017500000000704700000000000023171 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Mirantis, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. from muranoclient.common import base class Template(base.Resource): """Involves the template resource.""" def __repr__(self): return "