././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8645468 python-mistralclient-5.3.0/0000775000175000017500000000000000000000000015733 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/.coveragerc0000664000175000017500000000014200000000000020051 0ustar00zuulzuul00000000000000[run] source = mistralclient omit = .tox/* mistralclient/tests/* [report] ignore_errors = True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/.stestr.conf0000664000175000017500000000011000000000000020174 0ustar00zuulzuul00000000000000[DEFAULT] test_path=${TEST_PATH:-./mistralclient/tests/unit} top_dir=./ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/.zuul.yaml0000664000175000017500000000246700000000000017705 0ustar00zuulzuul00000000000000- job: name: python-mistralclient-functional-devstack parent: devstack-tox-functional-consumer timeout: 9000 vars: devstack_plugins: mistral: https://opendev.org/openstack/mistral heat: https://opendev.org/openstack/heat devstack_services: heat: true h-api: true h-api-cfn: true h-api-cw: true h-eng: true tox_envlist: functional tox_environment: IDENTITY_API_VERSION: 3 PYTHONUNBUFFERED: 'true' MISTRAL_USE_MOD_WSGI: true MISTRAL_RPC_IMPLEMENTATION: oslo MYSQL_ROOT_PW: secretdatabase required-projects: - openstack/heat - openstack/mistral - openstack/mistral-lib - openstack/mistral-extra - openstack/python-mistralclient - project: templates: - check-requirements - openstack-python3-jobs - openstackclient-plugin-jobs - publish-openstack-docs-pti - release-notes-jobs-python3 check: jobs: - openstack-tox-cover: voting: false irrelevant-files: - ^.zuul.yaml$ - ^.*\.rst$ - ^doc/.*$ - ^releasenotes/.*$ - python-mistralclient-functional-devstack gate: jobs: - python-mistralclient-functional-devstack ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030372.0 python-mistralclient-5.3.0/AUTHORS0000664000175000017500000001113600000000000017005 0ustar00zuulzuul00000000000000Adriano Petrich Alex Schultz Anastasia Kuznetsova Andras Kovi Andreas Jaeger Angus Salkeld Bob Haddleton Boris Bobrov Brad P. Crochet Cao Xuan Hoang Chen Christian Berendt Corey Bryant Daryl Mowrer David C Kennedy David Vallee Delisle Dawid Deja Dina Belova Dmitri Zimine Doug Hellmann Dougal Matthews Ed Cranford Eyal Flavio Percoco Ghanshyam Mann Goutham Pratapa GouthamPratapa Hangdong Zhang Hardik Parekh Hervé Beraud Ian Wienand Istvan Imre James Slagle Jamie Lennox Jeremy Liu Jeremy Stanley Ji zhaoxuan Juan Antonio Osorio Robles KATO Tomoyuki Kevin Pouget Kevin_Zheng Kirill Izotov Kupai József Leandro I. Costantino Limor Stotland Lingxian Kong LingxianKong LingxianKong Lucky samadhiya Marcos Fermin Lobo Michal Gershenzon Michal Gershenzon Mike Fedosin Nguyen Hai Nguyen Hung Phuong Nikolay Mahotkin OpenStack Release Bot Pierre Gaxatte Pierre-Arthur MATHIEU Prince Katiyar Quentin GROLLEAU Rafael Folco Renat Akhmerov Renat Akhmerov Robert Collins Sagi Shnaidman Sean McGinnis Sharat Sharma Sirushti Murugesan Stephen Finucane Steve Martinelli Takashi Kajinami Tang Chen Tetiana Lashchova Theodoros Tsioutsias Thomas Goirand Thomas Herve TimurNurlygayanov Tony Xu Toure Dunnon Tovin Seven Vieri <15050873171@163.com> Vitalii Solodilov W Chan Winson Chan Zhenguo Niu ZhijunWei ali cao.yuan caoyuan fengchaoyang gengchc2 hardik hardikj houweichao hparekh huang.zhiping jacky06 kangyufei pawnesh.kumar pengdake <19921207pq@gmail.com> pengyuesheng privaterookie <996514515@qq.com> qingszhao rakhmerov reedip ricolin ricolin shu-mutou sunjia venkatamahesh wangqi wangzhenyu wu.chunyang wu.shiming xianming mao yfzhao zhangboye zhangdebo zhangguoqing “ramboman” 翟小君 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/CONTRIBUTING.rst0000664000175000017500000000121700000000000020375 0ustar00zuulzuul00000000000000The source repository for this project can be found at: https://opendev.org/openstack/python-mistralclient 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-mistralclient For more specific information about contributing to this repository, see the python-mistralclient contributor guide: https://docs.openstack.org/python-mistralclient/latest/contributor/contributing.html ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030372.0 python-mistralclient-5.3.0/ChangeLog0000664000175000017500000005536400000000000017522 0ustar00zuulzuul00000000000000CHANGES ======= 5.3.0 ----- * Update master for stable/2024.1 5.2.0 ----- * Fix tox4 error and use python >=3.8 * Update master for stable/zed 4.5.0 ----- * Update master for stable/yoga 4.4.0 ----- * Allow enforcing usage of "raw" definitions * [community goal] Update contributor documentation * Remove tripleo job from mistralclient * Use py3 as the default runtime for tox * Add Python3 xena unit tests * Update master for stable/wallaby * Drop lower-constraints job 4.2.0 ----- * remove unicode from code * Update TOX\_CONSTRAINTS\_FILE * Add unit tests for dynamic actions CLI commands * Add unit tests for code sources CLI commands * Add support for code sources and dynamic actions * Fix functional action CLI test * requirements: Drop os-testr * Remove install unnecessary packages * Add Python3 wallaby unit tests * Update master for stable/victoria 4.1.1 ----- * [goal] Migrate testing to ubuntu focal 4.1.0 ----- * drop mock from lower-constraints * Switch to newer openstackdocstheme and reno versions * Fix hacking min version to 3.0.1 * Remove all usage of six library * added support statistics only for report execution API * Bump default tox env from py37 to py38 * Add py38 package metadata * Use unittest.mock instead of third party mock * Add Python3 victoria unit tests * Update master for stable/ussuri 4.0.1 ----- * fixed compatibility issues between mistral and client * Cleanup py27 support * Fix preinstalled workflows tests 4.0.0 ----- * Add "duration" to task executions printed by CLI commands * Add "duration" to workflow executions printed by CLI commands * Create client for interactive shell * Add new CLI commands for sub-executions new API endpoints * Add namespace support for actions to client * Update hacking and fix warning * Add script for unit test coverage job * [ussuri][goal] Drop python 2.7 support and testing * fix the mistralclient set endpoint\_type invaild * Roots only option for executions-list * tox: Keeping going with docs * Switch to Ussuri jobs * Update master for stable/train 3.10.0 ------ * remove unused gate code \* post\_test\_hook and run\_tests used in legacy gate * Add "published\_global" field to the task execution REST resource * Add "retry\_count" to execution report * Rename the test class so it will be PyCharm friendly * Some resource managers are not using the base underscore methods * Add Python 3 Train unit tests * Modify the url of upper\_constraints\_file * Use openstack-python3-train-jobs for python3 test runtime * Add python 3.7 classifier to setup.cfg * Bump openstackdocstheme to 1.30.0 * Blacklist sphinx 2.1.0 (autodoc bug) * Update sphinx dependency 3.9.0 ----- * Don't use default mutable parameter * 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 * Change tripleo job to multinode-containers job * Fix typo for self.list 3.8.0 ----- * Release note for the "execution-get-report" command * Fix execution report printing * Update json module to jsonutils * add python 3.7 unit test job * Add 'execution-get-report' command * Replace tripleo-scenario003-multinode with scenario003-standalone * Update hacking version * Use template for lower-constraints * Provide consistent options and return latest execution entries * Change openstack-dev to openstack-discuss * Add Python 3.6 classifier to setup.cfg * add python 3.6 unit test job * Fix Chinese quotes * Remove tripleo newton and ocata jobs * Remove setup.py check from pep8 job * Update min tox version to 2.0 * Remove setup.py check from pep8 job * Can't pause a async action using cli * Follow the new PTI for document build * add python 3.6 unit test job * switch documentation job to new PTI * import zuul job settings from project-config * fix mistral client failure * Add namespace support for workbooks to client * Migrate python-mistralclient jobs to Zuul v3 * Update reno for stable/rocky 3.7.0 ----- * Clarify details about the target cacert parameter * Add missing \`--public\` option to workbook api * Add the root execution ID to the CLI output * Add missing oslo.serialization requirement 3.6.1 ----- * Fixing region filter for getting workflow endpoint * Add release note link in README * Add the restructuredtext check to the flake8 job 3.6.0 ----- * Remove PyPI downloads * Fix force parameter being always added to URL in execution delete * Add --export option to environment-get * Revert "Adding environment-get-definition for easy environment-update" * fix tox python3 overrides * Switch to using stestr * Update task columns info * Add namespace parameter to workflow-get and workflow-get-definition * Adding environment-get-definition for easy environment-update 3.5.0 ----- * Trivial: Update pypi url to new url * Do not let keystoneauth mask the errors * Add the force parameter to delete executions 3.4.0 ----- * add lower-constraints job * Updated from global requirements * Updated from global requirements * Updated from global requirements * Add the scope attribute in workflow list * Updated from global requirements * Updated from global requirements * Update links in README * Clean imports in code * Updated from global requirements * Add TripleO jobs for mistral client * Don't override session during auth * Fix a race condition with execution creation * Remove broken tox-cover job * Update reno for stable/queens * Running new workflow based on existing execution 3.2.0 ----- * Clean up keystone authentication * Restore devstack job for python-mistralclient * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Avoid tox\_install.sh for constraints support * Fix limit handling to not send value of -1 * Remove setting of version/release from releasenotes * Updated from global requirements * Updated from global requirements * Support no\_auth mode in mistral client * Fix several problems in keycloak auth module * Don't create client for help and bash completion * Remove "insecure" in token-based authentication * Updated from global requirements * Fix a typo in env variable name * Add default values for domain related options * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Enable ssl support for keycloak auth module * Support for Project\_domain\_id and user\_domain\_id * Updated from global requirements * Updated from global requirements * Namespaces release note * Create and run a workflow within a namespace * Updated from global requirements * Task list now only queries the displayed fields * Use keystoneauth plugins and session instead of keystoneclient * Fix how "--limit" is passed to the server for action executions * mistral execution-list -f value should be empty * Add test for target parameters and fix requests lib error * Update reno for stable/pike * Updated from global requirements * Make README better 3.1.1 ----- * String interpolation should be delayed * Give better tox output * Apply Pike document structure * Updated from global requirements * Update and optimize documentation links * Enable warning-is-error in doc build * Updated from global requirements * Add CLI for event trigger operations * Set the default value of --limit parameter * Switch from oslosphinx to openstackdocstheme * Updated from global requirements * Enable some off-by-default checks * Make --profile load from environment variables * Updated from global requirements * Updated from global requirements * Replace request mocking in test\_httpclient with requests-mock * Updated from global requirements * Updated from global requirements * Change service type to workflowv2 * Updated from global requirements * Updated from global requirements * Updated from global requirements * Add a missing space to the help message for execution-create * fix release note formatting * Explicitly set 'builders' option * doc: Remove cruft from conf.py * Fix doc generation for python 3 3.1.0 ----- * Updated from global requirements * Optimize the link address * Add release note for region name bugfix * Take DST into account when converting to UTC * Fix region support in mistralclient * Update the gitingore * Remove log translations * Use session from OSC plugin * Accept keystone session in client * Use generic keystone client instead of versioned * Add --utc flag to cron trigger create * Updated from global requirements * Update test requirement * Updated from global requirements * Remove support for py34 * Updated from global requirements * Updated from global requirements * Add project\_id and project\_name options * Read project\_id and project\_name in env * Update reno for stable/ocata * Format the list of auth types in the Mistral client help * Added link for modindex * Fix doc build if git is absent * Changed the README.rst 3.0.0 ----- * Pass target insecure flag to msitral service * Updated from global requirements * Add user and project domain name parameters for target cloud * Update .gitignore * Removes unnecessary utf-8 encoding * Initial commit for mistral-i18n support * Add tests for filters in CLI list commands * Cosmetic changes in CLI tests * Add '--filter' parameter to list commands in CLI * Add filters to client Python APIs * Use assertGreater() or assertLess() * Keystone v3 needs extra parameters * Initial commit for python-mistralclient document * Remove commented-out Apache 2 classifier from setup.cfg * Make python mistralclient readme better * Filter workflow executions by creating task execution id * Updated from global requirements * Fix for failing dsvm gate * Move json.loads() method to utils.py, and use "with" to deal with file objects * Make python mistralclient readme better * Show team and repo badges on README * Updated tox.ini to pick up requirements from upper constraints * Region name related command line arguments are added * Replace uuid4() with generate\_uuid() from oslo\_utils * Removing deprecation warnings to pass py35 * Removed the extra space from tox.ini * Updated from global requirements * Remove unused pylint * Added the reno for stable/mitaka stable/newton and stable/liberty * Add cancelled state to action executions * Remove unused scripts in tools * Updated from global requirements * remove apiclient from mistralclient * remove openstack/common/cliutils.py * Updated from global requirements * Remove unused openstack/common/apiclient/client * Removed openstack/common/importutils.py * Updated from global requirements * Fix python35 job failures * Adding files to .gitignore * Enable DeprecationWarning in test environments * Add Python 3.5 classifier and venv * Updated from global requirements * Fixed errors while generating releasenotes * Replaced savanna word with mistralclient * Updated from global requirements * Fix warning when running tox -e docs * Remove white space between print () in cliutils.py and lintstack.py * Add plug-in summary for osc doc * Updated from global requirements * Fix PEP8 issues and incorrect version/release details * Enable release notes translation * Updated from global requirements * Expose the --run-sync Action Execution parameter on the CLI * Added the # -\*- coding: utf-8 -\*- to the conf.py * Updated from global requirements * Updated from global requirements * Send access info to server * Maintain releasenotes for python-mistralclient * Refactor common parts of client tests * Updated from global requirements * Avoid use xx=[] for parameter to initialize it's value * Trivial: Remove vim header from source files * Remove clashes of openstackclient command in mistral * Abstract authentication function * Add 'created\_at' and 'updated\_at' to action-execution-get and action-execution-list command Closes-bug: 1618767 Change-Id: I422fdcdfa66d6b7a781542c7acc458f8c46edb18 * Added sphinix config to setup.cfg * Updated from global requirements * Add 'created\_at' and 'updated\_at' to task-get command * Pass httpclient to managers * Use requests-mock for testing * Use oslotest instead of testtools/unittest * Fixing auth for keystone v2.0 * Fix wrong error message for environment operation * Make osprofiler dependency really "soft" * Fixing getting mistral\_url from keystone catalog 2.1.1 ----- * Make osprofiler dependency "soft" * Clean imports in code * TrivialFix: Remove logging import unused * Updated from global requirements 2.1.0 ----- * Updated from global requirements * Updated from global requirements * Added ID option to update Action Definition * Add error message when OS\_USERNAME or OS\_PASSWORD not provided * Add target-\* parameters to python-mistralclient * Different formatters for "action-execution-get" and "action-execution-list" * Add "Task ID" field for "action-execution-get" command output * Remove usage of private '\_url' property from OSC * Add cancelled state to executions * Added 'pip install -r requirements.txt' instruction * Updated from global requirements * Fixed ssl options for httpclient * Changed argument names as per other python clients * Change action-get help to get action info by ID * Updated from global requirements * Add KeyCloak OpenID Connect authentication * Updated from global requirements * Use osc\_lib instead of cliff * Use osc-lib instead of openstackclient * Updated from global requirements * Removed use of tempest\_lib and used tempest instead * Updated from global requirements * Remove .mailmap since it's no longer needed * Updated from global requirements * Add osprofiler option to trace operations * Remove AUTHORS file from git tracking * Updated from global requirements * Updated from global requirements * Updated from global requirements * Updated from global requirements * Added CONTRIBUTING.rst file * Validate ad-hoc action via cli * Updated from global requirements * Updated from global requirements * Updated from global requirements * Fix task result syntax in workflows used for functional tests * Updated from global requirements * Fix cacert and insecure options on HTTP requests * Change the mistralclient for Mistral action pack 2.0.0 ----- * Functional tests for workflow sharing * Fix show member error * Updated from global requirements * Support resource sharing CLI * Updated from global requirements * Support workflow id for execution CLI * Updated from global requirements * Support ID for workflow operations in CLI * Support workflow id for cron trigger creation * Add task\_execution\_id to workflow execution in CLI * Updated from global requirements * Updated from global requirements * Fix db error when running python34 unit tests * Add env option to CLI for executions and tasks update * Updated from global requirements * Updated from global requirements * Drop py33 support * Updated from global requirements * Updated from global requirements * Pass environment variables of proxy to tox * Updated from global requirements * Delete python bytecode before every test run * Remove py26 support 1.2.0 ----- * Updated from global requirements * Introduce openstackclient plugin * Show project id when retrieving workflow(s) * Show workflow\_id when getting workflow(s) * Add post\_test\_hook script that will run tests on the gate * Fix all H201 pep8 errors * Adding --insecure flag * Make help message of execution pagination query more readable * Create pagination for the mistral client * Wrapped long lines in the "Tags" column * Truncate 'input' data in the wf/action update command output * Changing attribute of run\_tests script * Added home-page link in setup.cfg file * Fix dict ordering issue in action executions CLI test * Updated from global requirements * Updated from global requirements * Adding correct version info to mistralclient * Fixing dict comparison in task CLI tests * Updated from global requirements * Show exact error message when authentication falied instead of HTML body * Fix support of dev use case with no auth * Add state info to tasks list * Add .mailmap for pbr AUTHORS generation * Update AUTHORS file * Updated from global requirements * Fix order of arguments in assertEqual * Corrected output when env description is not provided * Output format is corrected for wf-validate and wb-validate * Adding pagination support to mistral python client * Added some tests for environment * Default auth\_url is set, if it is not provided * Updated from global requirements 1.1.0 ----- * Fixing test failing on workflow-delete * Updated from global requirements * Provide an ability to make action/workflow public * Removed unused functional test * Fix order of arguments in assertEqual 1.0.2 ----- * Remove invalid test case of mistralclient * Updated from global requirements * Fix failure of run\_action CLI command * Support action\_execution deletion on client side * Fixing dict ordering issue in task tests * Update README.md for project namespace and repo change * Add service API support in client side 1.0.1 ----- * Add CLI for rerunning failed task execution * Add python 3 classifiers * Fix PY3 compatibility * Adding to\_dict() method to Resource class * Changing check of mistral availability in functional tests * User 'alt\_demo' user instead of 'demo' for cli tests * Fixing launching mistralclient tests * Configure logging with debug flag 1.0.0 ----- * Update requirements to unwedge solum * Append newlines to two new test files * Implementing run-action command in client * fix mistral-client use does not use endpoint keystone: * Add description param for execution create/update * Update .gitreview file for project rename * Remove unnecessary validation in update environment form the client * Make create commands cut long output * Mistral bash completion script optimization * Add bash-completion command support * Fix cli integration tests * Update requirements for master branch * Delete bash completion script auto placement * Adding '--params' to cron-trigger CLI * Support resource deletion in batches * Update cli integration test due to last changes * Modified help display format to present a more user-friendly display * Update commands bash completion script * Adding 'Task name' in Action Execution CLI * Fixing state\_info column in CLI * Delete requirement on unneeded arguments 2015.1.0rc1 ----------- * Remove "policies" keyword from test resources * Fixing a type in execution-create command help * Removing v1 stuff (API methods, commands, tests) * Add workbook and workflow validate commands * Updating AUTHORS * Add functional CLI tests for 'action-execution' endpoint * Fixing task CLI * Added tempest-lib to test-requirements.txt * Fixing tasks operations in mistralclient * Add action execution client lib and CLI * Fix failing after refactoring CLI test for execution * Trigger remaining-executions and first-exec-date * Rename execution\_id to workflow\_execution\_id * add bash completion script 2015.1.0b3 ---------- * Refactor task output: "wf\_name" -> "workflow\_name" * Deleting command 'get-task-output' from CLI * Adjust all test examples in mistralclient * Import 'decorators' from tempest\_lib instead of 'test' from tempest * Add support for auth against keystone on https * Fix tempest gate, add tempest import * Add tests which check env isolation * Add CLI tests for environments 2015.1.0b2 ---------- * Extend cli integration tests for update * Add error code to APIException * Fix import in functional tests * Implement commands for execution environment * Fix client initialization in the integration tests 2015.1.0b1 ---------- * Add mistralclient documentation * Add new functional CLI tests * Use YAML text instead of JSON in HTTP body (client side) * Add CLI multi tenancy tests * Fix pep8 issues * Fix execution-update CLI test * Refactor CLI tests * Add CLI tests for actions * Updating AUTHORS file 0.1.1 ----- * Fix CLI cron-trigger tests * Fix passing workflow input via UI * Fix execution state choices and commenting workflow input in triggers * Add CLI tests for triggers * Adding cron triggers * Provide workflow input * Adjust action commands (CLI) * Add script to run functional CLI and client tests locally * Fix 'is\_system' property in action commands * Refactoring CLI commands returning lists * Adding 'tags' to actions * Update requirements according to global requirements (master) 0.1 --- * Fix CLI v2 tests for workflows * Fix CLI v2 test for workbooks and executions * Getting rid of 'name' and 'tags' for workbook create/update * Removing 'name' and 'tags' from action API and CLI * Removing 'name' and 'tags' from workflow API and CLI * Add 'name' to test workbook * Renaming 'parameters' to 'input' * Fix client tests due to changes in code from 'mistral' repository * Add task-get-parameters command * Minor changes in tests files * Fix workflow-update command * Support naive filtering in python API * Fixed execution create method * Add actions API and CLI commands * Add CLI integration tests for v2 * Add hacking to the flake8 tests * Add unit tests on client CLI v2 * Add CLI for client v2 * Add unit tests on v2 client * Implement python-mistralclient v2 * Work toward Python 3.4 support and testing * Move integration tests under mistralclient/tests folder * Add negative tests for CLI * Fix failing 'get\_task' test * Add CLI tests for workbook, execution and task * Add simple tests for Mistral CLI * Remove unneeded definitions of Python Source Code Encoding * Fix typo in base.py * Fix path to integrational tests * Support v2 Keystone API in client * Override configure\_logging to quieten iso8601 and requests * Add integrational tests for executions and tasks * Add integration tests (actions with workbooks) * Update requirements due to global-requirements 0.0.4 ----- * Modify API to make use of /executions endpoint * Removing horizon plugin code base from client * Correct the name that the client is installed as * Remove the log module and all dependencies * Update openstack/common code * Don't use oslo logging in the client * change assertEquals to assertEqual * "target\_task" -> "task" in tests * cleaning up index.rst file * Handle error from upload definition command * Rename Target to Task in execution format * Print status code of requests * Return response as error if not valid json object 0.0.2 ----- * Fixed issues with tarballs 0.0.1 ----- * Replace Task text field with drop-down list * Add list of tasks for an execution * Add authentication to dashboard * Temporarily harbouring Horizon Dashboard * Add upload workbook definition * Add uploading context as a file * Update oslo-incubator dependencies * Add script to allow update dependencies in all envs * Fix context serialization in Execution.create * Modify API tests to check the calls * Fix a bug with empty context * Made keystone authentication optional * Add command-line interface * Add context parameter in execution creation * Rename "target\_task" to "task" * Adding changes related to Data Flow * Fix installation with requirements * Moving Mistral Client code from main Mistral repository * Adding .gitignore file * Initial commit ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/LICENSE0000664000175000017500000002363600000000000016752 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=1725030372.8645468 python-mistralclient-5.3.0/PKG-INFO0000664000175000017500000001134200000000000017031 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: python-mistralclient Version: 5.3.0 Summary: Mistral Client Library Home-page: https://docs.openstack.org/python-mistralclient/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: Apache Software License Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-mistralclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html Mistral ======= .. image:: https://img.shields.io/pypi/v/python-mistralclient.svg :target: https://pypi.org/project/python-mistralclient/ :alt: Latest Version Mistral is a workflow service. Most business processes consist of multiple distinct interconnected steps that need to be executed in a particular order in a distributed environment. A user can describe such a process as a set of tasks and their transitions. After that, it is possible to upload such a description to Mistral, which will take care of state management, correct execution order, parallelism, synchronization and high availability. Mistral also provides flexible task scheduling so that it can run a process according to a specified schedule (for example, every Sunday at 4.00pm) instead of running it immediately. In Mistral terminology such a set of tasks and relations between them is called a workflow. Mistral client ============== Python client for Mistral REST API. Includes python library for Mistral API and Command Line Interface (CLI) library. Installation ------------ First of all, clone the repo and go to the repo directory:: $ git clone https://opendev.org/openstack/python-mistralclient.git $ cd python-mistralclient Then just run:: $ pip install -e . or:: $ pip install -r requirements.txt $ python setup.py install Running Mistral client ---------------------- If Mistral authentication is enabled, provide the information about OpenStack auth to environment variables. Type:: $ export OS_AUTH_URL=http://:5000/v2.0 $ export OS_USERNAME=admin $ export OS_TENANT_NAME=tenant $ export OS_PASSWORD=secret $ export OS_MISTRAL_URL=http://:8989/v2 (optional, by default URL=http://localhost:8989/v2) and in the case that you are authenticating against keystone over https: $ export OS_CACERT= .. note:: In client, we can use both Keystone auth versions - v2.0 and v3. But server supports only v3.* To make sure Mistral client works, type:: $ mistral workbook-list You can see the list of available commands typing:: $ mistral --help Useful Links ============ * `PyPi`_ - package installation * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.org/project/python-mistralclient .. _Launchpad project: https://launchpad.net/python-mistralclient .. _Blueprints: https://blueprints.launchpad.net/python-mistralclient .. _Bugs: https://bugs.launchpad.net/python-mistralclient .. _Source: https://opendev.org/openstack/python-mistralclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/mistral-specs/ .. _Release Notes: https://docs.openstack.org/releasenotes/python-mistralclient Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Requires-Python: >=3.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/README.rst0000664000175000017500000000611700000000000017427 0ustar00zuulzuul00000000000000======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-mistralclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html Mistral ======= .. image:: https://img.shields.io/pypi/v/python-mistralclient.svg :target: https://pypi.org/project/python-mistralclient/ :alt: Latest Version Mistral is a workflow service. Most business processes consist of multiple distinct interconnected steps that need to be executed in a particular order in a distributed environment. A user can describe such a process as a set of tasks and their transitions. After that, it is possible to upload such a description to Mistral, which will take care of state management, correct execution order, parallelism, synchronization and high availability. Mistral also provides flexible task scheduling so that it can run a process according to a specified schedule (for example, every Sunday at 4.00pm) instead of running it immediately. In Mistral terminology such a set of tasks and relations between them is called a workflow. Mistral client ============== Python client for Mistral REST API. Includes python library for Mistral API and Command Line Interface (CLI) library. Installation ------------ First of all, clone the repo and go to the repo directory:: $ git clone https://opendev.org/openstack/python-mistralclient.git $ cd python-mistralclient Then just run:: $ pip install -e . or:: $ pip install -r requirements.txt $ python setup.py install Running Mistral client ---------------------- If Mistral authentication is enabled, provide the information about OpenStack auth to environment variables. Type:: $ export OS_AUTH_URL=http://:5000/v2.0 $ export OS_USERNAME=admin $ export OS_TENANT_NAME=tenant $ export OS_PASSWORD=secret $ export OS_MISTRAL_URL=http://:8989/v2 (optional, by default URL=http://localhost:8989/v2) and in the case that you are authenticating against keystone over https: $ export OS_CACERT= .. note:: In client, we can use both Keystone auth versions - v2.0 and v3. But server supports only v3.* To make sure Mistral client works, type:: $ mistral workbook-list You can see the list of available commands typing:: $ mistral --help Useful Links ============ * `PyPi`_ - package installation * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.org/project/python-mistralclient .. _Launchpad project: https://launchpad.net/python-mistralclient .. _Blueprints: https://blueprints.launchpad.net/python-mistralclient .. _Bugs: https://bugs.launchpad.net/python-mistralclient .. _Source: https://opendev.org/openstack/python-mistralclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/mistral-specs/ .. _Release Notes: https://docs.openstack.org/releasenotes/python-mistralclient ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8405466 python-mistralclient-5.3.0/doc/0000775000175000017500000000000000000000000016500 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/doc/requirements.txt0000664000175000017500000000052100000000000021762 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 reno>=3.1.0 # Apache-2.0 openstackdocstheme>=2.2.1 # Apache-2.0 sphinxcontrib-apidoc>=0.2.0 # BSD ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8405466 python-mistralclient-5.3.0/doc/source/0000775000175000017500000000000000000000000020000 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/doc/source/class_reference.rst0000664000175000017500000000017400000000000023657 0ustar00zuulzuul00000000000000Python Mistral bindings class refrence ====================================== .. toctree:: :maxdepth: 1 api/modules ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8445468 python-mistralclient-5.3.0/doc/source/cli/0000775000175000017500000000000000000000000020547 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/doc/source/cli/cli_usage_source_execution.rst0000664000175000017500000000506000000000000026700 0ustar00zuulzuul00000000000000Replicating Workflows with Mistral ================================== The new command line switch '-s' will allow the operator to replicate / clone an existing workflow execution based on its ID. Once id is given mistral will create a new workflow execution based on the parameters of the first, which will provide a simple approach to spawning a number of workflow executions without having to specify inputs or parameters. Otherwise you can override some of the parameters (e.g. some of the input variables) Basic Usage ----------- From the command line the operator will issue the following. The first step would be to list the current executions, which is done with "execution-list". The following step is to take the listed execution id and pass it to the source execution switch "-s". .. code-block:: shell mistral execution-list mistral execution-create -s Once the workflow execution is selected and the replicate command used you should see a newly created workflow execution based on an existing one with a new execution id. .. code-block:: shell mistral execution-create -s 123e4567-e89b-12d3-a456-426655440000 +--------------------+---------------------------------------+ | Field | Value | +--------------------+---------------------------------------+ | ID | 123e4567-e89b-12d3-a456-77046883182e | | | | | Workflow ID | 123e4567-e89b-12d3-a456-45411dfa33af | | | | | Workflow name | some.workflow.name.goes.here | | | | | Workflow namespace | | | | | | Description | | | | | | Task Execution ID | | | | | | State | RUNNING | | | | | State info | None | | | | | Created at | 2018-01-25 18:41:07 | | | | | Updated at | 2018-01-25 18:41:07 | +--------------------+---------------------------------------+ ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/doc/source/cli/cli_usage_targeting_workflows.rst0000664000175000017500000000405700000000000027423 0ustar00zuulzuul00000000000000Using Mistral to execute Workflows on an arbitrary cloud ======================================================== It is possible to execute a workflow on any arbitrary cloud without additional configuration on the Mistral server side. It is possible to have Mistral use an external OpenStack cloud even when it isn't deployed in an OpenStack environment (i.e. no Keystone integration). This setup is particularly useful when Mistral is used in standalone mode, where the Mistral service is not part of the OpenStack cloud and runs separately. To enable this operation, the user can use ``--os-target-username``, ``--os-target-password``, ``--os-target-tenant-id``, ``--os-target-tenant-name``, ``--os-target-auth-token``, ``--os-target-auth-url``, ``--os-target_cacert``, and ``--os-target-region-name`` parameters. For example, the user can return the heat stack list with this setup as shown below: .. code-block:: shell $ mistral \ --os-target-auth-url=http://keystone2.example.com:5000/v3 \ --os-target-username=testuser \ --os-target-tenant=testtenant \ --os-target-password="MistralRuleZ" \ --os-mistral-url=http://mistral.example.com:8989/v2 \ run-action heat.stacks_list The OS-TARGET-* parameters can be set in environment variables as: .. code-block:: shell $ export OS_TARGET_AUTH_URL=http://keystone2.example.com:5000/v3 $ export OS_TARGET_USERNAME=admin $ export OS_TARGET_TENANT_NAME=tenant $ export OS_TARGET_PASSWORD=secret $ export OS_TARGET_REGION_NAME=region Note on the --os-target_cacert parameter ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ The `--os-target_cacert` parameter can be used to set a CA certificate for SSL communication with the target cloud's Keystone service. The CA certificate file is **NOT** transferred to the Mistral server. It is the responsibility of the user to ensure that the SSL Certificate is accessible for the Mistral Executor and SSL communication is possible with the target cloud. For testing purposes it is suggested to use the `--target_insecure` parameter. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/doc/source/cli/cli_usage_with_keycloak.rst0000664000175000017500000000010600000000000026146 0ustar00zuulzuul00000000000000Using Mistral with KeyCloak Server ================================== ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/doc/source/cli/cli_usage_with_openstack.rst0000664000175000017500000000233000000000000026334 0ustar00zuulzuul00000000000000Using Mistral with OpenStack ============================ The **mistral** shell utility interacts with OpenStack Mistral API from the command-line. It supports the features in the OpenStack Mistral API. Basic Usage ----------- In order to use the CLI, you must provide your OpenStack credentials (for both user and project), and auth endpoint. Use the corresponding configuration options (``--os-username``, ``--os-password``, ``--os-project-name``, ``--os-user-domain-id``, ``os-project-domain-id``, and ``--os-auth-url``), but it is easier to set them in environment variables. .. code-block:: shell $ export OS_AUTH_URL=http://:5000/v2.0 $ export OS_USERNAME=admin $ export OS_TENANT_NAME=tenant $ export OS_PASSWORD=secret $ export OS_MISTRAL_URL=http://:8989/v2 When authenticating against keystone over https: .. code-block:: shell $ export OS_CACERT= Once you've configured your authentication parameters, you can run **mistral** commands. All commands take the form of:: mistral [arguments...] Run **mistral --help** to get a full list of all possible commands, and run **mistral help ** to get detailed help for that command. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/doc/source/conf.py0000664000175000017500000000636600000000000021312 0ustar00zuulzuul00000000000000# Mistral documentation build configuration file import os import sys on_rtd = os.environ.get('READTHEDOCS', None) == 'True' # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, os.path.abspath('../../')) sys.path.insert(0, os.path.abspath('../')) sys.path.insert(0, os.path.abspath('./')) # -- General configuration --------------------------------------------------- # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'sphinxcontrib.apidoc', 'openstackdocstheme', ] # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # sphinxcontrib.apidoc options apidoc_module_dir = '../../mistralclient' apidoc_output_dir = 'api' apidoc_excluded_paths = [ 'test', 'tests/*'] apidoc_separate_modules = True # The suffix of source filenames. source_suffix = '.rst' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Mistral Client' copyright = '2016, Mistral Contributors' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # If true, '()' will be appended to :func: etc. cross-reference text. add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. modindex_common_prefix = ['mistralclient.'] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". html_title = 'MistralClient' # Custom sidebar templates, maps document names to template names. html_sidebars = { 'index': [ 'sidebarlinks.html', 'localtoc.html', 'searchbox.html', 'sourcelink.html' ], '**': [ 'localtoc.html', 'relations.html', 'searchbox.html', 'sourcelink.html' ] } # Output file base name for HTML help builder. htmlhelp_basename = 'Mistraldoc' # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'mistral_client', 'Mistral Client Documentation', ['Mistral Contributors'], 1) ] # -- Options for openstackdocstheme ------------------------------------------- openstackdocs_repo_name = 'openstack/python-mistralclient' openstackdocs_bug_project = 'python-mistralclient' openstackdocs_bug_tag = '' openstackdocs_auto_name = False ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8445468 python-mistralclient-5.3.0/doc/source/contributor/0000775000175000017500000000000000000000000022352 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/doc/source/contributor/contributing.rst0000664000175000017500000000364200000000000025620 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-mistralclient. Communication ~~~~~~~~~~~~~ * IRC channel #openstack-mistralclient at OFTC * Mailing list (prefix subjects with ``[mistral]`` for faster responses) http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss Contacting the Core Team ~~~~~~~~~~~~~~~~~~~~~~~~ Please refer the `python-mistralclient Core Team `_ contacts. New Feature Planning ~~~~~~~~~~~~~~~~~~~~ python-mistralclient 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-mistralclient project require one or two +2 votes from python-mistralclient 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=1725030339.0 python-mistralclient-5.3.0/doc/source/index.rst0000664000175000017500000000201700000000000021641 0ustar00zuulzuul00000000000000Python bindings to the OpenStack Workflow API ============================================= This is a client for OpenStack Mistral API. There's a Python API (the :mod:`mistralclient` module), and a command-line script (installed as :program:`mistral`). Using mistralclient ------------------- .. toctree:: :maxdepth: 2 cli/cli_usage_with_openstack cli/cli_usage_with_keycloak cli/cli_usage_targeting_workflows cli/cli_usage_source_execution class_reference For information about using the mistral command-line client, see `Workflow service command-line client`_. .. _Workflow service command-line client: https://docs.openstack.org/mistral/latest/cli/index.html Python API Reference -------------------- * `REST API Specification`_ .. _REST API Specification: https://docs.openstack.org/mistral/latest/api/v2.html For Contributors ---------------- * If you are a new contributor to python-mistralclient please refer: :doc:`contributor/contributing` .. toctree:: :hidden: contributor/contributing ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functional_creds.conf.sample0000664000175000017500000000026500000000000023407 0ustar00zuulzuul00000000000000# Credentials for functional testing [auth] uri = http://10.42.0.50:5000/v2.0 [admin] user = admin tenant = admin pass = secrete [demo] user = demo tenant = demo pass = demo_secret././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8365467 python-mistralclient-5.3.0/functionaltests/0000775000175000017500000000000000000000000021160 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8365467 python-mistralclient-5.3.0/functionaltests/resources/0000775000175000017500000000000000000000000023172 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8445468 python-mistralclient-5.3.0/functionaltests/resources/v2/0000775000175000017500000000000000000000000023521 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/action_v2.yaml0000664000175000017500000000044400000000000026273 0ustar00zuulzuul00000000000000--- version: "2.0" greeting: description: "This action says 'Hello'" base: std.echo base-input: output: 'Hello, <% $.name %>' input: - name output: string: <% $.output %> farewell: base: std.echo base-input: output: 'Bye!' output: info: <% $.output %> ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/action_v2_tags.yaml0000664000175000017500000000032700000000000027311 0ustar00zuulzuul00000000000000--- version: "2.0" greeting: description: "This action says 'Hello'" tags: [tag, tag1] base: std.echo base-input: output: 'Hello, <% $.name %>' input: - name output: string: <% $.output %> ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/async.yaml0000664000175000017500000000014300000000000025520 0ustar00zuulzuul00000000000000--- version: '2.0' async_wf: type: direct tasks: async_task: action: std.async_noop ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8445468 python-mistralclient-5.3.0/functionaltests/resources/v2/for_namespaces/0000775000175000017500000000000000000000000026506 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/for_namespaces/lowest_level_wf.yaml0000664000175000017500000000013700000000000032573 0ustar00zuulzuul00000000000000--- version: '2.0' lowest_level_wf: type: direct tasks: hello: action: std.noop ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/for_namespaces/middle_wf.yaml0000664000175000017500000000014200000000000031321 0ustar00zuulzuul00000000000000--- version: '2.0' middle_wf: type: direct tasks: hello: workflow: lowest_level_wf ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/for_namespaces/top_level_wf.yaml0000664000175000017500000000013700000000000032060 0ustar00zuulzuul00000000000000--- version: '2.0' top_level_wf: type: direct tasks: hello: workflow: middle_wf ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/wb_v2.yaml0000664000175000017500000000045100000000000025424 0ustar00zuulzuul00000000000000--- version: '2.0' name: wb actions: ac1: input: - str1 - str2 base: std.echo output="<% $.str1 %><% $.str2 %>" workflows: wf1: type: direct tasks: hello: action: std.echo output="Hello" publish: result: <% task(hello).result %> ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/wb_with_tags_v2.yaml0000664000175000017500000000031200000000000027471 0ustar00zuulzuul00000000000000--- version: '2.0' name: wb tags: [tag] workflows: wf1: type: direct tasks: hello: action: std.echo output="Hello" publish: result: <% task(hello).result %> ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/wf_delay_v2.yaml0000664000175000017500000000051000000000000026602 0ustar00zuulzuul00000000000000--- version: '2.0' wf: type: direct tags: [tag] tasks: hello: action: std.echo output="Hello" publish: result: <% task(hello).result %> wait-after: 1 on-success: - task2 task2: action: std.echo output="Task 2" publish: task2: <% task(task2).result %> ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/wf_single_v2.yaml0000664000175000017500000000024100000000000026766 0ustar00zuulzuul00000000000000--- version: '2.0' wf_single: type: direct tasks: hello: action: std.echo output="Hello" publish: result: <% task(hello).result %> ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/wf_v2.yaml0000664000175000017500000000112100000000000025423 0ustar00zuulzuul00000000000000--- version: '2.0' wf: type: direct tasks: hello: action: std.echo output="Hello" wait-before: 1 publish: result: <% task().result %> on-success: bye bye: action: std.echo output="Bye" publish: result: <% $.result + ', ' + task().result %> wf1: type: reverse tags: [tag] input: - farewell tasks: addressee: action: std.echo output="John" publish: name: <% task(addressee).result %> goodbye: action: std.echo output="<% $.farewell %>, <% $.name %>" requires: [addressee] ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/functionaltests/resources/v2/wf_wrapping_wf_v2.yaml0000664000175000017500000000012700000000000030033 0ustar00zuulzuul00000000000000--- version: '2.0' wrapping_wf: type: direct tasks: hello: workflow: wf ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/lower-constraints.txt0000664000175000017500000000355300000000000022177 0ustar00zuulzuul00000000000000alabaster==0.7.10 amqp==2.1.1 appdirs==1.3.0 asn1crypto==0.23.0 Babel==2.3.4 cachetools==2.0.0 cffi==1.14.0 cliff==2.8.0 cmd2==0.8.0 contextlib2==0.4.0 coverage==4.0 cryptography==2.7 debtcollector==1.2.0 decorator==3.4.0 deprecation==1.0 docutils==0.11 dogpile.cache==0.6.2 dulwich==0.15.0 eventlet==0.18.2 extras==1.0.0 fasteners==0.7.0 fixtures==3.0.0 future==0.16.0 futurist==1.2.0 greenlet==0.4.15 idna==2.6 imagesize==0.7.1 iso8601==0.1.11 Jinja2==2.10 jmespath==0.9.0 jsonpatch==1.16 jsonpointer==1.13 jsonschema==2.6.0 keystoneauth1==3.4.0 kombu==4.0.0 linecache2==1.0.0 MarkupSafe==1.1.1 monotonic==0.6 mox3==0.20.0 msgpack-python==0.4.0 munch==2.1.0 netaddr==0.7.18 netifaces==0.10.4 nose==1.3.7 openstacksdk==0.11.2 os-client-config==1.28.0 os-service-types==1.2.0 osc-lib==1.8.0 oslo.concurrency==3.25.0 oslo.config==5.2.0 oslo.context==2.19.2 oslo.i18n==3.15.3 oslo.log==3.36.0 oslo.messaging==5.29.0 oslo.middleware==3.31.0 oslo.serialization==2.18.0 oslo.service==1.24.0 oslo.utils==3.33.0 oslotest==3.2.0 osprofiler==1.4.0 paramiko==2.0.0 Paste==2.0.2 PasteDeploy==1.5.0 pbr==2.0.0 pep8==1.5.7 pika-pool==0.1.3 pika==0.10.0 positional==1.2.1 prettytable==0.7.2 pyasn1==0.1.8 pycparser==2.18 Pygments==2.2.0 pyinotify==0.9.6 pyOpenSSL==17.1.0 pyparsing==2.1.0 pyperclip==1.5.27 python-cinderclient==3.3.0 python-dateutil==2.5.3 python-glanceclient==2.8.0 python-keystoneclient==3.8.0 python-mimeparse==1.6.0 python-novaclient==9.1.0 python-openstackclient==3.12.0 python-subunit==1.0.0 pytz==2013.6 PyYAML==3.13 repoze.lru==0.7 requests-mock==1.2.0 requests==2.14.2 requestsexceptions==1.2.0 rfc3986==0.3.1 Routes==2.3.1 simplejson==3.5.1 snowballstemmer==1.2.1 statsd==3.2.1 stestr==2.0.0 stevedore==1.20.0 tempest==17.1.0 tenacity==3.2.1 testrepository==0.0.18 testtools==2.2.0 traceback2==1.4.0 unittest2==1.1.0 urllib3==1.21.1 vine==1.1.4 warlock==1.2.0 WebOb==1.7.1 wrapt==1.7.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8445468 python-mistralclient-5.3.0/mistralclient/0000775000175000017500000000000000000000000020605 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/__init__.py0000664000175000017500000000125600000000000022722 0ustar00zuulzuul00000000000000# Copyright 2014 Rackspace Hosting # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. import pbr.version __version__ = pbr.version.VersionInfo( 'python-mistralclient').version_string() ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8445468 python-mistralclient-5.3.0/mistralclient/api/0000775000175000017500000000000000000000000021356 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/__init__.py0000664000175000017500000000000000000000000023455 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/base.py0000664000175000017500000001776200000000000022657 0ustar00zuulzuul00000000000000# Copyright 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 copy import urllib from oslo_serialization import jsonutils from keystoneauth1 import exceptions from mistralclient import utils urlparse = urllib.parse class Resource(object): resource_name = 'Something' defaults = {} def __init__(self, manager, data): self.manager = manager self._data = data self._set_defaults() self._set_attributes() def _set_defaults(self): for k, v in self.defaults.items(): if k not in self._data: self._data[k] = v def _set_attributes(self): for k, v in self._data.items(): try: setattr(self, k, v) except AttributeError: # In this case we already defined the attribute on the class pass def to_dict(self): return copy.deepcopy(self._data) def __str__(self): values = ", ".join( ["%s='%s'" % (n, v) for n, v in self._data.items()] ) return "%s [%s]" % (self.resource_name, values) def _check_items(obj, searches): try: return all(getattr(obj, attr) == value for (attr, value) in searches) except AttributeError: return False def extract_json(response, response_key): if response_key is not None: return get_json(response)[response_key] else: return get_json(response) class ResourceManager(object): resource_class = None def __init__(self, http_client, enforce_raw_definitions=False): self.http_client = http_client self.enforce_raw_definitions = enforce_raw_definitions def get_contents_if_file(self, contents_or_file_name): if self.enforce_raw_definitions: return contents_or_file_name else: return utils.get_contents_if_file(contents_or_file_name) def find(self, **kwargs): return [i for i in self.list() if _check_items(i, kwargs.items())] def list(self): """This is an abstract method This is added here so that the find method gains some clarity. It must be implemented by the child class in order to find to work """ raise NotImplementedError("abstract method list must be implemented") @staticmethod def _build_query_params(marker=None, limit=None, sort_keys=None, sort_dirs=None, fields=None, filters=None, scope=None, namespace=None): qparams = {} if marker: qparams['marker'] = marker if limit and limit > 0: qparams['limit'] = limit if sort_keys: qparams['sort_keys'] = sort_keys if sort_dirs: qparams['sort_dirs'] = sort_dirs if fields: qparams['fields'] = ",".join(fields) if filters: for name, val in filters.items(): qparams[name] = val if scope: qparams['scope'] = scope if namespace: qparams['namespace'] = namespace return ("?%s" % urlparse.urlencode(list(qparams.items())) if qparams else "") def _ensure_not_empty(self, **kwargs): for name, value in kwargs.items(): if value is None or (isinstance(value, str) and len(value) == 0): raise APIException( 400, '%s is missing field "%s"' % (self.resource_class.__name__, name) ) def _validate(self, url, data, response_key=None, dump_json=True, headers=None, is_iter_resp=False): return self._create( url, data, response_key, dump_json, headers, is_iter_resp, resp_status_ok=200, as_class=False ) def _create(self, url, data, response_key=None, dump_json=True, headers=None, is_iter_resp=False, resp_status_ok=201, as_class=True): if dump_json: data = jsonutils.dumps(data) try: resp = self.http_client.post(url, data, headers) except exceptions.HttpError as ex: self._raise_api_exception(ex.response) if resp.status_code != resp_status_ok: self._raise_api_exception(resp) resource = extract_json(resp, response_key) if is_iter_resp: return [ self.resource_class(self, resource_data) for resource_data in resource ] return self.resource_class(self, resource) if as_class else resource def _update(self, url, data, response_key=None, dump_json=True, headers=None, is_iter_resp=False): if dump_json: data = jsonutils.dumps(data) try: resp = self.http_client.put(url, data, headers) except exceptions.HttpError as ex: self._raise_api_exception(ex.response) if resp.status_code != 200: self._raise_api_exception(resp) resource = extract_json(resp, response_key) if is_iter_resp: return [ self.resource_class(self, resource_data) for resource_data in resource ] return self.resource_class(self, resource) def _list(self, url, response_key=None, headers=None, returned_res_cls=None): try: resp = self.http_client.get(url, headers) except exceptions.HttpError as ex: self._raise_api_exception(ex.response) if resp.status_code != 200: self._raise_api_exception(resp) resource_class = returned_res_cls or self.resource_class return [ resource_class(self, resource_data) for resource_data in extract_json(resp, response_key) ] def _get(self, url, response_key=None, headers=None): try: resp = self.http_client.get(url, headers) except exceptions.HttpError as ex: self._raise_api_exception(ex.response) if resp.status_code == 200: return self.resource_class(self, extract_json(resp, response_key)) else: self._raise_api_exception(resp) def _delete(self, url, headers=None): try: resp = self.http_client.delete(url, headers) except exceptions.HttpError as ex: self._raise_api_exception(ex.response) if resp.status_code != 204: self._raise_api_exception(resp) @staticmethod def _raise_api_exception(resp): try: error_data = ( resp.headers.get("Server-Error-Message", None) or get_json(resp).get("faultstring") ) except ValueError: error_data = resp.content raise APIException( error_code=resp.status_code, error_message=error_data ) def get_json(response): """Gets JSON representation of response. This method provided backward compatibility with old versions of requests library. """ json_field_or_function = getattr(response, 'json', None) if callable(json_field_or_function): return response.json() else: return jsonutils.loads(response.content) class APIException(Exception): def __init__(self, error_code=None, error_message=None): super(APIException, self).__init__(error_message) self.error_code = error_code self.error_message = error_message ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/client.py0000664000175000017500000000165100000000000023211 0ustar00zuulzuul00000000000000# Copyright 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 mistralclient.api.v2 import client as client_v2 def client(auth_type='keystone', **kwargs): return client_v2.Client(auth_type=auth_type, **kwargs) def determine_client_version(mistral_version): if mistral_version.find("v2") != -1: return 2 raise RuntimeError("Cannot determine mistral API version") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/httpclient.py0000664000175000017500000001517600000000000024120 0ustar00zuulzuul00000000000000# Copyright 2013 - Mirantis, Inc. # Copyright 2016 - StackStorm, 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 base64 import copy import logging import os from oslo_utils import importutils import requests AUTH_TOKEN = 'auth_token' SESSION = 'session' CACERT = 'cacert' CERT_FILE = 'cert' CERT_KEY = 'key' INSECURE = 'insecure' PROJECT_ID = 'project_id' USER_ID = 'user_id' REGION_NAME = 'region_name' TARGET_AUTH_TOKEN = 'target_auth_token' TARGET_SESSION = 'target_session' TARGET_AUTH_URI = 'target_auth_url' TARGET_PROJECT_ID = 'target_project_id' TARGET_USER_ID = 'target_user_id' TARGET_INSECURE = 'target_insecure' TARGET_SERVICE_CATALOG = 'target_service_catalog' TARGET_REGION_NAME = 'target_region_name' TARGET_USER_DOMAIN_NAME = 'target_user_domain_name' TARGET_PROJECT_DOMAIN_NAME = 'target_project_domain_name' osprofiler_web = importutils.try_import("osprofiler.web") LOG = logging.getLogger(__name__) def log_request(func): def decorator(self, *args, **kwargs): resp = func(self, *args, **kwargs) LOG.debug("HTTP %s %s %d", resp.request.method, resp.url, resp.status_code) return resp return decorator class HTTPClient(object): def __init__(self, base_url, **kwargs): self.base_url = base_url self.session = kwargs.get('session') if not self.session: self.session = requests.Session() self.auth_token = kwargs.get(AUTH_TOKEN) self.project_id = kwargs.get(PROJECT_ID) self.user_id = kwargs.get(USER_ID) self.cacert = kwargs.get(CACERT) self.insecure = kwargs.get(INSECURE, False) self.region_name = kwargs.get(REGION_NAME) self.ssl_options = {} self.target_session = kwargs.get(TARGET_SESSION) self.target_auth_token = kwargs.get(TARGET_AUTH_TOKEN) self.target_auth_uri = kwargs.get(TARGET_AUTH_URI) self.target_user_id = kwargs.get(TARGET_USER_ID) self.target_project_id = kwargs.get(TARGET_PROJECT_ID) self.target_service_catalog = kwargs.get(TARGET_SERVICE_CATALOG) self.target_region_name = kwargs.get(TARGET_REGION_NAME) self.target_insecure = kwargs.get(TARGET_INSECURE) self.target_user_domain_name = kwargs.get(TARGET_USER_DOMAIN_NAME) self.target_project_domain_name = kwargs.get( TARGET_PROJECT_DOMAIN_NAME ) if self.base_url.startswith('https'): if self.cacert and not os.path.exists(self.cacert): raise ValueError('Unable to locate cacert file ' 'at %s.' % self.cacert) if self.cacert and self.insecure: LOG.warning('Client is set to not verify even though ' 'cacert is provided.') if self.insecure: self.ssl_options['verify'] = False else: if self.cacert: self.ssl_options['verify'] = self.cacert else: self.ssl_options['verify'] = True self.ssl_options['cert'] = ( kwargs.get(CERT_FILE), kwargs.get(CERT_KEY) ) @log_request def get(self, url, headers=None): options = self._get_request_options('get', headers) return self.session.get(self.base_url + url, **options) @log_request def post(self, url, body, headers=None): options = self._get_request_options('post', headers) return self.session.post(self.base_url + url, data=body, **options) @log_request def put(self, url, body, headers=None): options = self._get_request_options('put', headers) return self.session.put(self.base_url + url, data=body, **options) @log_request def delete(self, url, headers=None): options = self._get_request_options('delete', headers) return self.session.delete(self.base_url + url, **options) def _get_request_options(self, method, headers): headers = self._update_headers(headers) if method in ['post', 'put']: content_type = headers.get('content-type', 'application/json') headers['content-type'] = content_type options = copy.deepcopy(self.ssl_options) options['headers'] = headers return options def _update_headers(self, headers): if not headers: headers = {} if isinstance(self.session, requests.Session): if self.auth_token: headers['X-Auth-Token'] = self.auth_token if self.project_id: headers['X-Project-Id'] = self.project_id if self.user_id: headers['X-User-Id'] = self.user_id if self.region_name: headers['X-Region-Name'] = self.region_name if self.target_auth_token: headers['X-Target-Auth-Token'] = self.target_auth_token if self.target_auth_uri: headers['X-Target-Auth-Uri'] = self.target_auth_uri if self.target_project_id: headers['X-Target-Project-Id'] = self.target_project_id if self.target_user_id: headers['X-Target-User-Id'] = self.target_user_id if self.target_insecure: # Note(akovi): due to changes in requests, this parameter # must be a string. Basically, it is a truthy value on # the server side. headers['X-Target-Insecure'] = str(self.target_insecure) if self.target_region_name: headers['X-Target-Region-Name'] = self.target_region_name if self.target_user_domain_name: headers['X-Target-User-Domain-Name'] = self.target_user_domain_name if self.target_project_domain_name: h_name = 'X-Target-Project-Domain-Name' headers[h_name] = self.target_project_domain_name if self.target_service_catalog: headers['X-Target-Service-Catalog'] = base64.b64encode( self.target_service_catalog.encode('utf-8') ) if osprofiler_web: # Add headers for osprofiler. headers.update(osprofiler_web.get_trace_id_headers()) return headers ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8365467 python-mistralclient-5.3.0/mistralclient/api/releasenotes/0000775000175000017500000000000000000000000024047 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8485467 python-mistralclient-5.3.0/mistralclient/api/releasenotes/notes/0000775000175000017500000000000000000000000025177 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000021400000000000011452 xustar0000000000000000118 path=python-mistralclient-5.3.0/mistralclient/api/releasenotes/notes/fix-cli-error-messages-2f59329557a5734f.yaml 22 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/releasenotes/notes/fix-cli-error-messages-2f59329557a570000664000175000017500000000047400000000000033115 0ustar00zuulzuul00000000000000--- fixes: - | Some cli mistral calls when failed would return unhelpful messages like error(400) that is not what the mistral api is returning. This changes those messages to the useful messages sent by the api. The affected are the run-action, the crud for actions, workflows, and workbooks. ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8485467 python-mistralclient-5.3.0/mistralclient/api/v2/0000775000175000017500000000000000000000000021705 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/__init__.py0000664000175000017500000000000000000000000024004 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/action_executions.py0000664000175000017500000000515100000000000026004 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 from mistralclient.api import base class ActionExecution(base.Resource): resource_name = 'ActionExecution' class ActionExecutionManager(base.ResourceManager): resource_class = ActionExecution def create(self, name, input=None, namespace='', **params): self._ensure_not_empty(name=name) data = {'name': name} if input: data['input'] = jsonutils.dumps(input) if params: data['params'] = jsonutils.dumps(params) if namespace: data['workflow_namespace'] = namespace return self._create( '/action_executions', data, dump_json=True ) def update(self, id, state=None, output=None): self._ensure_not_empty(id=id) if not (state or output): raise base.APIException( 400, "Please provide either state or output for action execution." ) data = {} if state: data['state'] = state if output: data['output'] = output return self._update('/action_executions/%s' % id, data) def list(self, task_execution_id=None, limit=None, marker='', fields=None, sort_keys='', sort_dirs='', **filters): url = '/action_executions' if task_execution_id: url = '/tasks/%s/action_executions' % task_execution_id url += "%s" query_string = self._build_query_params( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, fields=fields, filters=filters ) return self._list(url % query_string, response_key='action_executions') def get(self, id): self._ensure_not_empty(id=id) return self._get('/action_executions/%s' % id) def delete(self, id): self._ensure_not_empty(id=id) self._delete('/action_executions/%s' % id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/actions.py0000664000175000017500000000653100000000000023724 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 mistralclient.api import base class Action(base.Resource): resource_name = 'Action' class ActionManager(base.ResourceManager): resource_class = Action def create(self, definition, scope='private', namespace=''): self._ensure_not_empty(definition=definition) # If the specified definition is actually a file, read in the # definition file definition = self.get_contents_if_file(definition) url = '/actions?scope=%s' % scope if namespace: url += '&namespace=%s' % namespace return self._create( url, definition, response_key='actions', dump_json=False, headers={'content-type': 'text/plain'}, is_iter_resp=True ) def update(self, definition, scope='private', id=None, namespace=''): self._ensure_not_empty(definition=definition) params = '?scope=%s' % scope if namespace: params += '&namespace=%s' % namespace url = ('/actions/%s' % id if id else '/actions') + params # If the specified definition is actually a file, read in the # definition file definition = self.get_contents_if_file(definition) return self._update( url, definition, response_key='actions', dump_json=False, headers={'content-type': 'text/plain'}, is_iter_resp=True ) def list(self, marker='', limit=None, sort_keys='', sort_dirs='', fields='', namespace='', **filters): if namespace: filters['namespace'] = namespace query_string = self._build_query_params( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, fields=fields, filters=filters ) return self._list( '/actions%s' % query_string, response_key='actions', ) def get(self, identifier, namespace=''): self._ensure_not_empty(identifier=identifier) return self._get('/actions/%s/%s' % (identifier, namespace)) def delete(self, identifier, namespace=''): self._ensure_not_empty(identifier=identifier) self._delete('/actions/%s/%s' % (identifier, namespace)) def validate(self, definition): self._ensure_not_empty(definition=definition) # If the specified definition is actually a file, read in the # definition file definition = self.get_contents_if_file(definition) return self._validate( '/actions/validate', definition, dump_json=False, headers={'content-type': 'text/plain'} ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/client.py0000664000175000017500000000743400000000000023545 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, 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 copy from oslo_utils import importutils from mistralclient.api import httpclient from mistralclient.api.v2 import action_executions from mistralclient.api.v2 import actions from mistralclient.api.v2 import code_sources from mistralclient.api.v2 import cron_triggers from mistralclient.api.v2 import dynamic_actions from mistralclient.api.v2 import environments from mistralclient.api.v2 import event_triggers from mistralclient.api.v2 import executions from mistralclient.api.v2 import members from mistralclient.api.v2 import services from mistralclient.api.v2 import tasks from mistralclient.api.v2 import workbooks from mistralclient.api.v2 import workflows from mistralclient import auth osprofiler_profiler = importutils.try_import("osprofiler.profiler") _DEFAULT_MISTRAL_URL = "http://localhost:8989/v2" class Client(object): MANAGERS = { 'workbooks': workbooks.WorkbookManager, 'executions': executions.ExecutionManager, 'tasks': tasks.TaskManager, 'actions': actions.ActionManager, 'workflows': workflows.WorkflowManager, 'cron_triggers': cron_triggers.CronTriggerManager, 'event_triggers': event_triggers.EventTriggerManager, 'environments': environments.EnvironmentManager, 'action_executions': action_executions.ActionExecutionManager, 'services': services.ServiceManager, 'members': members.MemberManager, 'code_sources': code_sources.CodeSourceManager, 'dynamic_actions': dynamic_actions.DynamicActionManager, } def __init__(self, auth_type='keystone', **kwargs): # We get the session at this point, as some instances of session # objects might have mutexes that can't be deep-copied. session = kwargs.pop('session', None) enforce_raw_definitions = kwargs.pop('enforce_raw_definitions', False) req = copy.deepcopy(kwargs) mistral_url = req.get('mistral_url') profile = req.get('profile') if mistral_url and not isinstance(mistral_url, str): raise RuntimeError('Mistral url should be a string.') # If auth url was provided then we perform an authentication, otherwise # just ignore this step if req.get('auth_url') or req.get('target_auth_url'): auth_handler = auth.get_auth_handler(auth_type) auth_response = auth_handler.authenticate(req, session=session) else: auth_response = {} if session is None: # If the session was None and we're using keystone auth, it will be # created by the auth_handler. session = auth_response.pop('session', None) req.update(auth_response) mistral_url = auth_response.get('mistral_url') or mistral_url if not mistral_url: mistral_url = _DEFAULT_MISTRAL_URL if profile: osprofiler_profiler.init(profile) http_client = httpclient.HTTPClient(mistral_url, session=session, **req) # Create all resource managers. for name, cls in self.MANAGERS.items(): setattr(self, name, cls(http_client, enforce_raw_definitions)) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/code_sources.py0000664000175000017500000000537100000000000024742 0ustar00zuulzuul00000000000000# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 mistralclient.api import base class CodeSource(base.Resource): resource_name = 'CodeSource' class CodeSourceManager(base.ResourceManager): resource_class = CodeSource def create(self, name, content, namespace='', scope='private'): self._ensure_not_empty(name=name, content=content) # If the specified content is actually a file, read from it. content = self.get_contents_if_file(content) return self._create( '/code_sources?name=%s&scope=%s&namespace=%s' % (name, scope, namespace), content, dump_json=False, headers={'content-type': 'text/plain'} ) def update(self, identifier, content, namespace='', scope='private'): self._ensure_not_empty(identifier=identifier, content=content) # If the specified content is actually a file, read from it. content = self.get_contents_if_file(content) return self._update( '/code_sources?identifier=%s&scope=%s&namespace=%s' % (identifier, scope, namespace), content, dump_json=False, headers={'content-type': 'text/plain'}, ) def list(self, namespace='', marker='', limit=None, sort_keys='', sort_dirs='', fields='', **filters): if namespace: filters['namespace'] = namespace query_string = self._build_query_params( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, fields=fields, filters=filters ) return self._list( '/code_sources%s' % query_string, response_key='code_sources', ) def get(self, identifier, namespace=''): self._ensure_not_empty(identifier=identifier) return self._get( '/code_sources/%s?namespace=%s' % (identifier, namespace) ) def delete(self, identifier, namespace=None): self._ensure_not_empty(identifier=identifier) url = '/code_sources/%s' % identifier if namespace: url = url + '?namespace=%s' % namespace self._delete(url) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/cron_triggers.py0000664000175000017500000000467500000000000025142 0ustar00zuulzuul00000000000000# Copyright 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 from oslo_utils import uuidutils from mistralclient.api import base class CronTrigger(base.Resource): resource_name = 'CronTrigger' class CronTriggerManager(base.ResourceManager): resource_class = CronTrigger def create(self, name, workflow_identifier, workflow_input=None, workflow_params=None, pattern=None, first_time=None, count=None): self._ensure_not_empty( name=name, workflow_identifier=workflow_identifier ) data = { 'name': name, 'pattern': pattern, 'first_execution_time': first_time, 'remaining_executions': count } if uuidutils.is_uuid_like(workflow_identifier): data.update({'workflow_id': workflow_identifier}) else: data.update({'workflow_name': workflow_identifier}) if workflow_input: data.update({'workflow_input': jsonutils.dumps(workflow_input)}) if workflow_params: data.update({'workflow_params': jsonutils.dumps(workflow_params)}) return self._create('/cron_triggers', data) def list(self, marker='', limit=None, sort_keys='', fields='', sort_dirs='', **filters): query_string = self._build_query_params( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, fields=fields, filters=filters ) return self._list('/cron_triggers%s' % query_string, response_key='cron_triggers') def get(self, name): self._ensure_not_empty(name=name) return self._get('/cron_triggers/%s' % name) def delete(self, name): self._ensure_not_empty(name=name) self._delete('/cron_triggers/%s' % name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/dynamic_actions.py0000664000175000017500000000610400000000000025424 0ustar00zuulzuul00000000000000# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 uuidutils from mistralclient.api import base class DynamicAction(base.Resource): resource_name = 'DynamicAction' class DynamicActionManager(base.ResourceManager): resource_class = DynamicAction def get(self, identifier, namespace=''): self._ensure_not_empty(identifier=identifier) return self._get( '/dynamic_actions/%s?namespace=%s' % (identifier, namespace) ) def create(self, name, class_name, code_source, scope='private', namespace=''): self._ensure_not_empty( name=name, class_name=class_name, code_source=code_source ) data = { "name": name, "class_name": class_name, "scope": scope, "namespace": namespace } if uuidutils.is_uuid_like(code_source): data['code_source_id'] = code_source else: data['code_source_name'] = code_source return self._create('/dynamic_actions', data) def update(self, identifier, class_name=None, code_source=None, scope='private', namespace=''): self._ensure_not_empty(identifier=identifier) data = { 'scope': scope, 'namespace': namespace } if uuidutils.is_uuid_like(identifier): data['id'] = identifier else: data['name'] = identifier if class_name: data['class_name'] = class_name if code_source: if uuidutils.is_uuid_like(code_source): data['code_source_id'] = code_source else: data['code_source_name'] = code_source return self._update('/dynamic_actions', data) def list(self, marker='', limit=None, sort_keys='', sort_dirs='', fields='', namespace='', **filters): if namespace: filters['namespace'] = namespace query_string = self._build_query_params( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, fields=fields, filters=filters ) return self._list( '/dynamic_actions%s' % query_string, response_key='dynamic_actions', ) def delete(self, identifier, namespace=''): self._ensure_not_empty(identifier=identifier) self._delete( '/dynamic_actions/%s?namespace=%s' % (identifier, namespace) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/environments.py0000664000175000017500000000631600000000000025014 0ustar00zuulzuul00000000000000# Copyright 2015 - StackStorm, 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 from mistralclient.api import base from mistralclient import utils class Environment(base.Resource): resource_name = 'Environment' def _set_attributes(self): """Override loading of the "variables" attribute from text to dict.""" for k, v in self._data.items(): if k == 'variables' and isinstance(v, str): v = jsonutils.loads(v) try: setattr(self, k, v) except AttributeError: # In this case we already defined the attribute on the class pass class EnvironmentManager(base.ResourceManager): resource_class = Environment def create(self, **kwargs): # Check to see if the file name or URI is being passed in. If so, # read it's contents first. if 'file' in kwargs: file = kwargs['file'] kwargs = utils.load_content(self.get_contents_if_file(file)) self._ensure_not_empty(name=kwargs.get('name', None), variables=kwargs.get('variables', None)) # Convert dict to text for the variables attribute. if isinstance(kwargs['variables'], dict): kwargs['variables'] = jsonutils.dumps(kwargs['variables']) return self._create('/environments', kwargs) def update(self, **kwargs): # Check to see if the file name or URI is being passed in. If so, # read it's contents first. if 'file' in kwargs: file = kwargs['file'] kwargs = utils.load_content(self.get_contents_if_file(file)) name = kwargs.get('name', None) self._ensure_not_empty(name=name) # Convert dict to text for the variables attribute. if kwargs.get('variables') and isinstance(kwargs['variables'], dict): kwargs['variables'] = jsonutils.dumps(kwargs['variables']) return self._update('/environments', kwargs) def list(self, marker='', limit=None, sort_keys='', sort_dirs='', fields='', **filters): query_string = self._build_query_params( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, fields=fields, filters=filters ) return self._list('/environments%s' % query_string, response_key='environments') def get(self, name): self._ensure_not_empty(name=name) return self._get('/environments/%s' % name) def delete(self, name): self._ensure_not_empty(name=name) self._delete('/environments/%s' % name) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/event_triggers.py0000664000175000017500000000424200000000000025310 0ustar00zuulzuul00000000000000# Copyright 2017, OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from oslo_serialization import jsonutils from mistralclient.api import base class EventTrigger(base.Resource): resource_name = 'EventTrigger' class EventTriggerManager(base.ResourceManager): resource_class = EventTrigger def create(self, name, workflow_id, exchange, topic, event, workflow_input=None, workflow_params=None): self._ensure_not_empty( name=name, workflow_id=workflow_id ) data = { 'workflow_id': workflow_id, 'name': name, 'exchange': exchange, 'topic': topic, 'event': event } if workflow_input: data.update({'workflow_input': jsonutils.dumps(workflow_input)}) if workflow_params: data.update({'workflow_params': jsonutils.dumps(workflow_params)}) return self._create('/event_triggers', data) def list(self, marker='', limit=None, sort_keys='', sort_dirs='', fields='', **filters): query_string = self._build_query_params( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, fields=fields, filters=filters ) return self._list('/event_triggers%s' % query_string, response_key='event_triggers') def get(self, id): self._ensure_not_empty(id=id) return self._get('/event_triggers/%s' % id) def delete(self, id): self._ensure_not_empty(id=id) self._delete('/event_triggers/%s' % id) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/executions.py0000664000175000017500000001042000000000000024442 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. # Copyright 2020 - Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 from oslo_utils import uuidutils from mistralclient.api import base class Execution(base.Resource): resource_name = 'Execution' class ExecutionManager(base.ResourceManager): resource_class = Execution def create(self, wf_identifier='', namespace='', workflow_input=None, description='', source_execution_id=None, **params): self._ensure_not_empty( workflow_identifier=wf_identifier or source_execution_id ) data = {'description': description} if uuidutils.is_uuid_like(source_execution_id): data.update({'source_execution_id': source_execution_id}) if wf_identifier: if uuidutils.is_uuid_like(wf_identifier): data.update({'workflow_id': wf_identifier}) else: data.update({'workflow_name': wf_identifier}) if namespace: data.update({'workflow_namespace': namespace}) if workflow_input: if isinstance(workflow_input, str): data.update({'input': workflow_input}) else: data.update({'input': jsonutils.dumps(workflow_input)}) if params: data.update({'params': jsonutils.dumps(params)}) return self._create('/executions', data) def update(self, id, state, description=None, env=None): data = {} if state: data['state'] = state if description: data['description'] = description if env: data['params'] = {'env': env} return self._update('/executions/%s' % id, data) def list(self, task=None, marker='', limit=None, sort_keys='', sort_dirs='', fields='', **filters): if task: filters['task_execution_id'] = task # for backwards compatibility if 'nulls' in filters and not filters['nulls']: filters.pop('nulls') query_string = self._build_query_params( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, fields=fields, filters=filters ) return self._list( '/executions%s' % query_string, response_key='executions', ) def get(self, id): self._ensure_not_empty(id=id) return self._get('/executions/%s' % id) def get_ex_sub_executions(self, id, errors_only='', max_depth=-1): ex_sub_execs_path = '/executions/%s/executions%s' params = '?max_depth=%s&errors_only=%s' % (max_depth, errors_only) return self._list( ex_sub_execs_path % (id, params), response_key='executions' ) def delete(self, id, force=None): self._ensure_not_empty(id=id) query_params = {} if force: query_params['force'] = True query_string = self._build_query_params(filters=query_params) self._delete('/executions/%s%s' % (id, query_string)) def get_report(self, id, errors_only=True, max_depth=None, statistics_only=False): self._ensure_not_empty(id=id) query_params = {} if errors_only: query_params['errors_only'] = True if statistics_only: query_params['statistics_only'] = True if max_depth is not None: query_params['max_depth'] = max_depth query_string = self._build_query_params(filters=query_params) resp = self.http_client.get( '/executions/%s/report%s' % (id, query_string) ) return resp.json() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/members.py0000664000175000017500000000443300000000000023715 0ustar00zuulzuul00000000000000# Copyright 2016 - Catalyst IT Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 mistralclient.api import base class Member(base.Resource): resource_name = 'Member' class MemberManager(base.ResourceManager): resource_class = Member def create(self, resource_id, resource_type, member_id): self._ensure_not_empty( resource_id=resource_id, resource_type=resource_type, member_id=member_id ) data = { 'member_id': member_id, } url = '/%ss/%s/members' % (resource_type, resource_id) return self._create(url, data) def update(self, resource_id, resource_type, member_id='', status='accepted'): if not member_id: member_id = self.http_client.project_id url = '/%ss/%s/members/%s' % (resource_type, resource_id, member_id) return self._update(url, {'status': status}) def list(self, resource_id, resource_type): url = '/%ss/%s/members' % (resource_type, resource_id) return self._list(url, response_key='members') def get(self, resource_id, resource_type, member_id=None): self._ensure_not_empty( resource_id=resource_id, resource_type=resource_type, ) if not member_id: member_id = self.http_client.project_id url = '/%ss/%s/members/%s' % (resource_type, resource_id, member_id) return self._get(url) def delete(self, resource_id, resource_type, member_id): self._ensure_not_empty( resource_id=resource_id, resource_type=resource_type, member_id=member_id ) url = '/%ss/%s/members/%s' % (resource_type, resource_id, member_id) self._delete(url) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/services.py0000664000175000017500000000155600000000000024111 0ustar00zuulzuul00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 mistralclient.api import base class Service(base.Resource): resource_name = 'Service' class ServiceManager(base.ResourceManager): resource_class = Service def list(self): return self._list('/services', response_key='services') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/tasks.py0000664000175000017500000000434200000000000023407 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, 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 from mistralclient.api import base from mistralclient.api.v2 import executions class Task(base.Resource): resource_name = 'Task' class TaskManager(base.ResourceManager): resource_class = Task def list(self, workflow_execution_id=None, marker='', limit=None, sort_keys='', sort_dirs='', fields=None, **filters): url = '/tasks' if workflow_execution_id: url = '/executions/%s/tasks' % workflow_execution_id url += '%s' query_string = self._build_query_params( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, fields=fields, filters=filters ) return self._list(url % query_string, response_key='tasks') def get(self, id): self._ensure_not_empty(id=id) return self._get('/tasks/%s' % id) def get_task_sub_executions(self, id, errors_only='', max_depth=-1): task_sub_execs_path = '/tasks/%s/executions%s' params = '?max_depth=%s&errors_only=%s' % (max_depth, errors_only) return self._list( task_sub_execs_path % (id, params), response_key='executions', returned_res_cls=executions.Execution ) def rerun(self, task_ex_id, reset=True, env=None): url = '/tasks/%s' % task_ex_id body = { 'id': task_ex_id, 'state': 'RUNNING', 'reset': reset } if env: body['env'] = jsonutils.dumps(env) return self._update(url, body) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/workbooks.py0000664000175000017500000000654100000000000024305 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, 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 mistralclient.api import base class Workbook(base.Resource): resource_name = 'Workbook' class WorkbookManager(base.ResourceManager): resource_class = Workbook def _get_workbooks_url(self, resource=None, namespace=None, scope=None): url = '/workbooks' if resource: url += '/%s' % resource if scope and namespace: url += '?scope=%s&namespace=%s' % (scope, namespace) elif scope: url += '?scope=%s' % scope elif namespace: url += '?namespace=%s' % namespace return url def create(self, definition, namespace='', scope='private'): self._ensure_not_empty(definition=definition) # If the specified definition is actually a file, read in the # definition file definition = self.get_contents_if_file(definition) return self._create( self._get_workbooks_url(None, namespace, scope), definition, dump_json=False, headers={'content-type': 'text/plain'} ) def update(self, definition, namespace='', scope='private'): self._ensure_not_empty(definition=definition) # If the specified definition is actually a file, read in the # definition file definition = self.get_contents_if_file(definition) return self._update( self._get_workbooks_url(None, namespace, scope), definition, dump_json=False, headers={'content-type': 'text/plain'} ) def list(self, namespace='', marker='', limit=None, sort_keys='', sort_dirs='', fields='', **filters): query_string = self._build_query_params( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, filters=filters, namespace=namespace ) return self._list( '/workbooks{}'.format(query_string), response_key='workbooks' ) def get(self, name, namespace=''): self._ensure_not_empty(name=name) return self._get( self._get_workbooks_url(name, namespace) ) def delete(self, name, namespace=''): self._ensure_not_empty(name=name) self._delete(self._get_workbooks_url(name, namespace)) def validate(self, definition): self._ensure_not_empty(definition=definition) # If the specified definition is actually a file, read in the # definition file definition = self.get_contents_if_file(definition) return self._validate( '/workbooks/validate', definition, dump_json=False, headers={'content-type': 'text/plain'} ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/api/v2/workflows.py0000664000175000017500000000701200000000000024314 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, 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 mistralclient.api import base class Workflow(base.Resource): resource_name = 'Workflow' class WorkflowManager(base.ResourceManager): resource_class = Workflow def create(self, definition, namespace='', scope='private'): self._ensure_not_empty(definition=definition) # If the specified definition is actually a file, read in the # definition file definition = self.get_contents_if_file(definition) return self._create( '/workflows?scope=%s&namespace=%s' % (scope, namespace), definition, response_key='workflows', dump_json=False, headers={'content-type': 'text/plain'}, is_iter_resp=True ) def update(self, definition, namespace='', scope='private', id=None): self._ensure_not_empty(definition=definition) url_pre = ('/workflows/%s' % id) if id else '/workflows' # If the specified definition is actually a file, read in the # definition file definition = self.get_contents_if_file(definition) is_iter_resp = True response_key = 'workflows' if id: is_iter_resp = False response_key = None return self._update( '%s?namespace=%s&scope=%s' % (url_pre, namespace, scope), definition, response_key=response_key, dump_json=False, headers={'content-type': 'text/plain'}, is_iter_resp=is_iter_resp, ) def list(self, namespace='', marker='', limit=None, sort_keys='', sort_dirs='', fields='', **filters): if namespace: filters['namespace'] = namespace query_string = self._build_query_params( marker=marker, limit=limit, sort_keys=sort_keys, sort_dirs=sort_dirs, fields=fields, filters=filters ) return self._list( '/workflows%s' % query_string, response_key='workflows', ) def get(self, identifier, namespace=''): self._ensure_not_empty(identifier=identifier) return self._get( '/workflows/%s?namespace=%s' % (identifier, namespace) ) def delete(self, identifier, namespace=None): self._ensure_not_empty(identifier=identifier) path = '/workflows/%s' % identifier if namespace: path = path + '?namespace=%s' % namespace self._delete(path) def validate(self, definition): self._ensure_not_empty(definition=definition) # If the specified definition is actually a file, read in the # definition file definition = self.get_contents_if_file(definition) return self._validate( '/workflows/validate', definition, dump_json=False, headers={'content-type': 'text/plain'} ) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8485467 python-mistralclient-5.3.0/mistralclient/auth/0000775000175000017500000000000000000000000021546 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/auth/__init__.py0000664000175000017500000000202200000000000023653 0ustar00zuulzuul00000000000000# Copyright 2016 - Brocade Communications Systems, 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 abc from stevedore import driver def get_auth_handler(auth_type): mgr = driver.DriverManager( 'mistralclient.auth', auth_type, invoke_on_load=True ) return mgr.driver class AuthHandler(metaclass=abc.ABCMeta): """Abstract base class for an authentication plugin.""" @abc.abstractmethod def authenticate(self, req): raise NotImplementedError() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/auth/auth_types.py0000664000175000017500000000141000000000000024301 0ustar00zuulzuul00000000000000# Copyright 2016 - Nokia Networks # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 stevedore import extension # Valid authentication types. ALL = extension.ExtensionManager( namespace='mistralclient.auth', invoke_on_load=False ).names() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/auth/keycloak.py0000664000175000017500000001507700000000000023734 0ustar00zuulzuul00000000000000# Copyright 2016 - Nokia Networks # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import os import pprint import urllib import requests from mistralclient import auth LOG = logging.getLogger(__name__) class KeycloakAuthHandler(auth.AuthHandler): def authenticate(self, req, session=None): """Performs authentication using Keycloak OpenID Protocol. :param req: Request dict containing list of parameters required for Keycloak authentication. * auth_url: Base authentication url of KeyCloak server (e.g. "https://my.keycloak:8443/auth" * client_id: Client ID (according to OpenID Connect protocol). * client_secret: Client secret (according to OpenID Connect protocol). * project_name: KeyCloak realm name. * username: User name (Optional, if None then access_token must be provided). * api_key: Password (Optional). * access_token: Access token. If passed, username and password are not used and this method just validates the token and refreshes it if needed (Optional, if None then username must be provided). * cacert: SSL certificate file (Optional). * insecure: If True, SSL certificate is not verified (Optional). :param session: Keystone session object. Not used by this plugin. """ if not isinstance(req, dict): raise TypeError('The input "req" is not typeof dict.') auth_url = req.get('auth_url') client_id = req.get('client_id') client_secret = req.get('client_secret') realm_name = req.get('project_name') username = req.get('username') password = req.get('api_key') access_token = req.get('access_token') cacert = req.get('cacert') insecure = req.get('insecure', False) if not auth_url: raise ValueError('Base authentication url is not provided.') if not client_id: raise ValueError('Client ID is not provided.') if not realm_name: raise ValueError('Project(realm) name is not provided.') if username and access_token: raise ValueError( "User name and access token can't be " "provided at the same time." ) if not username and not access_token: raise ValueError( 'Either user name or access token must be provided.' ) if access_token: response = self._authenticate_with_token( auth_url, client_id, client_secret, access_token, cacert, insecure ) else: response = self._authenticate_with_password( auth_url, client_id, client_secret, realm_name, username, password, cacert, insecure ) return {'auth_token': response, 'project_id': realm_name} @staticmethod def _authenticate_with_token(auth_url, client_id, client_secret, auth_token, cacert=None, insecure=None): # TODO(rakhmerov): Implement. raise NotImplementedError @staticmethod def _authenticate_with_password(auth_url, client_id, client_secret, realm_name, username, password, cacert=None, insecure=None): access_token_endpoint = ( "%s/realms/%s/protocol/openid-connect/token" % (auth_url, realm_name) ) verify = None if urllib.parse.urlparse(access_token_endpoint).scheme == "https": verify = False if insecure else cacert if cacert else True body = { 'grant_type': 'password', 'username': username, 'password': password, 'client_id': client_id, 'scope': 'profile' } if client_secret: body['client_secret'] = client_secret, resp = requests.post( access_token_endpoint, data=body, verify=verify ) try: resp.raise_for_status() except Exception as e: raise Exception("Failed to get access token:\n %s" % str(e)) LOG.debug("HTTP response from OIDC provider: %s", pprint.pformat(resp.json())) return resp.json()['access_token'] 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.") # An example of working curl request to keycloak # curl -d "client_id=admin-cli" -d "client_secret=secret" # -d "username=admin" -d "password=qwerty" -d "grant_type=password" # "http://localhost:8080/auth/realms/master/protocol/openid-connect/token" # An example of using KeyCloak OpenID authentication. if __name__ == '__main__': print("Using username/password to get access token from KeyCloak...") auth_handler = KeycloakAuthHandler() a_token = auth_handler.authenticate( dict( "https://my.keycloak:8443/auth", client_id="mistral_client", client_secret="secret", project_name="mistral", username="user", api_key="secret", insecure=True ) )['auth_token'] print("Auth token: %s" % a_token) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/auth/keystone.py0000664000175000017500000002162600000000000023770 0ustar00zuulzuul00000000000000# Copyright 2016 - Nokia Networks # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import logging import keystoneauth1.access.service_catalog as sc import keystoneauth1.identity.generic as auth_plugin from keystoneauth1 import session as ks_session import mistralclient.api.httpclient as api from mistralclient import auth as mistral_auth from oslo_serialization import jsonutils LOG = logging.getLogger(__name__) class KeystoneAuthHandler(mistral_auth.AuthHandler): def authenticate(self, req, session=None): """Perform authentication via Keystone. :param req: Request dict containing the parameters required for Keystone authentication. """ reqs = self._separate_target_reqs(req) try: result = self._authenticate(reqs, session) except Exception as e: if "Cannot use v2 authentication with domain scope" in str(e): LOG.warning("Client tried to use v2 authentication with " "domain scope. Domain parameters are assumed " "to be erroneously set. Retrying " "authentication without them. " "Request parameters: %s" % str(reqs)) domainless_reqs = [reqs[0], self._remove_domain(reqs[1])] result = self._authenticate(domainless_reqs, session) else: raise return result @staticmethod def _separate_target_reqs(req): """Separates parameters into target and non-target ones. target_* parameters are rekeyed by removing the prefix. :param req: Request dict containing the parameters for Keystone authentication. :return: list of [non-target, target] request parameters """ r = {} target_r = {} target_prefix = "target_" for key in req: if key.startswith(target_prefix): target_r[key[len(target_prefix):]] = req[key] else: r[key] = req[key] return [r, target_r] @staticmethod def _remove_domain(req): """Remove all domain parameters from req. Keystoneauth with V2 does not accept domain parameters. This is an incompatible change from Keystoneclient but it would unnecessarily break clients of Mistral. It is safe to remove domain parameters if V2 auth is targeted. :param req: Request dict containing list of parameters required :return: Request dict without domains """ r = {} for key in req: if "domain" not in key: r[key] = req[key] return r @staticmethod def _get_auth(api_key=None, auth_token=None, auth_url=None, project_domain_id=None, project_domain_name=None, project_id=None, project_name=None, user_domain_id=None, user_domain_name=None, user_id=None, username=None, **kwargs): if project_name and project_id: project_name = None LOG.warning( "Only one of project_name or project_id should be set," "project_name will be ignored" ) if username and user_id: username = None LOG.warning( "Only one of username or user_id should be set," "username will be ignored" ) auth = {} if auth_token: auth = auth_plugin.Token( auth_url=auth_url, project_domain_id=project_domain_id, project_domain_name=project_domain_name, project_id=project_id, project_name=project_name, token=auth_token ) elif api_key and (username or user_id): auth = auth_plugin.Password( auth_url=auth_url, password=api_key, project_domain_id=project_domain_id, project_domain_name=project_domain_name, project_id=project_id, project_name=project_name, user_domain_id=user_domain_id, user_domain_name=user_domain_name, user_id=user_id, username=username ) return auth def _authenticate(self, reqs, session=None): """Performs authentication via Keystone. :param reqs: Request dict containing list of parameters required for Keystone authentication. :return: Auth response dict """ if not isinstance(reqs[0], dict): raise TypeError('The input "req" is not typeof dict.') if not isinstance(reqs[1], dict): raise TypeError('The input "req" is not typeof dict.') auth_response = {} req = reqs[0] cacert = req.get('cacert') endpoint_type = req.get('endpoint_type', 'publicURL') insecure = req.get('insecure') mistral_url = req.get('mistral_url') region_name = req.get('region_name') service_type = req.get('service_type', 'workflowv2') verify = self._verification_needed(cacert, insecure) if not session: auth = self._get_auth(**req) if auth: session = ks_session.Session(auth=auth, verify=verify) if session: if not mistral_url: try: mistral_url = session.get_endpoint( service_type=service_type, interface=endpoint_type, region_name=region_name ) except Exception: mistral_url = None auth_response['mistral_url'] = mistral_url auth_response['session'] = session target_req = reqs[1] if "auth_url" in target_req: target_auth = self._get_auth(**target_req) if target_auth: # target cacert and insecure cacert = target_req.get('cacert') insecure = target_req.get('insecure') verify = self._verification_needed(cacert, insecure) target_session = ks_session.Session( auth=target_auth, verify=verify ) target_auth_headers = target_session.get_auth_headers() or {} target_auth_token = target_auth_headers.get('X-Auth-Token') auth_response.update({ api.TARGET_AUTH_TOKEN: target_auth_token, api.TARGET_PROJECT_ID: target_session.get_project_id(), api.TARGET_USER_ID: target_session.get_user_id(), api.TARGET_AUTH_URI: target_auth._plugin.auth_url, }) access = target_auth.get_access(target_session) service_catalog = access.service_catalog if self._is_service_catalog_v2(service_catalog): access_data = access._data["access"] if not len(access_data['serviceCatalog']): LOG.warning( "Service Catalog empty, some authentication" "credentials may be missing. This can cause" "malfunction in the Mistral action executions.") sc_json = jsonutils.dumps(access_data) auth_response[api.TARGET_SERVICE_CATALOG] = sc_json if not auth_response: LOG.debug("No valid token or password + user provided. " "Continuing without authentication") return {} return auth_response @staticmethod def _verification_needed(cacert, insecure): """Return the verify parameter. The value of verify can be either True/False or a cacert. :param cacert None or path to CA cert file :param insecure truthy value to switch on SSL verification """ if insecure is False or insecure is None: verify = cacert or True else: verify = False return verify @staticmethod def _is_service_catalog_v2(catalog): """Check if the service catalog is of type ServiceCatalogV2 :param catalog: the service catalog :return: True if V2, False otherwise """ return type(catalog) is sc.ServiceCatalogV2 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8485467 python-mistralclient-5.3.0/mistralclient/commands/0000775000175000017500000000000000000000000022406 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/__init__.py0000664000175000017500000000000000000000000024505 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8525467 python-mistralclient-5.3.0/mistralclient/commands/v2/0000775000175000017500000000000000000000000022735 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/__init__.py0000664000175000017500000000000000000000000025034 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/action_executions.py0000664000175000017500000002272000000000000027035 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2016 - Brocade Communications Systems, Inc. # Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import logging from oslo_serialization import jsonutils from osc_lib.command import command from mistralclient.commands.v2 import base from mistralclient import utils LOG = logging.getLogger(__name__) class ActionExecutionFormatter(base.MistralFormatter): COLUMNS = [ ('id', 'ID'), ('name', 'Name'), ('workflow_name', 'Workflow name'), ('workflow_namespace', 'Workflow namespace'), ('task_name', 'Task name'), ('task_execution_id', 'Task ID'), ('state', 'State'), ('state_info', 'State info'), ('accepted', 'Accepted'), ('created_at', 'Created at'), ('updated_at', 'Updated at'), ] LIST_COLUMN_FIELD_NAMES = [c[0] for c in COLUMNS if c[0] != 'state_info'] LIST_COLUMN_HEADING_NAMES = [c[1] for c in COLUMNS if c[0] != 'state_info'] @staticmethod def format(action_ex=None, lister=False): if lister: columns = ActionExecutionFormatter.LIST_COLUMN_HEADING_NAMES else: columns = ActionExecutionFormatter.headings() if action_ex: if hasattr(action_ex, 'task_name'): task_name = action_ex.task_name else: task_name = None data = ( action_ex.id, action_ex.name, action_ex.workflow_name, action_ex.workflow_namespace, task_name, action_ex.task_execution_id, action_ex.state, ) if not lister: data += (action_ex.state_info,) data += ( action_ex.accepted, action_ex.created_at, action_ex.updated_at or '' ) else: data = (('',) * len(columns),) return columns, data class Create(command.ShowOne): """Create new Action execution or just run specific action.""" def produce_output(self, parsed_args, column_names, data): if not column_names: return 0 return super(Create, self).produce_output( parsed_args, column_names, data ) def get_parser(self, prog_name): parser = super(Create, self).get_parser(prog_name) parser.add_argument( 'name', help='Action name to execute.' ) parser.add_argument( dest='input', nargs='?', help='Action input.' ) parser.add_argument( '-s', '--save-result', dest='save_result', action='store_true', help='Save the result into DB.' ) parser.add_argument( '--run-sync', dest='run_sync', action='store_true', help='Run the action synchronously.' ) parser.add_argument( '-t', '--target', dest='target', help='Action will be executed on executor.' ) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace of the action(s).", ) return parser def take_action(self, parsed_args): params = {} if parsed_args.save_result: params['save_result'] = parsed_args.save_result if parsed_args.run_sync: params['run_sync'] = parsed_args.run_sync if parsed_args.target: params['target'] = parsed_args.target action_input = None if parsed_args.input: action_input = utils.load_json(parsed_args.input) mistral_client = self.app.client_manager.workflow_engine action_ex = mistral_client.action_executions.create( parsed_args.name, action_input, namespace=parsed_args.namespace, **params ) if not parsed_args.run_sync and parsed_args.save_result: return ActionExecutionFormatter.format(action_ex) else: self.app.stdout.write("%s\n" % action_ex.output) return None, None class List(base.MistralExecutionLister): """List all Action executions.""" def _get_format_function(self): return ActionExecutionFormatter.format_list def get_parser(self, prog_name): parser = super(List, self).get_parser(prog_name) parser.add_argument( 'task_execution_id', nargs='?', help='Task execution ID.' ) return parser def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.action_executions.list( parsed_args.task_execution_id, marker=parsed_args.marker, limit=parsed_args.limit, sort_keys=parsed_args.sort_keys, sort_dirs=parsed_args.sort_dirs, # TODO(bobh) - Uncomment when the fix for bug 1800322 merges # fields=ActionExecutionFormatter.LIST_COLUMN_FIELD_NAMES, **base.get_filters(parsed_args) ) class Get(command.ShowOne): """Show specific Action execution.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument('action_execution', help='Action execution ID.') return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine execution = mistral_client.action_executions.get( parsed_args.action_execution ) return ActionExecutionFormatter.format(execution) class Update(command.ShowOne): """Update specific Action execution.""" def get_parser(self, prog_name): parser = super(Update, self).get_parser(prog_name) parser.add_argument( 'id', help='Action execution ID.' ) parser.add_argument( '--state', dest='state', choices=['PAUSED', 'RUNNING', 'SUCCESS', 'ERROR', 'CANCELLED'], help='Action execution state' ) parser.add_argument( '--output', dest='output', help='Action execution output' ) return parser def take_action(self, parsed_args): output = None if parsed_args.output: output = utils.load_json(parsed_args.output) mistral_client = self.app.client_manager.workflow_engine execution = mistral_client.action_executions.update( parsed_args.id, parsed_args.state, output ) return ActionExecutionFormatter.format(execution) class GetOutput(command.Command): """Show Action execution output data.""" def get_parser(self, prog_name): parser = super(GetOutput, self).get_parser(prog_name) parser.add_argument( 'id', help='Action execution ID.' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine output = mistral_client.action_executions.get(parsed_args.id).output try: output = jsonutils.loads(output) output = jsonutils.dumps(output, indent=4) + "\n" except Exception: LOG.debug("Task result is not JSON.") self.app.stdout.write(output or "\n") class GetInput(command.Command): """Show Action execution input data.""" def get_parser(self, prog_name): parser = super(GetInput, self).get_parser(prog_name) parser.add_argument( 'id', help='Action execution ID.' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine result = mistral_client.action_executions.get(parsed_args.id).input try: result = jsonutils.loads(result) result = jsonutils.dumps(result, indent=4) + "\n" except Exception: LOG.debug("Task result is not JSON.") self.app.stdout.write(result or "\n") class Delete(command.Command): """Delete action execution.""" def get_parser(self, prog_name): parser = super(Delete, self).get_parser(prog_name) parser.add_argument( 'action_execution', nargs='+', help='Id of action execution identifier(s).' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine utils.do_action_on_many( lambda s: mistral_client.action_executions.delete(s), parsed_args.action_execution, "Request to delete action execution %s has been accepted.", "Unable to delete the specified action execution(s)." ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/actions.py0000664000175000017500000002027400000000000024754 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2020 Nokia Software. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import argparse from osc_lib.command import command from mistralclient.commands.v2 import base from mistralclient import utils class ActionFormatter(base.MistralFormatter): COLUMNS = [ ('id', 'ID'), ('name', 'Name'), ('is_system', 'Is system'), ('input', 'Input'), ('description', 'Description'), ('tags', 'Tags'), ('created_at', 'Created at'), ('updated_at', 'Updated at'), ] @staticmethod def format(action=None, lister=False): if action: tags = getattr(action, 'tags', None) or [] input_ = action.input if not lister else base.cut(action.input) desc = (action.description if not lister else base.cut(action.description)) data = ( action.id, action.name, action.is_system, input_, desc, base.wrap(', '.join(tags)) or '', action.created_at, ) if hasattr(action, 'updated_at'): data += (action.updated_at,) else: data += (None,) else: data = (tuple('' for _ in range(len(ActionFormatter.COLUMNS))),) return ActionFormatter.headings(), data class List(base.MistralLister): """List all actions.""" def _get_format_function(self): return ActionFormatter.format_list def get_parser(self, prog_name): parser = super(List, self).get_parser(prog_name) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace of the actions.", ) return parser def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.actions.list( marker=parsed_args.marker, limit=parsed_args.limit, sort_keys=parsed_args.sort_keys, sort_dirs=parsed_args.sort_dirs, fields=ActionFormatter.fields(), namespace=parsed_args.namespace, **base.get_filters(parsed_args) ) class Get(command.ShowOne): """Show specific action.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument('action', help='Action (name or ID)') parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to create the action within.", ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine action = mistral_client.actions.get( parsed_args.action, parsed_args.namespace ) return ActionFormatter.format(action) class Create(base.MistralLister): """Create new action.""" def get_parser(self, prog_name): parser = super(Create, self).get_parser(prog_name) parser.add_argument( 'definition', type=argparse.FileType('r'), help='Action definition file' ) parser.add_argument( '--public', action='store_true', help='With this flag action will be marked as "public".' ) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to create the action within.", ) return parser def _validate_parsed_args(self, parsed_args): if not parsed_args.definition: raise RuntimeError("Provide action definition file.") def _get_format_function(self): return ActionFormatter.format_list def _get_resources(self, parsed_args): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine return mistral_client.actions.create( parsed_args.definition.read(), namespace=parsed_args.namespace, scope=scope ) class Delete(command.Command): """Delete action.""" def get_parser(self, prog_name): parser = super(Delete, self).get_parser(prog_name) parser.add_argument( 'action', nargs='+', help='Name or ID of action(s).' ) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace of the action(s).", ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine utils.do_action_on_many( lambda s: mistral_client.actions.delete( s, namespace=parsed_args.namespace), parsed_args.action, "Request to delete action %s has been accepted.", "Unable to delete the specified action(s)." ) class Update(base.MistralLister): """Update action.""" def get_parser(self, prog_name): parser = super(Update, self).get_parser(prog_name) parser.add_argument( 'definition', type=argparse.FileType('r'), help='Action definition file' ) parser.add_argument('--id', help='Action ID.') parser.add_argument( '--public', action='store_true', help='With this flag action will be marked as "public".' ) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace of the action.", ) return parser def _get_format_function(self): return ActionFormatter.format_list def _get_resources(self, parsed_args): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine return mistral_client.actions.update( parsed_args.definition.read(), scope=scope, id=parsed_args.id ) class GetDefinition(command.Command): """Show action definition.""" def get_parser(self, prog_name): parser = super(GetDefinition, self).get_parser(prog_name) parser.add_argument('name', help='Action name') parser.add_argument( '--namespace', nargs='?', default='', help="Namespace of the action.", ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine definition = mistral_client.actions.get( parsed_args.name, namespace=parsed_args.namespace).definition self.app.stdout.write(definition or "\n") class Validate(command.ShowOne): """Validate action.""" @staticmethod def _format(result=None): columns = ('Valid', 'Error') if result: data = (result.get('valid'), result.get('error')) else: data = (tuple('' for _ in range(len(columns))),) return columns, data def get_parser(self, prog_name): parser = super(Validate, self).get_parser(prog_name) parser.add_argument( 'definition', type=argparse.FileType('r'), help='action definition file' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine result = mistral_client.actions.validate( parsed_args.definition.read() ) return self._format(result) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/base.py0000664000175000017500000001442100000000000024223 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2020 - Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 abc import datetime as dt import textwrap from osc_lib.command import command DEFAULT_LIMIT = 100 class MistralFormatter(metaclass=abc.ABCMeta): COLUMNS = [] @classmethod def fields(cls): # Column should be a tuple: # (, , ) # If the 3rd value is specified and it's True then # the field is synthetic (calculated) and should not be requested # from the API client. return [c[0] for c in cls.COLUMNS if len(c) == 2 or not c[2]] @classmethod def headings(cls): return [c[1] for c in cls.COLUMNS] @classmethod def format_list(cls, instance=None): return cls.format(instance, lister=True) @staticmethod def format(instance=None, lister=False): pass class MistralLister(command.Lister, metaclass=abc.ABCMeta): @abc.abstractmethod def _get_format_function(self): raise NotImplementedError def get_parser(self, parsed_args): parser = super(MistralLister, self).get_parser(parsed_args) parser.add_argument( '--marker', type=str, help='The last execution uuid of the previous page, displays list ' 'of executions after "marker".', default='', nargs='?' ) parser.add_argument( '--limit', type=int, help='Maximum number of entries to return in a single result. ', nargs='?' ) parser.add_argument( '--sort_keys', help='Comma-separated list of sort keys to sort results by. ' 'Default: created_at. ' 'Example: mistral execution-list --sort_keys=id,description', default='created_at', nargs='?' ) parser.add_argument( '--sort_dirs', help='Comma-separated list of sort directions. Default: asc. ' 'Example: mistral execution-list --sort_keys=id,description ' '--sort_dirs=asc,desc', default='asc', nargs='?' ) parser.add_argument( '--filter', dest='filters', action='append', help='Filters. Can be repeated.' ) return parser @abc.abstractmethod def _get_resources(self, parsed_args): """Gets a list of API resources (e.g. using client).""" raise NotImplementedError def _validate_parsed_args(self, parsed_args): # No-op by default. pass def take_action(self, parsed_args): self._validate_parsed_args(parsed_args) f = self._get_format_function() ret = self._get_resources(parsed_args) if not isinstance(ret, list): ret = [ret] data = [f(r)[1] for r in ret] if data: return f()[0], data else: return f() class MistralExecutionLister(MistralLister, metaclass=abc.ABCMeta): def get_parser(self, parsed_args): parser = super(MistralExecutionLister, self).get_parser(parsed_args) parser.set_defaults(limit=DEFAULT_LIMIT) parser.add_argument( '--oldest', help='Display the executions starting from the oldest entries ' 'instead of the newest', default=False, action='store_true' ) return parser def take_action(self, parsed_args): self._validate_parsed_args(parsed_args) f = self._get_format_function() reverse_results = False if (parsed_args.marker == '' and parsed_args.sort_dirs == 'asc' and parsed_args.sort_keys == 'created_at' and not parsed_args.oldest): reverse_results = True parsed_args.sort_dirs = 'desc' ret = self._get_resources(parsed_args) if not isinstance(ret, list): ret = [ret] if reverse_results: ret.reverse() data = [f(r)[1] for r in ret] if data: return f()[0], data else: return f() def cut(string, length=25): if string and len(string) > length: return "%s..." % string[:length] else: return string def wrap(string, width=25): if string and len(string) > width: return textwrap.fill(string, width) else: return string def get_filters(parsed_args): filters = {} if parsed_args.filters: for f in parsed_args.filters: arr = f.split('=') if len(arr) != 2: raise ValueError('Invalid filter: %s' % f) filters[arr[0]] = arr[1] return filters def get_duration_str(start_dt_str, end_dt_str): """Builds a human friendly duration string. :param start_dt_str: Start date time as an ISO string. :param end_dt_str: End date time as an ISO string. If empty, duration is calculated from the current time. :return: Duration(delta) string. """ if not start_dt_str: return '' start_dt = dt.datetime.strptime(start_dt_str, '%Y-%m-%d %H:%M:%S') if end_dt_str: end_dt = dt.datetime.strptime(end_dt_str, '%Y-%m-%d %H:%M:%S') return str(end_dt - start_dt) delta_from_now = dt.datetime.utcnow() - start_dt # If delta is too small then we won't show any value. It means that # the corresponding process (e.g. an execution) just started. if delta_from_now < dt.timedelta(seconds=2): return '...' # Drop microseconds to decrease verbosity. delta = ( delta_from_now - dt.timedelta(microseconds=delta_from_now.microseconds) ) return "{}...".format(delta) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/code_sources.py0000664000175000017500000001601600000000000025770 0ustar00zuulzuul00000000000000# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import argparse from cliff import show from osc_lib.command import command from mistralclient.commands.v2 import base from mistralclient import utils class CodeSourceFormatter(base.MistralFormatter): COLUMNS = [ ('id', 'ID'), ('name', 'Name'), ('namespace', 'Namespace'), ('project_id', 'Project ID'), ('scope', 'Scope'), ('created_at', 'Created at'), ('updated_at', 'Updated at') ] @staticmethod def format(code_src=None, lister=False): if code_src: data = ( code_src.id, code_src.name, code_src.namespace, code_src.project_id, code_src.scope, code_src.created_at ) if hasattr(code_src, 'updated_at'): data += (code_src.updated_at,) else: data += (None,) else: data = (('',) * len(CodeSourceFormatter.COLUMNS),) return CodeSourceFormatter.headings(), data class List(base.MistralLister): """List all workflows.""" def _get_format_function(self): return CodeSourceFormatter.format_list def get_parser(self, prog_name): parser = super(List, self).get_parser(prog_name) return parser def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.code_sources.list( marker=parsed_args.marker, limit=parsed_args.limit, sort_keys=parsed_args.sort_keys, sort_dirs=parsed_args.sort_dirs, fields=CodeSourceFormatter.fields(), **base.get_filters(parsed_args) ) class Get(show.ShowOne): """Show specific code source.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument('identifier', help='Code source ID or name.') parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to get the code source from.", ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine wf = mistral_client.code_sources.get( parsed_args.identifier, parsed_args.namespace ) return CodeSourceFormatter.format(wf) class Create(command.ShowOne): """Create new code source.""" def get_parser(self, prog_name): parser = super(Create, self).get_parser(prog_name) parser.add_argument('name', help='Code source name.') parser.add_argument( 'content', type=argparse.FileType('r'), help='Code source content file.' ) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to create the code source within.", ) parser.add_argument( '--public', action='store_true', help='With this flag the code source will be marked as "public".' ) return parser def take_action(self, parsed_args): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine code_source = mistral_client.code_sources.create( parsed_args.name, parsed_args.content.read(), namespace=parsed_args.namespace, scope=scope ) return CodeSourceFormatter.format(code_source) class Delete(command.Command): """Delete workflow.""" def get_parser(self, prog_name): parser = super(Delete, self).get_parser(prog_name) parser.add_argument( 'identifier', nargs='+', help='Code source name or ID (can be repeated multiple times).' ) parser.add_argument( '--namespace', nargs='?', default=None, help="Namespace to delete the code source(s) from.", ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine utils.do_action_on_many( lambda s: mistral_client.code_sources.delete(s, parsed_args.namespace), parsed_args.identifier, "Request to delete code source '%s' has been accepted.", "Unable to delete the specified code source(s)." ) class Update(command.ShowOne): """Update workflow.""" def get_parser(self, prog_name): parser = super(Update, self).get_parser(prog_name) parser.add_argument( 'identifier', help='Code source identifier (name or ID).' ) parser.add_argument( 'content', type=argparse.FileType('r'), help='Code source content' ) parser.add_argument('--id', help='Workflow ID.') parser.add_argument( '--namespace', nargs='?', default='', help="Namespace of the workflow.", ) parser.add_argument( '--public', action='store_true', help='With this flag workflow will be marked as "public".' ) return parser def take_action(self, parsed_args): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine code_src = mistral_client.code_sources.update( parsed_args.identifier, parsed_args.content.read(), namespace=parsed_args.namespace, scope=scope ) return CodeSourceFormatter.format(code_src) class GetContent(command.Command): """Show workflow definition.""" def get_parser(self, prog_name): parser = super(GetContent, self).get_parser(prog_name) parser.add_argument('identifier', help='Code source ID or name.') parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to get the code source from.", ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine code_src = mistral_client.code_sources.get( parsed_args.identifier, parsed_args.namespace ) self.app.stdout.write(code_src.content or "\n") ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/cron_triggers.py0000664000175000017500000001540200000000000026160 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import datetime import time from osc_lib.command import command from mistralclient.commands.v2 import base from mistralclient import utils class CronTriggerFormatter(base.MistralFormatter): COLUMNS = [ ('name', 'Name'), ('workflow_name', 'Workflow'), ('workflow_params', 'Params'), ('pattern', 'Pattern'), # TODO(rakhmerov): Uncomment when passwords are handled properly. # TODO(rakhmerov): Add 'Workflow input' column. ('next_execution_time', 'Next execution time'), ('remaining_executions', 'Remaining executions'), ('created_at', 'Created at'), ('updated_at', 'Updated at') ] @staticmethod def format(trigger=None, lister=False): if trigger: # TODO(rakhmerov): Add following here: # TODO(rakhmerov): wf_input = trigger.workflow_input if not lister # TODO(rakhmerov:): else base.cut(trigger.workflow_input) data = ( trigger.name, trigger.workflow_name, trigger.workflow_params, trigger.pattern, # TODO(rakhmerov): Uncomment when passwords are handled # properly. # TODo(rakhmerov): Add 'wf_input' here. trigger.next_execution_time, trigger.remaining_executions, trigger.created_at, ) if hasattr(trigger, 'updated_at'): data += (trigger.updated_at,) else: data += (None,) else: data = (tuple('' for _ in range(len(CronTriggerFormatter.COLUMNS))),) return CronTriggerFormatter.headings(), data class List(base.MistralLister): """List all cron triggers.""" def _get_format_function(self): return CronTriggerFormatter.format_list def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.cron_triggers.list( marker=parsed_args.marker, limit=parsed_args.limit, sort_keys=parsed_args.sort_keys, sort_dirs=parsed_args.sort_dirs, fields=CronTriggerFormatter.fields(), **base.get_filters(parsed_args) ) class Get(command.ShowOne): """Show specific cron trigger.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument('cron_trigger', help='Cron trigger name') return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return CronTriggerFormatter.format(mistral_client.cron_triggers.get( parsed_args.cron_trigger )) class Create(command.ShowOne): """Create new trigger.""" def get_parser(self, prog_name): parser = super(Create, self).get_parser(prog_name) parser.add_argument('name', help='Cron trigger name') parser.add_argument('workflow_identifier', help='Workflow name or ID') parser.add_argument( 'workflow_input', nargs='?', help='Workflow input' ) parser.add_argument( '--params', help='Workflow params', ) parser.add_argument( '--pattern', type=str, help='Cron trigger pattern', metavar='<* * * * *>' ) parser.add_argument( '--first-time', type=str, default=None, help=("Date and time of the first execution. Time is treated as " "local time unless --utc is also specified"), metavar='' ) parser.add_argument( '--count', type=int, help="Number of wanted executions", metavar='' ) parser.add_argument( '--utc', action='store_true', help="All times specified should be treated as UTC" ) return parser @staticmethod def _get_file_content_or_dict(string): if string: return utils.load_json(string) else: return {} @staticmethod def _convert_time_string_to_utc(time_string): datetime_format = '%Y-%m-%d %H:%M' the_time = time_string if the_time: the_time = datetime.datetime.strptime( the_time, datetime_format) is_dst = time.daylight and time.localtime().tm_isdst > 0 utc_offset = - (time.altzone if is_dst else time.timezone) the_time = (the_time - datetime.timedelta( 0, utc_offset)).strftime(datetime_format) return the_time def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine wf_input = self._get_file_content_or_dict(parsed_args.workflow_input) wf_params = self._get_file_content_or_dict(parsed_args.params) first_time = parsed_args.first_time if not parsed_args.utc: first_time = self._convert_time_string_to_utc( parsed_args.first_time) trigger = mistral_client.cron_triggers.create( parsed_args.name, parsed_args.workflow_identifier, wf_input, wf_params, parsed_args.pattern, first_time, parsed_args.count ) return CronTriggerFormatter.format(trigger) class Delete(command.Command): """Delete trigger.""" def get_parser(self, prog_name): parser = super(Delete, self).get_parser(prog_name) parser.add_argument( 'cron_trigger', nargs='+', help='Name of cron trigger(s).' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine utils.do_action_on_many( lambda s: mistral_client.cron_triggers.delete(s), parsed_args.cron_trigger, "Request to delete cron trigger %s has been accepted.", "Unable to delete the specified cron trigger(s)." ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/dynamic_actions.py0000664000175000017500000001575600000000000026471 0ustar00zuulzuul00000000000000# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from osc_lib.command import command from mistralclient.commands.v2 import base from mistralclient import utils class DynamicActionFormatter(base.MistralFormatter): COLUMNS = [ ('id', 'ID'), ('name', 'Name'), ('class_name', 'Class'), ('code_source_id', 'Code source ID'), ('code_source_name', 'Code source name'), ('project_id', 'Project ID'), ('scope', 'Scope'), ('created_at', 'Created at'), ('updated_at', 'Updated at'), ] @staticmethod def format(action=None, lister=False): if action: data = ( action.id, action.name, action.class_name, action.code_source_id, action.code_source_name, action.project_id, action.scope, action.created_at ) if hasattr(action, 'updated_at'): data += (action.updated_at,) else: data += (None,) else: data = (('',)*len(DynamicActionFormatter.COLUMNS),) return DynamicActionFormatter.headings(), data class List(base.MistralLister): """List all dynamic actions.""" def _get_format_function(self): return DynamicActionFormatter.format_list def get_parser(self, prog_name): parser = super(List, self).get_parser(prog_name) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace of dynamic actions.", ) return parser def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.dynamic_actions.list( marker=parsed_args.marker, limit=parsed_args.limit, sort_keys=parsed_args.sort_keys, sort_dirs=parsed_args.sort_dirs, fields=DynamicActionFormatter.fields(), namespace=parsed_args.namespace, **base.get_filters(parsed_args) ) class Get(command.ShowOne): """Show specific dynamic action.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument( 'identifier', help='Dynamic action identifier (name or ID)' ) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to create the dynamic action within.", ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine action = mistral_client.dynamic_actions.get( parsed_args.identifier, parsed_args.namespace ) return DynamicActionFormatter.format(action) class Create(command.ShowOne): """Create new action.""" def get_parser(self, prog_name): parser = super(Create, self).get_parser(prog_name) parser.add_argument('name', help='Dynamic action name') parser.add_argument('class_name', help='Dynamic action class name') parser.add_argument('code_source', help='Code source ID or name') parser.add_argument( '--public', action='store_true', help='With this flag an action will be marked as "public".' ) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to create the action within.", ) return parser def take_action(self, parsed_args): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine dyn_action = mistral_client.dynamic_actions.create( parsed_args.name, parsed_args.class_name, parsed_args.code_source, namespace=parsed_args.namespace, scope=scope ) return DynamicActionFormatter.format(dyn_action) class Delete(command.Command): """Delete action.""" def get_parser(self, prog_name): parser = super(Delete, self).get_parser(prog_name) parser.add_argument( 'identifier', nargs='+', help="Dynamic action name or ID (can be repeated multiple times)." ) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace of the dynamic action(s).", ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine utils.do_action_on_many( lambda s: mistral_client.dynamic_actions.delete( s, namespace=parsed_args.namespace ), parsed_args.identifier, "Request to delete dynamic action(s) %s has been accepted.", "Unable to delete the specified dynamic action(s)." ) class Update(command.ShowOne): """Update dynamic action.""" def get_parser(self, prog_name): parser = super(Update, self).get_parser(prog_name) parser.add_argument( 'identifier', help='Dynamic action identifier (ID or name)' ) parser.add_argument( '--class-name', dest='class_name', nargs='?', help='Dynamic action class name.' ) parser.add_argument( '--code-source', dest='code_source', nargs='?', help='Code source identifier (ID or name).' ) parser.add_argument( '--public', action='store_true', help='With this flag action will be marked as "public".' ) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace of the action.", ) return parser def take_action(self, parsed_args): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine dyn_action = mistral_client.dynamic_actions.update( parsed_args.identifier, class_name=parsed_args.class_name, code_source=parsed_args.code_source, scope=scope, namespace=parsed_args.namespace ) return DynamicActionFormatter.format(dyn_action) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/environments.py0000664000175000017500000001321700000000000026042 0ustar00zuulzuul00000000000000# Copyright 2015 - StackStorm, 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 argparse from oslo_serialization import jsonutils from osc_lib.command import command from mistralclient.commands.v2 import base from mistralclient import utils class EnvironmentFormatter(base.MistralFormatter): COLUMNS = [ ('name', 'Name'), ('description', 'Description'), ('variables', 'Variables'), ('scope', 'Scope'), ('created_at', 'Created at'), ('updated_at', 'Updated at'), ] LIST_COLUMN_FIELD_NAMES = [c[0] for c in COLUMNS if c[0] != 'variables'] LIST_COLUMN_HEADING_NAMES = [c[1] for c in COLUMNS if c[0] != 'variables'] @staticmethod def format(environment=None, lister=False): if lister: columns = EnvironmentFormatter.LIST_COLUMN_HEADING_NAMES else: columns = EnvironmentFormatter.headings() if environment: data = ( environment.name,) if hasattr(environment, 'description'): data += (environment.description or '',) else: data += (None,) if not lister: data += (jsonutils.dumps(environment.variables, indent=4),) data += ( environment.scope, environment.created_at,) if hasattr(environment, 'updated_at'): data += (environment.updated_at or '',) else: data += (None,) else: data = (tuple('' for _ in range(len(columns))),) return columns, data class List(base.MistralLister): """List all environments.""" def _get_format_function(self): return EnvironmentFormatter.format_list def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.environments.list( marker=parsed_args.marker, limit=parsed_args.limit, sort_keys=parsed_args.sort_keys, sort_dirs=parsed_args.sort_dirs, fields=EnvironmentFormatter.fields(), **base.get_filters(parsed_args) ) class Get(command.ShowOne): """Show specific environment.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument('environment', help='Environment name') parser.add_argument( '--export', default=False, action='store_true', help='Export the environment suitable for import' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine environment = mistral_client.environments.get(parsed_args.environment) if parsed_args.export: columns = ('name', 'description', 'scope', 'variables') data = (environment.name, environment.description, environment.scope, jsonutils.dumps(environment.variables)) return columns, data return EnvironmentFormatter.format(environment) class Create(command.ShowOne): """Create new environment.""" def get_parser(self, prog_name): parser = super(Create, self).get_parser(prog_name) parser.add_argument( 'file', type=argparse.FileType('r'), help='Environment configuration file in JSON or YAML' ) return parser def take_action(self, parsed_args): data = utils.load_content(parsed_args.file.read()) mistral_client = self.app.client_manager.workflow_engine environment = mistral_client.environments.create(**data) return EnvironmentFormatter.format(environment) class Delete(command.Command): """Delete environment.""" def get_parser(self, prog_name): parser = super(Delete, self).get_parser(prog_name) parser.add_argument( 'environment', nargs='+', help='Name of environment(s).' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine utils.do_action_on_many( lambda s: mistral_client.environments.delete(s), parsed_args.environment, "Request to delete environment %s has been accepted.", "Unable to delete the specified environment(s)." ) class Update(command.ShowOne): """Update environment.""" def get_parser(self, prog_name): parser = super(Update, self).get_parser(prog_name) parser.add_argument( 'file', type=argparse.FileType('r'), help='Environment configuration file in JSON or YAML' ) return parser def take_action(self, parsed_args): data = utils.load_content(parsed_args.file.read()) mistral_client = self.app.client_manager.workflow_engine environment = mistral_client.environments.update(**data) return EnvironmentFormatter.format(environment) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/event_triggers.py0000664000175000017500000001224600000000000026343 0ustar00zuulzuul00000000000000# Copyright 2017, OpenStack Foundation # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from osc_lib.command import command from mistralclient.commands.v2 import base from mistralclient import utils class EventTriggerFormatter(base.MistralFormatter): COLUMNS = [ ('id', 'ID'), ('name', 'Name'), ('workflow_id', 'Workflow ID'), ('workflow_params', 'Params'), ('exchange', 'Exchange'), ('topic', 'Topic'), ('event', 'Event'), ('created_at', 'Created at'), ('updated_at', 'Updated at') ] @staticmethod def format(trigger=None, lister=False): if trigger: data = ( trigger.id, trigger.name, trigger.workflow_id, trigger.workflow_params, trigger.exchange, trigger.topic, trigger.event, trigger.created_at, ) if hasattr(trigger, 'updated_at'): data += (trigger.updated_at,) else: data += (None,) else: data = (tuple('' for _ in range(len(EventTriggerFormatter.COLUMNS))),) return EventTriggerFormatter.headings(), data class List(base.MistralLister): """List all event triggers.""" def _get_format_function(self): return EventTriggerFormatter.format_list def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.event_triggers.list( marker=parsed_args.marker, limit=parsed_args.limit, sort_keys=parsed_args.sort_keys, sort_dirs=parsed_args.sort_dirs, fields=EventTriggerFormatter.fields(), **base.get_filters(parsed_args) ) class Get(command.ShowOne): """Show specific event trigger.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument('event_trigger', help='Event trigger ID') return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return EventTriggerFormatter.format(mistral_client.event_triggers.get( parsed_args.event_trigger)) class Create(command.ShowOne): """Create new trigger.""" def get_parser(self, prog_name): parser = super(Create, self).get_parser(prog_name) parser.add_argument('name', help='Event trigger name') parser.add_argument('workflow_id', help='Workflow ID') parser.add_argument('exchange', type=str, help='Event trigger exchange') parser.add_argument('topic', type=str, help='Event trigger topic') parser.add_argument('event', type=str, help='Event trigger event name') parser.add_argument('workflow_input', nargs='?', help='Workflow input') parser.add_argument('--params', help='Workflow params') return parser @staticmethod def _get_json_string_or_dict(string): if string: return utils.load_json(string) else: return {} def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine wf_input = self._get_json_string_or_dict(parsed_args.workflow_input) wf_params = self._get_json_string_or_dict(parsed_args.params) trigger = mistral_client.event_triggers.create( parsed_args.name, parsed_args.workflow_id, parsed_args.exchange, parsed_args.topic, parsed_args.event, wf_input, wf_params, ) return EventTriggerFormatter.format(trigger) class Delete(command.Command): """Delete trigger.""" def get_parser(self, prog_name): parser = super(Delete, self).get_parser(prog_name) parser.add_argument( 'event_trigger_id', nargs='+', help='ID of event trigger(s).' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine utils.do_action_on_many( lambda s: mistral_client.event_triggers.delete(s), parsed_args.event_trigger_id, "Request to delete event trigger %s has been accepted.", "Unable to delete the specified event trigger(s)." ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/executions.py0000664000175000017500000004255700000000000025512 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. # Copyright 2016 - Brocade Communications Systems, Inc. # # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import logging import os.path from oslo_serialization import jsonutils from osc_lib.command import command from cliff.lister import Lister as cliff_lister from mistralclient.commands.v2 import base from mistralclient import utils LOG = logging.getLogger(__name__) class ExecutionFormatter(base.MistralFormatter): COLUMNS = [ ('id', 'ID'), ('workflow_id', 'Workflow ID'), ('workflow_name', 'Workflow name'), ('workflow_namespace', 'Workflow namespace'), ('description', 'Description'), ('task_execution_id', 'Task Execution ID'), ('root_execution_id', 'Root Execution ID'), ('state', 'State'), ('state_info', 'State info'), ('created_at', 'Created at'), ('updated_at', 'Updated at'), ('duration', 'Duration', True), ] @staticmethod def format(wf_ex=None, lister=False): if wf_ex: state_info = ( wf_ex.state_info if not lister else base.cut(wf_ex.state_info) ) duration = base.get_duration_str( wf_ex.created_at, wf_ex.updated_at if wf_ex.state in ['ERROR', 'SUCCESS'] else '' ) data = ( wf_ex.id, wf_ex.workflow_id, wf_ex.workflow_name, wf_ex.workflow_namespace, wf_ex.description, wf_ex.task_execution_id or '', wf_ex.root_execution_id or '', wf_ex.state, state_info, wf_ex.created_at, wf_ex.updated_at or '', duration ) else: data = (tuple('' for _ in range(len(ExecutionFormatter.COLUMNS))),) return ExecutionFormatter.headings(), data class List(base.MistralExecutionLister): """List all executions.""" def _get_format_function(self): return ExecutionFormatter.format_list def get_parser(self, parsed_args): parser = super(List, self).get_parser(parsed_args) parser.add_argument( '--task', nargs='?', help="Parent task execution ID associated with workflow " "execution list.", ) parser.add_argument( '--rootsonly', help="return only root executions", action='store_true', ) return parser def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine none_fields = 'root_execution_id' if parsed_args.rootsonly else '' return mistral_client.executions.list( nulls=none_fields, task=parsed_args.task, marker=parsed_args.marker, limit=parsed_args.limit, sort_keys=parsed_args.sort_keys, sort_dirs=parsed_args.sort_dirs, fields=ExecutionFormatter.fields(), **base.get_filters(parsed_args) ) class Get(command.ShowOne): """Show specific execution.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument('execution', help='Execution identifier') return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine execution = mistral_client.executions.get(parsed_args.execution) return ExecutionFormatter.format(execution) class Create(command.ShowOne): """Create new execution.""" def get_parser(self, prog_name): parser = super(Create, self).get_parser(prog_name) parser.add_argument( 'workflow_identifier', nargs='?', help='Workflow ID or name' ) parser.add_argument( '--namespace', nargs='?', default='', help="Workflow namespace." ) parser.add_argument( 'workflow_input', nargs='?', help='Workflow input' ) parser.add_argument( 'params', nargs='?', help='Workflow additional parameters' ) parser.add_argument( '-d', '--description', dest='description', default='', help='Execution description' ) parser.add_argument( '-s', dest='source_execution_id', nargs='?', help="Workflow Execution id which will allow operators to create " "a new workflow execution based on the previously successful " "executed workflow. Example: mistral execution-create -s " "123e4567-e89b-12d3-a456-426655440000") return parser def take_action(self, parsed_args): if parsed_args.workflow_input: wf_input = utils.load_json(parsed_args.workflow_input) else: wf_input = {} if parsed_args.params: params = utils.load_json(parsed_args.params) else: params = {} mistral_client = self.app.client_manager.workflow_engine execution = mistral_client.executions.create( parsed_args.workflow_identifier, parsed_args.namespace, wf_input, parsed_args.description, parsed_args.source_execution_id, **params ) return ExecutionFormatter.format(execution) class Delete(command.Command): """Delete execution.""" def get_parser(self, prog_name): parser = super(Delete, self).get_parser(prog_name) parser.add_argument( 'execution', nargs='+', help='Id of execution identifier(s).' ) parser.add_argument( '--force', default=False, action='store_true', help='Force the deletion of an execution. Might cause a cascade ' ' of errors if used for running executions.' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine force = parsed_args.force utils.do_action_on_many( lambda s: mistral_client.executions.delete(s, force=force), parsed_args.execution, "Request to delete execution %s has been accepted.", "Unable to delete the specified execution(s)." ) class Update(command.ShowOne): """Update execution.""" def get_parser(self, prog_name): parser = super(Update, self).get_parser(prog_name) parser.add_argument('id', help='Execution identifier') parser.add_argument( '-s', '--state', dest='state', choices=['RUNNING', 'PAUSED', 'SUCCESS', 'ERROR', 'CANCELLED'], help='Execution state' ) parser.add_argument( '-e', '--env', dest='env', help='Environment variables' ) parser.add_argument( '-d', '--description', dest='description', help='Execution description' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine env = ( utils.load_file(parsed_args.env) if parsed_args.env and os.path.isfile(parsed_args.env) else utils.load_content(parsed_args.env) ) execution = mistral_client.executions.update( parsed_args.id, parsed_args.state, description=parsed_args.description, env=env ) return ExecutionFormatter.format(execution) class GetInput(command.Command): """Show execution input data.""" def get_parser(self, prog_name): parser = super(GetInput, self).get_parser(prog_name) parser.add_argument('id', help='Execution ID') return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine ex_input = mistral_client.executions.get(parsed_args.id).input try: ex_input = jsonutils.loads(ex_input) ex_input = jsonutils.dumps(ex_input, indent=4) + "\n" except Exception: LOG.debug("Execution input is not JSON.") self.app.stdout.write(ex_input or "\n") class GetOutput(command.Command): """Show execution output data.""" def get_parser(self, prog_name): parser = super(GetOutput, self).get_parser(prog_name) parser.add_argument('id', help='Execution ID') return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine output = mistral_client.executions.get(parsed_args.id).output try: output = jsonutils.loads(output) output = jsonutils.dumps(output, indent=4) + "\n" except Exception: LOG.debug("Execution output is not JSON.") self.app.stdout.write(output or "\n") REPORT_ENTRY_INDENT = 4 class GetReport(command.Command): """Print execution report.""" def get_parser(self, prog_name): parser = super(GetReport, self).get_parser(prog_name) parser.add_argument('id', help='Execution ID') parser.add_argument( '--errors-only', dest='errors_only', action='store_true', help='Only error paths will be included.' ) parser.add_argument( '--statistics-only', dest='statistics_only', action='store_true', help='Only the statistics will be included.' ) parser.add_argument( '--no-errors-only', dest='errors_only', action='store_false', help='Not only error paths will be included.' ) parser.set_defaults(errors_only=True) parser.add_argument( '--max-depth', dest='max_depth', nargs='?', type=int, default=-1, help='Maximum depth of the workflow execution tree. ' 'If 0, only the root workflow execution and its ' 'tasks will be included' ) return parser def print_line(self, line, level=0): self.app.stdout.write( "%s%s\n" % (' ' * (level * REPORT_ENTRY_INDENT), line) ) def print_workflow_execution_entry(self, wf_ex, level): self.print_line( "workflow '%s' [%s] %s" % (wf_ex['name'], wf_ex['state'], wf_ex['id']), level ) if 'task_executions' in wf_ex: for t_ex in wf_ex['task_executions']: self.print_task_execution_entry(t_ex, level + 1) def print_task_execution_entry(self, t_ex, level): self.print_line( "task '%s' [%s] %s" % (t_ex['name'], t_ex['state'], t_ex['id']), level ) if 'retry_count' in t_ex: self.print_line('(retry count: %s)' % t_ex['retry_count'], level) if t_ex['state'] == 'ERROR': state_info = t_ex['state_info'] if state_info: state_info = state_info[0:100].replace('\n', ' ') + '...' self.print_line('(error info: %s)' % state_info, level) if 'action_executions' in t_ex: for a_ex in t_ex['action_executions']: self.print_action_execution_entry(a_ex, level + 1) if 'workflow_executions' in t_ex: for wf_ex in t_ex['workflow_executions']: self.print_workflow_execution_entry(wf_ex, level + 1) def print_action_execution_entry(self, a_ex, level): self.print_line( "action '%s' [%s] %s" % (a_ex['name'], a_ex['state'], a_ex['id']), level ) if a_ex['state'] == 'ERROR': state_info = a_ex['state_info'] if state_info: state_info = state_info[0:100] + '...' self.print_line('(error info: %s)' % state_info, level) def print_statistics(self, stat): self.print_line( 'Number of tasks in SUCCESS state: %s' % stat['success_tasks_count'] ) self.print_line( 'Number of tasks in ERROR state: %s' % stat['error_tasks_count'] ) self.print_line( 'Number of tasks in RUNNING state: %s' % stat['running_tasks_count'] ) self.print_line( 'Number of tasks in IDLE state: %s' % stat['idle_tasks_count'] ) self.print_line( 'Number of tasks in PAUSED state: %s\n' % stat['paused_tasks_count'] ) if 'estimated_time' in stat: self.print_line( 'Estimated time (seconds) for the execution to finish:'' %s\n' % stat['estimated_time'] ) def print_report(self, report_json): self.print_line( "\nTo get more details on a task failure " "run: mistral task-get -c 'State info'\n" ) frame_line = '=' * 30 self.print_line( '%s General Statistics %s\n' % (frame_line, frame_line) ) self.print_statistics(report_json['statistics']) if 'root_workflow_execution' in report_json: self.print_line( '%s Workflow Execution Tree %s\n' % (frame_line, frame_line) ) self.print_workflow_execution_entry( report_json['root_workflow_execution'], 0 ) def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine report_json = mistral_client.executions.get_report( parsed_args.id, errors_only=parsed_args.errors_only, max_depth=parsed_args.max_depth, statistics_only=parsed_args.statistics_only, ) self.print_report(report_json) class GetPublished(command.Command): """Show workflow global published variables.""" def get_parser(self, prog_name): parser = super(GetPublished, self).get_parser(prog_name) parser.add_argument( 'id', help='Workflow ID') return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine res = mistral_client.executions.get(parsed_args.id) published = res.published_global \ if hasattr(res, 'published_global') else None try: published = jsonutils.loads(published) published = jsonutils.dumps(published, indent=4) + "\n" except Exception: LOG.debug("Task result is not JSON.") self.app.stdout.write(published or "\n") class SubExecutionsBaseLister(cliff_lister): def _get_format_function(self): raise NotImplementedError def _get_resources_function(self): raise NotImplementedError def get_parser(self, prog_name): parser = super(SubExecutionsBaseLister, self).get_parser(prog_name) parser.add_argument( 'id', metavar='ID', help='origin id' ) parser.add_argument( '--errors-only', dest='errors_only', action='store_true', help='Only error paths will be included.' ) parser.add_argument( '--max-depth', dest='max_depth', nargs='?', type=int, default=-1, help='Maximum depth of the workflow execution tree. ' 'If 0, only the root workflow execution and its ' 'tasks will be included' ) return parser def _get_resources(self, parsed_args): resource_function = self._get_resources_function() errors_only = parsed_args.errors_only or '' return resource_function( parsed_args.id, errors_only=errors_only, max_depth=parsed_args.max_depth, ) def take_action(self, parsed_args): format_func = self._get_format_function() execs_list = self._get_resources(parsed_args) if not isinstance(execs_list, list): execs_list = [execs_list] data = [format_func(r)[1] for r in execs_list] return (format_func()[0], data) if data else format_func() class SubExecutionsLister(SubExecutionsBaseLister): def _get_format_function(self): return ExecutionFormatter.format_list def _get_resources_function(self): mistral_client = self.app.client_manager.workflow_engine return mistral_client.executions.get_ex_sub_executions ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/members.py0000664000175000017500000001500400000000000024741 0ustar00zuulzuul00000000000000# Copyright 2016 - Catalyst IT Limited # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from osc_lib.command import command from mistralclient.commands.v2 import base from mistralclient import exceptions class MemberFormatter(base.MistralFormatter): COLUMNS = [ ('resource_id', 'Resource ID'), ('resource_type', 'Resource Type'), ('project_id', 'Resource Owner'), ('member_id', 'Member ID'), ('status', 'Status'), ('created_at', 'Created at'), ('updated_at', 'Updated at') ] @staticmethod def format(member=None, lister=False): if member: data = ( member.resource_id, member.resource_type, member.project_id, member.member_id, member.status, member.created_at, ) if hasattr(member, 'updated_at'): data += (member.updated_at,) else: data += (None,) else: data = (tuple('' for _ in range(len(MemberFormatter.COLUMNS))),) return MemberFormatter.headings(), data class List(base.MistralLister): """List all members.""" def _get_format_function(self): return MemberFormatter.format_list def get_parser(self, parsed_args): parser = super(List, self).get_parser(parsed_args) parser.add_argument( 'resource_id', help='Resource id to be shared.' ) parser.add_argument( 'resource_type', help='Resource type.' ) return parser def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.members.list( parsed_args.resource_id, parsed_args.resource_type ) class Get(command.ShowOne): """Show specific member information.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument( 'resource', help='Resource ID to be shared.' ) parser.add_argument( 'resource_type', help='Resource type.' ) parser.add_argument( '-m', '--member-id', default='', help='Project ID to whom the resource is shared to. No need to ' 'provide this param if you are the resource member.' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine member = mistral_client.members.get( parsed_args.resource, parsed_args.resource_type, parsed_args.member_id, ) return MemberFormatter.format(member) class Create(command.ShowOne): """Shares a resource to another tenant.""" def get_parser(self, prog_name): parser = super(Create, self).get_parser(prog_name) parser.add_argument( 'resource_id', help='Resource ID to be shared.' ) parser.add_argument( 'resource_type', help='Resource type.' ) parser.add_argument( 'member_id', help='Project ID to whom the resource is shared to.' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine member = mistral_client.members.create( parsed_args.resource_id, parsed_args.resource_type, parsed_args.member_id, ) return MemberFormatter.format(member) class Delete(command.Command): """Delete a resource sharing relationship.""" def get_parser(self, prog_name): parser = super(Delete, self).get_parser(prog_name) parser.add_argument( 'resource', help='Resource ID to be shared.' ) parser.add_argument( 'resource_type', help='Resource type.' ) parser.add_argument( 'member_id', help='Project ID to whom the resource is shared to.' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine try: mistral_client.members.delete( parsed_args.resource, parsed_args.resource_type, parsed_args.member_id, ) print( "Request to delete %s member %s has been accepted." % (parsed_args.resource_type, parsed_args.member_id) ) except Exception as e: print(e) error_msg = "Unable to delete the specified member." raise exceptions.MistralClientException(error_msg) class Update(command.ShowOne): """Update resource sharing status.""" def get_parser(self, prog_name): parser = super(Update, self).get_parser(prog_name) parser.add_argument( 'resource_id', help='Resource ID to be shared.' ) parser.add_argument( 'resource_type', help='Resource type.' ) parser.add_argument( '-m', '--member-id', default='', help='Project ID to whom the resource is shared to. No need to ' 'provide this param if you are the resource member.' ) parser.add_argument( '-s', '--status', default='accepted', choices=['pending', 'accepted', 'rejected'], help='status of the sharing.' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine member = mistral_client.members.update( parsed_args.resource_id, parsed_args.resource_type, parsed_args.member_id, status=parsed_args.status ) return MemberFormatter.format(member) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/services.py0000664000175000017500000000221400000000000025131 0ustar00zuulzuul00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 mistralclient.commands.v2 import base def format_list(service=None): columns = ('Name', 'Type') if service: data = (service.name, service.type) else: data = (tuple('' for _ in range(len(columns))),) return columns, data class List(base.MistralLister): """List all services.""" def _get_format_function(self): return format_list def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.services.list() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/tasks.py0000664000175000017500000001522700000000000024443 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # import logging import os.path from oslo_serialization import jsonutils from osc_lib.command import command from mistralclient.commands.v2 import base from mistralclient.commands.v2 import executions from mistralclient import utils LOG = logging.getLogger(__name__) class TaskFormatter(base.MistralFormatter): COLUMNS = [ ('id', 'ID'), ('name', 'Name'), ('workflow_name', 'Workflow name'), ('workflow_namespace', 'Workflow namespace'), ('workflow_execution_id', 'Workflow Execution ID'), ('state', 'State'), ('state_info', 'State info'), ('created_at', 'Created at'), ('started_at', 'Started at'), ('finished_at', 'Finished at'), ('duration', 'Duration', True), ] @staticmethod def format(task=None, lister=False): if task: state_info = (task.state_info if not lister else base.cut(task.state_info)) duration = base.get_duration_str(task.started_at, task.finished_at) data = ( task.id, task.name, task.workflow_name, task.workflow_namespace, task.workflow_execution_id, task.state, state_info, task.created_at, task.started_at or '', task.finished_at or '', duration ) else: data = (tuple('' for _ in range(len(TaskFormatter.COLUMNS))),) return TaskFormatter.headings(), data class List(base.MistralExecutionLister): """List all tasks.""" def get_parser(self, prog_name): parser = super(List, self).get_parser(prog_name) parser.add_argument( 'workflow_execution', nargs='?', help='Workflow execution ID associated with list of Tasks.' ) return parser def _get_format_function(self): return TaskFormatter.format_list def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.tasks.list( parsed_args.workflow_execution, marker=parsed_args.marker, limit=parsed_args.limit, sort_keys=parsed_args.sort_keys, sort_dirs=parsed_args.sort_dirs, fields=TaskFormatter.fields(), **base.get_filters(parsed_args) ) class Get(command.ShowOne): """Show specific task.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument('task', help='Task identifier') return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine execution = mistral_client.tasks.get(parsed_args.task) return TaskFormatter.format(execution) class GetResult(command.Command): """Show task output data.""" def get_parser(self, prog_name): parser = super(GetResult, self).get_parser(prog_name) parser.add_argument( 'id', help='Task ID') return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine result = mistral_client.tasks.get(parsed_args.id).result try: result = jsonutils.loads(result) result = jsonutils.dumps(result, indent=4) + "\n" except Exception: LOG.debug("Task result is not JSON.") self.app.stdout.write(result or "\n") class GetPublished(command.Command): """Show task published variables.""" def get_parser(self, prog_name): parser = super(GetPublished, self).get_parser(prog_name) parser.add_argument( 'id', help='Task ID') return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine res = mistral_client.tasks.get(parsed_args.id) published = res.published published_glob = res.published_global \ if hasattr(res, 'published_global') else None try: published = jsonutils.loads(published) published = jsonutils.dumps(published, indent=4) + "\n" if published_glob: published_glob = jsonutils.loads(published_glob) published += jsonutils.dumps(published_glob, indent=4) + "\n" except Exception: LOG.debug("Task result is not JSON.") self.app.stdout.write(published or "\n") class Rerun(command.ShowOne): """Rerun an existing task.""" def get_parser(self, prog_name): parser = super(Rerun, self).get_parser(prog_name) parser.add_argument( 'id', help='Task identifier' ) parser.add_argument( '--resume', action='store_true', dest='resume', default=False, help=('rerun only failed or unstarted action ' 'executions for with-items task') ) parser.add_argument( '-e', '--env', dest='env', help='Environment variables' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine env = ( utils.load_file(parsed_args.env) if parsed_args.env and os.path.isfile(parsed_args.env) else utils.load_content(parsed_args.env) ) execution = mistral_client.tasks.rerun( parsed_args.id, reset=(not parsed_args.resume), env=env ) return TaskFormatter.format(execution) class SubExecutionsLister(executions.SubExecutionsBaseLister): def _get_format_function(self): return executions.ExecutionFormatter.format_list def _get_resources_function(self): mistral_client = self.app.client_manager.workflow_engine return mistral_client.tasks.get_task_sub_executions ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/workbooks.py0000664000175000017500000001612500000000000025334 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, 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 argparse from osc_lib.command import command from mistralclient.commands.v2 import base from mistralclient import utils class WorkbookFormatter(base.MistralFormatter): COLUMNS = [ ('name', 'Name'), ('namespace', 'Namespace'), ('tags', 'Tags'), ('scope', 'Scope'), ('created_at', 'Created at'), ('updated_at', 'Updated at') ] @staticmethod def format(workbook=None, lister=False): if workbook: data = ( workbook.name, workbook.namespace, base.wrap(', '.join(workbook.tags or '')) or '', workbook.scope, workbook.created_at, ) if hasattr(workbook, 'updated_at'): data += (workbook.updated_at,) else: data += (None,) else: data = (tuple('' for _ in range(len(WorkbookFormatter.COLUMNS))),) return WorkbookFormatter.headings(), data class List(base.MistralLister): """List all workbooks.""" def _get_format_function(self): return WorkbookFormatter.format def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.workbooks.list( marker=parsed_args.marker, limit=parsed_args.limit, sort_keys=parsed_args.sort_keys, sort_dirs=parsed_args.sort_dirs, fields=WorkbookFormatter.fields(), **base.get_filters(parsed_args) ) class Get(command.ShowOne): """Show specific workbook.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument('workbook', help='Workbook name') parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to get the workbook from." ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine workbook = mistral_client.workbooks.get( parsed_args.workbook, parsed_args.namespace ) return WorkbookFormatter.format(workbook) class Create(command.ShowOne): """Create new workbook.""" def get_parser(self, prog_name): parser = super(Create, self).get_parser(prog_name) parser.add_argument( 'definition', type=argparse.FileType('r'), help='Workbook definition file' ) parser.add_argument( '--public', action='store_true', help='With this flag workbook will be marked as "public".' ) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to create the workbook within." ) return parser def take_action(self, parsed_args): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine workbook = mistral_client.workbooks.create( parsed_args.definition.read(), namespace=parsed_args.namespace, scope=scope ) return WorkbookFormatter.format(workbook) class Delete(command.Command): """Delete workbook.""" def get_parser(self, prog_name): parser = super(Delete, self).get_parser(prog_name) parser.add_argument('workbook', nargs='+', help='Name of workbook(s).') parser.add_argument( '--namespace', nargs='?', default=None, help="Namespace to delete the workbook(s) from." ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine utils.do_action_on_many( lambda s: mistral_client.workbooks.delete(s, parsed_args.namespace), parsed_args.workbook, "Request to delete workbook %s has been accepted.", "Unable to delete the specified workbook(s)." ) class Update(command.ShowOne): """Update workbook.""" def get_parser(self, prog_name): parser = super(Update, self).get_parser(prog_name) parser.add_argument( 'definition', type=argparse.FileType('r'), help='Workbook definition file' ) parser.add_argument( '--namespace', nargs='?', default=None, help="Namespace to update the workbook in." ) parser.add_argument( '--public', action='store_true', help='With this flag workbook will be marked as "public".' ) return parser def take_action(self, parsed_args): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine workbook = mistral_client.workbooks.update( parsed_args.definition.read(), namespace=parsed_args.namespace, scope=scope ) return WorkbookFormatter.format(workbook) class GetDefinition(command.Command): """Show workbook definition.""" def get_parser(self, prog_name): parser = super(GetDefinition, self).get_parser(prog_name) parser.add_argument('name', help='Workbook name') return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine definition = mistral_client.workbooks.get(parsed_args.name).definition self.app.stdout.write(definition or "\n") class Validate(command.ShowOne): """Validate workbook.""" @staticmethod def _format(result=None): columns = ('Valid', 'Error') if result: data = (result.get('valid'), result.get('error'),) else: data = (tuple('' for _ in range(len(columns))),) return columns, data def get_parser(self, prog_name): parser = super(Validate, self).get_parser(prog_name) parser.add_argument( 'definition', type=argparse.FileType('r'), help='Workbook definition file' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine result = mistral_client.workbooks.validate( parsed_args.definition.read() ) return self._format(result) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/commands/v2/workflows.py0000664000175000017500000002017600000000000025352 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, 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 argparse from cliff import command from cliff import show from mistralclient.commands.v2 import base from mistralclient import utils class WorkflowFormatter(base.MistralFormatter): COLUMNS = [ ('id', 'ID'), ('name', 'Name'), ('namespace', 'Namespace'), ('project_id', 'Project ID'), ('tags', 'Tags'), ('input', 'Input'), ('scope', 'Scope'), ('created_at', 'Created at'), ('updated_at', 'Updated at') ] @staticmethod def format(workflow=None, lister=False): if workflow: tags = getattr(workflow, 'tags', None) or [] data = ( workflow.id, workflow.name, workflow.namespace, workflow.project_id, base.wrap(', '.join(tags)) or '', workflow.input if not lister else base.cut(workflow.input), workflow.scope, workflow.created_at ) if hasattr(workflow, 'updated_at'): data += (workflow.updated_at,) else: data += (None,) else: data = (tuple('' for _ in range(len(WorkflowFormatter.COLUMNS))),) return WorkflowFormatter.headings(), data class List(base.MistralLister): """List all workflows.""" def _get_format_function(self): return WorkflowFormatter.format_list def get_parser(self, prog_name): parser = super(List, self).get_parser(prog_name) return parser def _get_resources(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine return mistral_client.workflows.list( marker=parsed_args.marker, limit=parsed_args.limit, sort_keys=parsed_args.sort_keys, sort_dirs=parsed_args.sort_dirs, fields=WorkflowFormatter.fields(), **base.get_filters(parsed_args) ) class Get(show.ShowOne): """Show specific workflow.""" def get_parser(self, prog_name): parser = super(Get, self).get_parser(prog_name) parser.add_argument('workflow', help='Workflow ID or name.') parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to get the workflow from.", ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine wf = mistral_client.workflows.get( parsed_args.workflow, parsed_args.namespace ) return WorkflowFormatter.format(wf) class Create(base.MistralLister): """Create new workflow.""" def get_parser(self, prog_name): parser = super(Create, self).get_parser(prog_name) parser.add_argument( 'definition', type=argparse.FileType('r'), help='Workflow definition file.' ) parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to create the workflow within.", ) parser.add_argument( '--public', action='store_true', help='With this flag workflow will be marked as "public".' ) return parser def _get_format_function(self): return WorkflowFormatter.format_list def _validate_parsed_args(self, parsed_args): if not parsed_args.definition: raise RuntimeError("You must provide path to workflow " "definition file.") def _get_resources(self, parsed_args): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine return mistral_client.workflows.create( parsed_args.definition.read(), namespace=parsed_args.namespace, scope=scope ) class Delete(command.Command): """Delete workflow.""" def get_parser(self, prog_name): parser = super(Delete, self).get_parser(prog_name) parser.add_argument( 'workflow', nargs='+', help='Name or ID of workflow(s).' ) parser.add_argument( '--namespace', nargs='?', default=None, help="Namespace to delete the workflow from.", ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine utils.do_action_on_many( lambda s: mistral_client.workflows.delete(s, parsed_args.namespace), parsed_args.workflow, "Request to delete workflow %s has been accepted.", "Unable to delete the specified workflow(s)." ) class Update(base.MistralLister): """Update workflow.""" def get_parser(self, prog_name): parser = super(Update, self).get_parser(prog_name) parser.add_argument( 'definition', type=argparse.FileType('r'), help='Workflow definition' ) parser.add_argument('--id', help='Workflow ID.') parser.add_argument( '--namespace', nargs='?', default='', help="Namespace of the workflow.", ) parser.add_argument( '--public', action='store_true', help='With this flag workflow will be marked as "public".' ) return parser def _get_format_function(self): return WorkflowFormatter.format_list def _get_resources(self, parsed_args): scope = 'public' if parsed_args.public else 'private' mistral_client = self.app.client_manager.workflow_engine return mistral_client.workflows.update( parsed_args.definition.read(), scope=scope, id=parsed_args.id, namespace=parsed_args.namespace ) class GetDefinition(command.Command): """Show workflow definition.""" def get_parser(self, prog_name): parser = super(GetDefinition, self).get_parser(prog_name) parser.add_argument('identifier', help='Workflow ID or name.') parser.add_argument( '--namespace', nargs='?', default='', help="Namespace to get the workflow from.", ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine wf = mistral_client.workflows.get( parsed_args.identifier, parsed_args.namespace ) self.app.stdout.write(wf.definition or "\n") class Validate(show.ShowOne): """Validate workflow.""" @staticmethod def _format(result=None): columns = ('Valid', 'Error') if result: data = (result.get('valid'), result.get('error'),) else: data = (tuple('' for _ in range(len(columns))),) return columns, data def get_parser(self, prog_name): parser = super(Validate, self).get_parser(prog_name) parser.add_argument( 'definition', type=argparse.FileType('r'), help='Workflow definition file' ) return parser def take_action(self, parsed_args): mistral_client = self.app.client_manager.workflow_engine result = mistral_client.workflows.validate( parsed_args.definition.read() ) return self._format(result) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/exceptions.py0000664000175000017500000000250300000000000023340 0ustar00zuulzuul00000000000000# Copyright 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. class MistralClientException(Exception): """Base Exception for Mistral client To correctly use this class, inherit from it and define a 'message' and 'code' properties. """ message = "An unknown exception occurred" code = "UNKNOWN_EXCEPTION" def __str__(self): return self.message def __init__(self, message=message): self.message = message super(MistralClientException, self).__init__( '%s: %s' % (self.code, self.message)) class IllegalArgumentException(MistralClientException): message = "IllegalArgumentException occurred" code = "ILLEGAL_ARGUMENT_EXCEPTION" def __init__(self, message=None): if message: self.message = message ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/i18n.py0000664000175000017500000000152300000000000021737 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/usage.html """ import oslo_i18n _translators = oslo_i18n.TranslatorFactory(domain='mistralclient') # The primary translation function using the well-known name "_" _ = _translators.primary ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8525467 python-mistralclient-5.3.0/mistralclient/osc/0000775000175000017500000000000000000000000021371 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/osc/__init__.py0000664000175000017500000000000000000000000023470 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/osc/plugin.py0000664000175000017500000000354200000000000023245 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 Workflow service.""" import logging from osc_lib import utils LOG = logging.getLogger(__name__) DEFAULT_WORKFLOW_API_VERSION = '2' API_VERSION_OPTION = 'os_workflow_api_version' API_NAME = 'workflow_engine' API_VERSIONS = { '2': 'mistralclient.api.v2.client.Client', } def make_client(instance): """Returns a workflow_engine service client.""" version = instance._api_version[API_NAME] workflow_client = utils.get_client_class( API_NAME, version, API_VERSIONS) LOG.debug('Instantiating workflow engine client: %s', workflow_client) mistral_url = instance.get_endpoint_for_service_type( 'workflowv2', region_name=instance.region_name, interface='publicURL' ) client = workflow_client(mistral_url=mistral_url, session=instance.session) return client def build_option_parser(parser): """Hook to add global options.""" parser.add_argument( '--os-workflow-api-version', metavar='', default=utils.env( 'OS_WORKFLOW_API_VERSION', default=DEFAULT_WORKFLOW_API_VERSION), help='Workflow API version, default=' + DEFAULT_WORKFLOW_API_VERSION + ' (Env: OS_WORKFLOW_API_VERSION)') return parser ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/shell.py0000664000175000017500000007262600000000000022303 0ustar00zuulzuul00000000000000# Copyright 2015 - StackStorm, 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 Mistral APIs """ import argparse import logging import os import sys from cliff import app from cliff import commandmanager from osc_lib.command import command from mistralclient.api import client from mistralclient.auth import auth_types import mistralclient.commands.v2.action_executions import mistralclient.commands.v2.actions import mistralclient.commands.v2.code_sources import mistralclient.commands.v2.cron_triggers import mistralclient.commands.v2.dynamic_actions import mistralclient.commands.v2.environments import mistralclient.commands.v2.event_triggers import mistralclient.commands.v2.executions import mistralclient.commands.v2.members import mistralclient.commands.v2.services import mistralclient.commands.v2.tasks import mistralclient.commands.v2.workbooks import mistralclient.commands.v2.workflows from mistralclient import exceptions as exe def env(*args, **kwargs): """Returns the first environment variable set. If all are empty, defaults to '' or keyword arg `default`. """ for arg in args: value = os.environ.get(arg) if value: return value return kwargs.get('default', '') class OpenStackHelpFormatter(argparse.HelpFormatter): def __init__(self, prog, indent_increment=2, max_help_position=32, width=None): super(OpenStackHelpFormatter, self).__init__( prog, indent_increment, max_help_position, width ) def start_section(self, heading): # Title-case the headings. heading = '%s%s' % (heading[0].upper(), heading[1:]) super(OpenStackHelpFormatter, self).start_section(heading) class HelpAction(argparse.Action): """Custom help action. Provide a custom action so the -h and --help options to the main app will print a list of the commands. The commands are determined by checking the CommandManager instance, passed in as the "default" value for the action. """ def __call__(self, parser, namespace, values, option_string=None): outputs = [] max_len = 0 app = self.default parser.print_help(app.stdout) app.stdout.write('\nCommands for API v2 :\n') for name, ep in sorted(app.command_manager): factory = ep.load() cmd = factory(self, None) one_liner = cmd.get_description().split('\n')[0] outputs.append((name, one_liner)) max_len = max(len(name), max_len) for (name, one_liner) in outputs: app.stdout.write(' %s %s\n' % (name.ljust(max_len), one_liner)) sys.exit(0) class BashCompletionCommand(command.Command): """Prints all of the commands and options for bash-completion.""" def take_action(self, parsed_args): commands = set() options = set() for option, _action in self.app.parser._option_string_actions.items(): options.add(option) for command_name, _cmd in self.app.command_manager: commands.add(command_name) print(' '.join(commands | options)) class MistralShell(app.App): def __init__(self): super(MistralShell, self).__init__( description=__doc__.strip(), version=mistralclient.__version__, command_manager=commandmanager.CommandManager('mistral.cli'), ) # Set v2 commands by default self._set_shell_commands(self._get_commands_v2()) def configure_logging(self): log_lvl = logging.DEBUG if self.options.debug else logging.WARNING logging.basicConfig( format="%(levelname)s (%(module)s) %(message)s", level=log_lvl ) logging.getLogger('iso8601').setLevel(logging.WARNING) if self.options.verbose_level <= 1: logging.getLogger('requests').setLevel(logging.WARNING) def build_option_parser(self, description, version, argparse_kwargs=None): """Return an argparse option parser for this application. Subclasses may override this method to extend the parser with more global options. :param description: full description of the application :paramtype description: str :param version: version number for the application :paramtype version: str :param argparse_kwargs: extra keyword argument passed to the ArgumentParser constructor :paramtype extra_kwargs: dict """ argparse_kwargs = argparse_kwargs or {} parser = argparse.ArgumentParser( description=description, add_help=False, formatter_class=OpenStackHelpFormatter, **argparse_kwargs ) parser.add_argument( '--version', action='version', version='%(prog)s {0}'.format(version), help='Show program\'s version number and exit.' ) parser.add_argument( '-v', '--verbose', action='count', dest='verbose_level', default=self.DEFAULT_VERBOSE_LEVEL, help='Increase verbosity of output. Can be repeated.', ) parser.add_argument( '--log-file', action='store', default=None, help='Specify a file to log output. Disabled by default.', ) parser.add_argument( '-q', '--quiet', action='store_const', dest='verbose_level', const=0, help='Suppress output except warnings and errors.', ) parser.add_argument( '-h', '--help', action=HelpAction, nargs=0, default=self, # tricky help="Show this help message and exit.", ) parser.add_argument( '--debug', default=False, action='store_true', help='Show tracebacks on errors.', ) parser.add_argument( '--os-mistral-url', action='store', dest='mistral_url', default=env('OS_MISTRAL_URL'), help='Mistral API host (Env: OS_MISTRAL_URL)' ) parser.add_argument( '--os-mistral-version', action='store', dest='mistral_version', default=env('OS_MISTRAL_VERSION', default='v2'), help='Mistral API version (default = v2) (Env: ' 'OS_MISTRAL_VERSION)' ) parser.add_argument( '--os-mistral-service-type', action='store', dest='service_type', default=env('OS_MISTRAL_SERVICE_TYPE', default='workflowv2'), help='Mistral service-type (should be the same name as in ' 'keystone-endpoint) (default = workflowv2) (Env: ' 'OS_MISTRAL_SERVICE_TYPE)' ) parser.add_argument( '--os-mistral-endpoint-type', action='store', dest='endpoint_type', default=env('OS_MISTRAL_ENDPOINT_TYPE', default='publicURL'), help='Mistral endpoint-type (should be the same name as in ' 'keystone-endpoint) (default = publicURL) (Env: ' 'OS_MISTRAL_ENDPOINT_TYPE)' ) parser.add_argument( '--os-username', action='store', dest='username', default=env('OS_USERNAME'), help='Authentication username (Env: OS_USERNAME)' ) parser.add_argument( '--os-password', action='store', dest='password', default=env('OS_PASSWORD'), help='Authentication password (Env: OS_PASSWORD)' ) parser.add_argument( '--os-tenant-id', action='store', dest='tenant_id', default=env('OS_TENANT_ID', 'OS_PROJECT_ID'), help='Authentication tenant identifier (Env: OS_TENANT_ID' ' or OS_PROJECT_ID)' ) parser.add_argument( '--os-project-id', action='store', dest='project_id', default=env('OS_TENANT_ID', 'OS_PROJECT_ID'), help='Authentication project identifier (Env: OS_TENANT_ID' ' or OS_PROJECT_ID), will use tenant_id if both tenant_id' ' and project_id are set' ) parser.add_argument( '--os-tenant-name', action='store', dest='tenant_name', default=env('OS_TENANT_NAME', 'OS_PROJECT_NAME'), help='Authentication tenant name (Env: OS_TENANT_NAME' ' or OS_PROJECT_NAME)' ) parser.add_argument( '--os-project-name', action='store', dest='project_name', default=env('OS_TENANT_NAME', 'OS_PROJECT_NAME'), help='Authentication project name (Env: OS_TENANT_NAME' ' or OS_PROJECT_NAME), will use tenant_name if both' ' tenant_name and project_name are set' ) parser.add_argument( '--os-auth-token', action='store', dest='token', default=env('OS_AUTH_TOKEN'), help='Authentication token (Env: OS_AUTH_TOKEN)' ) parser.add_argument( '--os-project-domain-name', action='store', dest='project_domain_name', default=env('OS_PROJECT_DOMAIN_NAME'), help='Authentication project domain name or ID' ' (Env: OS_PROJECT_DOMAIN_NAME or OS_PROJECT_DOMAIN_NAME)' ) parser.add_argument( '--os-project-domain-id', action='store', dest='project_domain_id', default=env('OS_PROJECT_DOMAIN_ID'), help='Authentication project domain ID' ' (Env: OS_PROJECT_DOMAIN_ID)' ) parser.add_argument( '--os-user-domain-name', action='store', dest='user_domain_name', default=env('OS_USER_DOMAIN_NAME'), help='Authentication user domain name' ' (Env: OS_USER_DOMAIN_NAME)' ) parser.add_argument( '--os-user-domain-id', action='store', dest='user_domain_id', default=env('OS_USER_DOMAIN_ID'), help='Authentication user domain name' ' (Env: OS_USER_DOMAIN_ID)' ) parser.add_argument( '--os-auth-url', action='store', dest='auth_url', default=env('OS_AUTH_URL'), help='Authentication URL (Env: OS_AUTH_URL)' ) parser.add_argument( '--os-cert', action='store', dest='os_cert', default=env('OS_CERT'), help='Client Certificate (Env: OS_CERT)' ) parser.add_argument( '--os-key', action='store', dest='os_key', default=env('OS_KEY'), help='Client Key (Env: OS_KEY)' ) parser.add_argument( '--os-cacert', action='store', dest='os_cacert', default=env('OS_CACERT'), help='Authentication CA Certificate (Env: OS_CACERT)' ) parser.add_argument( '--os-region-name', action='store', dest='region_name', default=env('OS_REGION_NAME'), help='Region name (Env: OS_REGION_NAME)' ) parser.add_argument( '--insecure', action='store_true', dest='insecure', default=env('MISTRALCLIENT_INSECURE', default=False), help='Disables SSL/TLS certificate verification ' '(Env: MISTRALCLIENT_INSECURE)' ) parser.add_argument( '--auth-type', action='store', dest='auth_type', default=env('MISTRAL_AUTH_TYPE', default='keystone'), help='Authentication type. Valid options are: %s.' ' (Env: MISTRAL_AUTH_TYPE)' % ', '.join(auth_types.ALL) ) parser.add_argument( '--openid-client-id', action='store', dest='client_id', default=env('OPENID_CLIENT_ID'), help='Client ID (according to OpenID Connect).' ' (Env: OPENID_CLIENT_ID)' ) parser.add_argument( '--openid-client-secret', action='store', dest='client_secret', default=env('OPENID_CLIENT_SECRET'), help='Client secret (according to OpenID Connect)' ' (Env: OPENID_CLIENT_SECRET)' ) parser.add_argument( '--os-target-username', action='store', dest='target_username', default=env('OS_TARGET_USERNAME', default='admin'), help='Authentication username for target cloud' ' (Env: OS_TARGET_USERNAME)' ) parser.add_argument( '--os-target-password', action='store', dest='target_password', default=env('OS_TARGET_PASSWORD'), help='Authentication password for target cloud' ' (Env: OS_TARGET_PASSWORD)' ) parser.add_argument( '--os-target-tenant-id', action='store', dest='target_tenant_id', default=env('OS_TARGET_TENANT_ID'), help='Authentication tenant identifier for target cloud' ' (Env: OS_TARGET_TENANT_ID)' ) parser.add_argument( '--os-target-tenant-name', action='store', dest='target_tenant_name', default=env('OS_TARGET_TENANT_NAME'), help='Authentication tenant name for target cloud' ' (Env: OS_TARGET_TENANT_NAME)' ) parser.add_argument( '--os-target-auth-token', action='store', dest='target_token', default=env('OS_TARGET_AUTH_TOKEN'), help='Authentication token for target cloud' ' (Env: OS_TARGET_AUTH_TOKEN)' ) parser.add_argument( '--os-target-auth-url', action='store', dest='target_auth_url', default=env('OS_TARGET_AUTH_URL'), help='Authentication URL for target cloud' ' (Env: OS_TARGET_AUTH_URL)' ) parser.add_argument( '--os-target_cacert', action='store', dest='target_cacert', default=env('OS_TARGET_CACERT'), help='Authentication CA Certificate for target cloud' ' (Env: OS_TARGET_CACERT)' ) parser.add_argument( '--os-target-region-name', action='store', dest='target_region_name', default=env('OS_TARGET_REGION_NAME'), help='Region name for target cloud' '(Env: OS_TARGET_REGION_NAME)' ) parser.add_argument( '--os-target-user-domain-name', action='store', dest='target_user_domain_name', default=env('OS_TARGET_USER_DOMAIN_NAME'), help='User domain name for target cloud' '(Env: OS_TARGET_USER_DOMAIN_NAME)' ) parser.add_argument( '--os-target-user-domain-id', action='store', dest='target_user_domain_id', default=env('OS_TARGET_USER_DOMAIN_ID'), help='User domain ID for target cloud' '(Env: OS_TARGET_USER_DOMAIN_ID)' ) parser.add_argument( '--os-target-project-domain-name', action='store', dest='target_project_domain_name', default=env('OS_TARGET_PROJECT_DOMAIN_NAME'), help='Project domain name for target cloud' '(Env: OS_TARGET_PROJECT_DOMAIN_NAME)' ) parser.add_argument( '--os-target-project-domain-id', action='store', dest='target_project_domain_id', default=env('OS_TARGET_PROJECT_DOMAIN_ID'), help='Project domain ID for target cloud' '(Env: OS_TARGET_PROJECT_DOMAIN_ID)' ) parser.add_argument( '--target_insecure', action='store_true', dest='target_insecure', default=env('TARGET_MISTRALCLIENT_INSECURE', default=False), help='Disables SSL/TLS certificate verification for target cloud ' '(Env: TARGET_MISTRALCLIENT_INSECURE)' ) parser.add_argument( '--profile', dest='profile', metavar='HMAC_KEY', default=env('OS_PROFILE'), help='HMAC key to use for encrypting context data for performance ' 'profiling of operation. This key should be one of the ' 'values configured for the osprofiler middleware in mistral, ' 'it is specified in the profiler section of the mistral ' 'configuration (i.e. /etc/mistral/mistral.conf). Without the ' 'key, profiling will not be triggered even if osprofiler is ' 'enabled on the server side.' ) return parser def initialize_app(self, argv): self._clear_shell_commands() ver = client.determine_client_version(self.options.mistral_version) self._set_shell_commands(self._get_commands(ver)) # bash-completion and help messages should not require client creation need_client = not ( ('bash-completion' in argv) or ('help' in argv) or ('-h' in argv) or ('--help' in argv)) # Set default for auth_url if not supplied. The default is not # set at the parser to support use cases where auth is not enabled. # An example use case would be a developer's environment. if not self.options.auth_url: if self.options.password or self.options.token: self.options.auth_url = 'http://localhost:35357/v3' if (self.options.auth_type == 'keystone' and not self.options.auth_url.endswith("/v2.0")): # Assume that keystone V3 is used and try to be more user-friendly, # i.e provide default values for domains if (not self.options.project_domain_id and not self.options.project_domain_name): self.options.project_domain_id = "default" if (not self.options.user_domain_id and not self.options.user_domain_name): self.options.user_domain_id = "default" if (not self.options.target_project_domain_id and not self.options.target_project_domain_name): self.options.target_project_domain_id = "default" if (not self.options.target_user_domain_id and not self.options.target_user_domain_name): self.options.target_user_domain_id = "default" if self.options.auth_url and not self.options.token: if not self.options.username: raise exe.IllegalArgumentException( ("You must provide a username " "via --os-username env[OS_USERNAME]") ) if not self.options.password: raise exe.IllegalArgumentException( ("You must provide a password " "via --os-password env[OS_PASSWORD]") ) self.client = self._create_client() if need_client else None # Adding client_manager variable to make mistral client work with # unified OpenStack client. ClientManager = type( 'ClientManager', (object,), dict(workflow_engine=self.client) ) self.client_manager = ClientManager() def _create_client(self): kwargs = { 'cert': self.options.os_cert, 'key': self.options.os_key, 'user_domain_name': self.options.user_domain_name, 'user_domain_id': self.options.user_domain_id, 'project_domain_name': self.options.project_domain_name, 'project_domain_id': self.options.project_domain_id, 'target_project_domain_name': self.options.target_project_domain_name, 'target_project_domain_id': self.options.target_project_domain_id, 'target_user_domain_name': self.options.target_user_domain_name, 'target_user_domain_id': self.options.target_user_domain_id } return client.client( mistral_url=self.options.mistral_url, username=self.options.username, api_key=self.options.password, project_name=self.options.tenant_name or self.options.project_name, auth_url=self.options.auth_url, project_id=self.options.tenant_id or self.options.project_id, endpoint_type=self.options.endpoint_type, service_type=self.options.service_type, region_name=self.options.region_name, auth_token=self.options.token, cacert=self.options.os_cacert, insecure=self.options.insecure, profile=self.options.profile, auth_type=self.options.auth_type, client_id=self.options.client_id, client_secret=self.options.client_secret, target_username=self.options.target_username, target_api_key=self.options.target_password, target_project_name=self.options.target_tenant_name, target_auth_url=self.options.target_auth_url, target_project_id=self.options.target_tenant_id, target_auth_token=self.options.target_token, target_cacert=self.options.target_cacert, target_region_name=self.options.target_region_name, target_insecure=self.options.target_insecure, **kwargs ) def _set_shell_commands(self, cmds_dict): for k, v in cmds_dict.items(): self.command_manager.add_command(k, v) def _clear_shell_commands(self): exclude_cmds = ['help', 'complete'] cmds = self.command_manager.commands.copy() for k, v in cmds.items(): if k not in exclude_cmds: self.command_manager.commands.pop(k) def _get_commands(self, version): if version == 2: return self._get_commands_v2() return {} @staticmethod def _get_commands_v2(): return { 'bash-completion': BashCompletionCommand, 'workbook-list': mistralclient.commands.v2.workbooks.List, 'workbook-get': mistralclient.commands.v2.workbooks.Get, 'workbook-create': mistralclient.commands.v2.workbooks.Create, 'workbook-delete': mistralclient.commands.v2.workbooks.Delete, 'workbook-update': mistralclient.commands.v2.workbooks.Update, 'workbook-get-definition': mistralclient.commands.v2.workbooks.GetDefinition, 'workbook-validate': mistralclient.commands.v2.workbooks.Validate, 'workflow-list': mistralclient.commands.v2.workflows.List, 'workflow-get': mistralclient.commands.v2.workflows.Get, 'workflow-create': mistralclient.commands.v2.workflows.Create, 'workflow-delete': mistralclient.commands.v2.workflows.Delete, 'workflow-update': mistralclient.commands.v2.workflows.Update, 'workflow-get-definition': mistralclient.commands.v2.workflows.GetDefinition, 'workflow-validate': mistralclient.commands.v2.workflows.Validate, 'environment-create': mistralclient.commands.v2.environments.Create, 'environment-delete': mistralclient.commands.v2.environments.Delete, 'environment-update': mistralclient.commands.v2.environments.Update, 'environment-list': mistralclient.commands.v2.environments.List, 'environment-get': mistralclient.commands.v2.environments.Get, 'run-action': mistralclient.commands.v2.action_executions.Create, 'action-execution-list': mistralclient.commands.v2.action_executions.List, 'action-execution-get': mistralclient.commands.v2.action_executions.Get, 'action-execution-get-input': mistralclient.commands.v2.action_executions.GetInput, 'action-execution-get-output': mistralclient.commands.v2.action_executions.GetOutput, 'action-execution-update': mistralclient.commands.v2.action_executions.Update, 'action-execution-delete': mistralclient.commands.v2.action_executions.Delete, 'execution-create': mistralclient.commands.v2.executions.Create, 'execution-delete': mistralclient.commands.v2.executions.Delete, 'execution-update': mistralclient.commands.v2.executions.Update, 'execution-list': mistralclient.commands.v2.executions.List, 'execution-get': mistralclient.commands.v2.executions.Get, 'execution-get-input': mistralclient.commands.v2.executions.GetInput, 'execution-get-output': mistralclient.commands.v2.executions.GetOutput, 'execution-get-report': mistralclient.commands.v2.executions.GetReport, 'execution-get-published': mistralclient.commands.v2.executions.GetPublished, 'execution-get-sub-executions': mistralclient.commands.v2.executions.SubExecutionsLister, 'task-list': mistralclient.commands.v2.tasks.List, 'task-get': mistralclient.commands.v2.tasks.Get, 'task-get-published': mistralclient.commands.v2.tasks.GetPublished, 'task-get-result': mistralclient.commands.v2.tasks.GetResult, 'task-get-sub-executions': mistralclient.commands.v2.tasks.SubExecutionsLister, 'task-rerun': mistralclient.commands.v2.tasks.Rerun, 'action-list': mistralclient.commands.v2.actions.List, 'action-get': mistralclient.commands.v2.actions.Get, 'action-create': mistralclient.commands.v2.actions.Create, 'action-delete': mistralclient.commands.v2.actions.Delete, 'action-update': mistralclient.commands.v2.actions.Update, 'action-get-definition': mistralclient.commands.v2.actions.GetDefinition, 'action-validate': mistralclient.commands.v2.actions.Validate, 'cron-trigger-list': mistralclient.commands.v2.cron_triggers.List, 'cron-trigger-get': mistralclient.commands.v2.cron_triggers.Get, 'cron-trigger-create': mistralclient.commands.v2.cron_triggers.Create, 'cron-trigger-delete': mistralclient.commands.v2.cron_triggers.Delete, 'event-trigger-list': mistralclient.commands.v2.event_triggers.List, 'event-trigger-get': mistralclient.commands.v2.event_triggers.Get, 'event-trigger-create': mistralclient.commands.v2.event_triggers.Create, 'event-trigger-delete': mistralclient.commands.v2.event_triggers.Delete, 'service-list': mistralclient.commands.v2.services.List, 'member-create': mistralclient.commands.v2.members.Create, 'member-delete': mistralclient.commands.v2.members.Delete, 'member-update': mistralclient.commands.v2.members.Update, 'member-list': mistralclient.commands.v2.members.List, 'member-get': mistralclient.commands.v2.members.Get, 'code-source-create': mistralclient.commands.v2.code_sources.Create, 'code-source-get': mistralclient.commands.v2.code_sources.Get, 'code-source-update': mistralclient.commands.v2.code_sources.Update, 'code-source-list': mistralclient.commands.v2.code_sources.List, 'code-source-delete': mistralclient.commands.v2.code_sources.Delete, 'code-source-get-content': mistralclient.commands.v2.code_sources.GetContent, 'dynamic-action-create': mistralclient.commands.v2.dynamic_actions.Create, 'dynamic-action-get': mistralclient.commands.v2.dynamic_actions.Get, 'dynamic-action-update': mistralclient.commands.v2.dynamic_actions.Update, 'dynamic-action-list': mistralclient.commands.v2.dynamic_actions.List, 'dynamic-action-delete': mistralclient.commands.v2.dynamic_actions.Delete, } def main(argv=sys.argv[1:]): return MistralShell().run(argv) if __name__ == '__main__': sys.exit(main(sys.argv[1:])) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8525467 python-mistralclient-5.3.0/mistralclient/tests/0000775000175000017500000000000000000000000021747 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/__init__.py0000664000175000017500000000000000000000000024046 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8525467 python-mistralclient-5.3.0/mistralclient/tests/functional/0000775000175000017500000000000000000000000024111 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/functional/__init__.py0000664000175000017500000000000000000000000026210 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8525467 python-mistralclient-5.3.0/mistralclient/tests/functional/cli/0000775000175000017500000000000000000000000024660 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/functional/cli/__init__.py0000664000175000017500000000000000000000000026757 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/functional/cli/base.py0000664000175000017500000001026700000000000026152 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 os_client_config from tempest.lib.cli import base CLI_DIR = os.environ.get( 'OS_MISTRALCLIENT_EXEC_DIR', os.path.join(os.path.abspath('.'), '.tox/functional/bin') ) def credentials(cloud='devstack-admin'): """Retrieves credentials to run functional tests Credentials are either read via os-client-config from the environment or from a config file ('clouds.yaml'). Environment variables override those from the config file. devstack produces a clouds.yaml with two named clouds - one named 'devstack' which has user privs and one named 'devstack-admin' which has admin privs. This function will default to getting the devstack-admin cloud as that is the current expected behavior. """ return get_cloud_config(cloud=cloud).get_auth_args() def get_cloud_config(cloud='devstack-admin'): return os_client_config.OpenStackConfig().get_one_cloud(cloud=cloud) class MistralCLIAuth(base.ClientTestBase): _mistral_url = None def _get_admin_clients(self): creds = credentials() clients = base.CLIClient( username=creds['username'], password=creds['password'], tenant_name=creds['project_name'], project_name=creds['project_name'], user_domain_id=creds['user_domain_id'], project_domain_id=creds['project_domain_id'], uri=creds['auth_url'], cli_dir=CLI_DIR ) return clients def _get_clients(self): return self._get_admin_clients() def mistral(self, action, flags='', params='', fail_ok=False): """Executes Mistral command.""" mistral_url_op = "--os-mistral-url %s" % self._mistral_url flags = "{} --insecure".format(flags) if 'WITHOUT_AUTH' in os.environ: return base.execute( 'mistral %s' % mistral_url_op, action, flags, params, fail_ok, merge_stderr=False, cli_dir='' ) else: return self.clients.cmd_with_auth( 'mistral %s' % mistral_url_op, action, flags, params, fail_ok ) def get_project_id(self, project_name='admin'): admin_clients = self._get_clients() projects = self.parser.listing( admin_clients.openstack( 'project show', params=project_name, flags='--os-identity-api-version 3 --insecure' ) ) return [o['Value'] for o in projects if o['Field'] == 'id'][0] class MistralCLIAltAuth(base.ClientTestBase): _mistral_url = None def _get_alt_clients(self): creds = credentials('devstack-alt-member') clients = base.CLIClient( username=creds['username'], password=creds['password'], project_name=creds['project_name'], tenant_name=creds['project_name'], user_domain_id=creds['user_domain_id'], project_domain_id=creds['project_domain_id'], uri=creds['auth_url'], cli_dir=CLI_DIR ) return clients def _get_clients(self): return self._get_alt_clients() def mistral_alt(self, action, flags='', params='', mode='alt_user'): """Executes Mistral command for alt_user from alt_tenant.""" mistral_url_op = "--os-mistral-url %s" % self._mistral_url flags = "{} --insecure".format(flags) return self.clients.cmd_with_auth( 'mistral %s' % mistral_url_op, action, flags, params) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8525467 python-mistralclient-5.3.0/mistralclient/tests/functional/cli/v2/0000775000175000017500000000000000000000000025207 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/functional/cli/v2/__init__.py0000664000175000017500000000000000000000000027306 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/functional/cli/v2/base_v2.py0000664000175000017500000002233400000000000027106 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Mirantis, Inc. # Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 time from tempest.lib import exceptions from mistralclient.tests.functional.cli import base MISTRAL_URL = "http://localhost:8989/v2" class MistralClientTestBase(base.MistralCLIAuth, base.MistralCLIAltAuth): _mistral_url = MISTRAL_URL @classmethod def setUpClass(cls): super(MistralClientTestBase, cls).setUpClass() cls.wb_def = os.path.relpath( 'functionaltests/resources/v2/wb_v2.yaml', os.getcwd() ) cls.wb_with_tags_def = os.path.relpath( 'functionaltests/resources/v2/wb_with_tags_v2.yaml', os.getcwd() ) cls.wf_def = os.path.relpath( 'functionaltests/resources/v2/wf_v2.yaml', os.getcwd() ) cls.wf_single_def = os.path.relpath( 'functionaltests/resources/v2/wf_single_v2.yaml', os.getcwd() ) cls.wf_with_delay_def = os.path.relpath( 'functionaltests/resources/v2/wf_delay_v2.yaml', os.getcwd() ) cls.wf_wrapping_wf = os.path.relpath( 'functionaltests/resources/v2/wf_wrapping_wf_v2.yaml', os.getcwd() ) cls.top_level_wf = os.path.relpath( 'functionaltests/resources/v2/for_namespaces/top_level_wf.yaml', os.getcwd() ) cls.middle_wf = os.path.relpath( 'functionaltests/resources/v2/for_namespaces/middle_wf.yaml', os.getcwd() ) cls.lowest_level_wf = os.path.relpath( 'functionaltests/resources/v2/for_namespaces/lowest_level_wf.yaml', os.getcwd() ) cls.async_wf_def = os.path.relpath( 'functionaltests/resources/v2/async.yaml', os.getcwd() ) cls.act_def = os.path.relpath( 'functionaltests/resources/v2/action_v2.yaml', os.getcwd() ) cls.act_tag_def = os.path.relpath( 'functionaltests/resources/v2/action_v2_tags.yaml', os.getcwd() ) def setUp(self): super(MistralClientTestBase, self).setUp() def assertPartIn(self, needle, haystack, message=''): self.assertTrue(any(needle in s for s in haystack), message) def get_field_value(self, obj, field): return [ o['Value'] for o in obj if o['Field'] == "{0}".format(field) ][0] def get_item_info(self, get_from, get_by, value): return [i for i in get_from if i[get_by] == value][0] def mistral_admin(self, cmd, params=""): self.clients = self._get_admin_clients() return self.parser.listing( self.mistral('{0}'.format(cmd), params='{0}'.format(params)) ) def mistral_alt_user(self, cmd, params=""): self.clients = self._get_alt_clients() return self.parser.listing( self.mistral_alt('{0}'.format(cmd), params='{0}'.format(params)) ) def mistral_cli(self, admin, cmd, params=''): if admin: return self.mistral_admin(cmd, params) else: return self.mistral_alt_user(cmd, params) def workbook_create(self, wb_def, namespace='', admin=True, scope='private'): params = '{0}'.format(wb_def) namespace_params = '' if namespace: namespace_params += ' --namespace {}'.format(namespace) if scope == 'public': params += ' --public' params += namespace_params wb = self.mistral_cli( admin, 'workbook-create', params=params ) wb_name = self.get_field_value(wb, "Name") wb_delete_params = wb_name + namespace_params self.addCleanup( self.mistral_cli, admin, 'workbook-delete', params=wb_delete_params ) self.addCleanup( self.mistral_cli, admin, 'workflow-delete', params='wb.wf1' ) return wb def workflow_create(self, wf_def, namespace='', admin=True, scope='private'): params = '{0}'.format(wf_def) if scope == 'public': params += ' --public' if namespace: params += " --namespace " + namespace wf = self.mistral_cli( admin, 'workflow-create', params=params ) for workflow in wf: self.addCleanup( self.mistral_cli, admin, 'workflow-delete', params=workflow['ID'] ) return wf def workflow_member_create(self, wf_id): cmd_param = ( '%s workflow %s' % (wf_id, self.get_project_id("alt_demo")) ) member = self.mistral_admin("member-create", params=cmd_param) self.addCleanup( self.mistral_admin, 'member-delete', params=cmd_param ) return member def action_create(self, act_def, admin=True, scope='private', namespace=''): params = '{0}'.format(act_def) if scope == 'public': params += ' --public' if namespace: params += " --namespace " + namespace acts = self.mistral_cli( admin, 'action-create', params=params ) for action in acts: self.addCleanup( self.mistral_cli, admin, 'action-delete', params=action['Name'] ) return acts def cron_trigger_create(self, name, wf_name, wf_input, pattern=None, count=None, first_time=None, admin=True): optional_params = "" if pattern: optional_params += ' --pattern "{}"'.format(pattern) if count: optional_params += ' --count {}'.format(count) if first_time: optional_params += ' --first-time "{}"'.format(first_time) trigger = self.mistral_cli( admin, 'cron-trigger-create', params='{} {} {} {}'.format(name, wf_name, wf_input, optional_params)) self.addCleanup(self.mistral_cli, admin, 'cron-trigger-delete', params=name) return trigger def event_trigger_create(self, name, wf_id, exchange, topic, event, wf_input, admin=True): trigger = self.mistral_cli( admin, 'event-trigger-create', params=' '.join((name, wf_id, exchange, topic, event, wf_input))) ev_tr_id = self.get_field_value(trigger, 'ID') self.addCleanup(self.mistral_cli, admin, 'event-trigger-delete', params=ev_tr_id) return trigger def execution_create(self, params, admin=True): ex = self.mistral_cli(admin, 'execution-create', params=params) exec_id = self.get_field_value(ex, 'ID') self.addCleanup( self.mistral_cli, admin, 'execution-delete', params="{} --force".format(exec_id) ) return ex def environment_create(self, params, admin=True): env = self.mistral_cli(admin, 'environment-create', params=params) env_name = self.get_field_value(env, 'Name') self.addCleanup( self.mistral_cli, admin, 'environment-delete', params=env_name ) return env def create_file(self, file_name, file_body=""): f = open(file_name, 'w') f.write(file_body) f.close() self.addCleanup(os.remove, file_name) def wait_execution_success(self, exec_id, timeout=180): start_time = time.time() ex = self.mistral_admin('execution-get', params=exec_id) exec_state = self.get_field_value(ex, 'State') expected_states = ['SUCCESS', 'RUNNING'] while exec_state != 'SUCCESS': if time.time() - start_time > timeout: msg = ("Execution exceeds timeout {0} to change state " "to SUCCESS. Execution: {1}".format(timeout, ex)) raise exceptions.TimeoutException(msg) ex = self.mistral_admin('execution-get', params=exec_id) exec_state = self.get_field_value(ex, 'State') if exec_state not in expected_states: msg = ("Execution state %s is not in expected " "states: %s" % (exec_state, expected_states)) raise exceptions.TempestException(msg) time.sleep(2) return True ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/functional/cli/v2/test_cli_multi_tenancy.py0000664000175000017500000003357400000000000032336 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 tempest.lib import exceptions from mistralclient.tests.functional.cli.v2 import base_v2 class WorkbookIsolationCLITests(base_v2.MistralClientTestBase): def test_workbook_name_uniqueness(self): self.workbook_create(self.wb_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, "workbook-create", params="{0}".format(self.wb_def) ) self.workbook_create(self.wb_def, admin=False) self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "workbook-create", params="{0}".format(self.wb_def) ) def test_wb_isolation(self): wb = self.workbook_create(self.wb_def) wb_name = self.get_field_value(wb, "Name") wbs = self.mistral_admin("workbook-list") self.assertIn(wb_name, [w["Name"] for w in wbs]) alt_wbs = self.mistral_alt_user("workbook-list") self.assertNotIn(wb_name, [w["Name"] for w in alt_wbs]) def test_get_wb_from_another_tenant(self): wb = self.workbook_create(self.wb_def) name = self.get_field_value(wb, "Name") self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "workbook-get", params=name ) def test_create_public_workbook(self): wb = self.workbook_create(self.wb_def, scope='public') name = self.get_field_value(wb, "Name") same_wb = self.mistral_alt_user( "workbook-get", params=name ) self.assertEqual( name, self.get_field_value(same_wb, "Name") ) # The workflows should be public too self.mistral_alt_user( "workflow-get", params="wb.wf1" ) # The actions should be public too self.mistral_alt_user( "action-get", params="wb.ac1" ) def test_delete_wb_from_another_tenant(self): wb = self.workbook_create(self.wb_def) name = self.get_field_value(wb, "Name") self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "workbook-delete", params=name ) class WorkflowIsolationCLITests(base_v2.MistralClientTestBase): def test_workflow_name_uniqueness(self): self.workflow_create(self.wf_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, "workflow-create", params="{0}".format(self.wf_def) ) self.workflow_create(self.wf_def, admin=False) self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "workflow-create", params="{0}".format(self.wf_def) ) def test_wf_isolation(self): wf = self.workflow_create(self.wf_def) wfs = self.mistral_admin("workflow-list") self.assertIn(wf[0]["Name"], [w["Name"] for w in wfs]) alt_wfs = self.mistral_alt_user("workflow-list") self.assertNotIn(wf[0]["Name"], [w["Name"] for w in alt_wfs]) def test_get_wf_from_another_tenant(self): wf = self.workflow_create(self.wf_def) self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "workflow-get", params=wf[0]["ID"] ) def test_create_public_workflow(self): wf = self.workflow_create(self.wf_def, scope='public') same_wf = self.mistral_alt_user( "workflow-get", params=wf[0]["Name"] ) self.assertEqual( wf[0]["Name"], self.get_field_value(same_wf, "Name") ) def test_delete_wf_from_another_tenant(self): wf = self.workflow_create(self.wf_def) self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "workflow-delete", params=wf[0]["ID"] ) class WorkflowSharingCLITests(base_v2.MistralClientTestBase): def setUp(self): super(WorkflowSharingCLITests, self).setUp() self.wf = self.workflow_create(self.wf_def, admin=True) def _update_shared_workflow(self, new_status='accepted'): member = self.workflow_member_create(self.wf[0]["ID"]) status = self.get_field_value(member, 'Status') self.assertEqual('pending', status) cmd_param = '%s workflow --status %s --member-id %s' % ( self.wf[0]["ID"], new_status, self.get_project_id("alt_demo")) member = self.mistral_alt_user("member-update", params=cmd_param) status = self.get_field_value(member, 'Status') self.assertEqual(new_status, status) def test_list_accepted_shared_workflow(self): wfs = self.mistral_alt_user("workflow-list") self.assertNotIn(self.wf[0]["ID"], [w["ID"] for w in wfs]) self._update_shared_workflow(new_status='accepted') alt_wfs = self.mistral_alt_user("workflow-list") self.assertIn(self.wf[0]["ID"], [w["ID"] for w in alt_wfs]) self.assertIn( self.get_project_id("admin"), [w["Project ID"] for w in alt_wfs] ) def test_list_rejected_shared_workflow(self): self._update_shared_workflow(new_status='rejected') alt_wfs = self.mistral_alt_user("workflow-list") self.assertNotIn(self.wf[0]["ID"], [w["ID"] for w in alt_wfs]) def test_create_execution_using_shared_workflow(self): self._update_shared_workflow(new_status='accepted') execution = self.execution_create(self.wf[0]["ID"], admin=False) wf_name = self.get_field_value(execution, 'Workflow name') self.assertEqual(self.wf[0]["Name"], wf_name) def test_create_contrigger_using_shared_workflow(self): self._update_shared_workflow(new_status='accepted') trigger = self.cron_trigger_create( "test_trigger", self.wf[0]["ID"], "{}", "5 * * * *", admin=False ) wf_name = self.get_field_value(trigger, 'Workflow') self.assertEqual(self.wf[0]["Name"], wf_name) # Admin project can not delete the shared workflow, because it is used # in a cron-trigger of another project. self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-delete', params=self.wf[0]["ID"] ) class ActionIsolationCLITests(base_v2.MistralClientTestBase): def test_actions_name_uniqueness(self): self.action_create(self.act_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, "action-create", params="{0}".format(self.act_def) ) self.action_create(self.act_def, admin=False) self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "action-create", params="{0}".format(self.act_def) ) def test_action_isolation(self): act = self.action_create(self.act_def) acts = self.mistral_admin("action-list") self.assertIn(act[0]["Name"], [a["Name"] for a in acts]) alt_acts = self.mistral_alt_user("action-list") self.assertNotIn(act[0]["Name"], [a["Name"] for a in alt_acts]) def test_get_action_from_another_tenant(self): act = self.action_create(self.act_def) self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "action-get", params=act[0]["Name"] ) def test_delete_action_from_another_tenant(self): act = self.action_create(self.act_def) self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "action-delete", params=act[0]["Name"] ) def test_create_public_action(self): act = self.action_create(self.act_def, scope='public') same_act = self.mistral_alt_user( "action-get", params=act[0]["Name"] ) self.assertEqual( act[0]["Name"], self.get_field_value(same_act, "Name") ) class CronTriggerIsolationCLITests(base_v2.MistralClientTestBase): def test_cron_trigger_name_uniqueness(self): wf = self.workflow_create(self.wf_def) self.cron_trigger_create( "admin_trigger", wf[0]["ID"], "{}", "5 * * * *" ) self.assertRaises( exceptions.CommandFailed, self.cron_trigger_create, "admin_trigger", wf[0]["ID"], "{}" "5 * * * *", ) wf = self.workflow_create(self.wf_def, admin=False) self.cron_trigger_create( "user_trigger", wf[0]["ID"], "{}", "5 * * * *", None, None, admin=False ) self.assertRaises( exceptions.CommandFailed, self.cron_trigger_create, "user_trigger", wf[0]["ID"], "{}", "5 * * * *", None, None, admin=False ) def test_cron_trigger_isolation(self): wf = self.workflow_create(self.wf_def) self.cron_trigger_create( "trigger", wf[0]["Name"], "{}", "5 * * * *") alt_trs = self.mistral_alt_user("cron-trigger-list") self.assertNotIn("trigger", [t["Name"] for t in alt_trs]) class ExecutionIsolationCLITests(base_v2.MistralClientTestBase): def test_execution_isolation(self): wf = self.workflow_create(self.wf_def) ex = self.execution_create(wf[0]["Name"]) exec_id = self.get_field_value(ex, "ID") execs = self.mistral_admin("execution-list") self.assertIn(exec_id, [e["ID"] for e in execs]) alt_execs = self.mistral_alt_user("execution-list") self.assertNotIn(exec_id, [e["ID"] for e in alt_execs]) def test_get_execution_from_another_tenant(self): wf = self.workflow_create(self.wf_def) ex = self.execution_create(wf[0]["Name"]) exec_id = self.get_field_value(ex, "ID") self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "execution-get", params=exec_id ) class EnvironmentIsolationCLITests(base_v2.MistralClientTestBase): def setUp(self): super(EnvironmentIsolationCLITests, self).setUp() self.env_file = "env.yaml" self.create_file("{0}".format(self.env_file), "name: env\n" "description: Test env\n" "variables:\n" " var: value") def test_environment_name_uniqueness(self): self.environment_create(self.env_file) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, "environment-create", params=self.env_file ) self.environment_create(self.env_file, admin=False) self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "environment-create", params=self.env_file ) def test_environment_isolation(self): env = self.environment_create(self.env_file) env_name = self.get_field_value(env, "Name") envs = self.mistral_admin("environment-list") self.assertIn(env_name, [en["Name"] for en in envs]) alt_envs = self.mistral_alt_user("environment-list") self.assertNotIn(env_name, [en["Name"] for en in alt_envs]) def test_get_env_from_another_tenant(self): env = self.environment_create(self.env_file) env_name = self.get_field_value(env, "Name") self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "environment-get", params=env_name ) def test_delete_env_from_another_tenant(self): env = self.environment_create(self.env_file) env_name = self.get_field_value(env, "Name") self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "environment-delete", params=env_name ) class ActionExecutionIsolationCLITests(base_v2.MistralClientTestBase): def test_action_execution_isolation(self): wf = self.workflow_create(self.wf_def) wf_exec = self.execution_create(wf[0]["Name"]) direct_ex_id = self.get_field_value(wf_exec, 'ID') self.wait_execution_success(direct_ex_id) act_execs = self.mistral_admin("action-execution-list") self.assertIn(wf[0]["Name"], [act["Workflow name"] for act in act_execs]) alt_act_execs = self.mistral_alt_user("action-execution-list") self.assertNotIn(wf[0]["Name"], [act["Workflow name"] for act in alt_act_execs]) def test_get_action_execution_from_another_tenant(self): wf = self.workflow_create(self.wf_def) ex = self.execution_create(wf[0]["Name"]) exec_id = self.get_field_value(ex, "ID") self.assertRaises( exceptions.CommandFailed, self.mistral_alt_user, "action-execution-get", params=exec_id ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/functional/cli/v2/test_cli_v2.py0000664000175000017500000022214400000000000030003 0ustar00zuulzuul00000000000000# Copyright (c) 2014 Mirantis, Inc. # Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 time from tempest.lib import exceptions from mistralclient.tests.functional.cli import base from mistralclient.tests.functional.cli.v2 import base_v2 MISTRAL_URL = "http://localhost:8989/v2" class SimpleMistralCLITests(base.MistralCLIAuth): """Basic tests, check '-list', '-help' commands.""" _mistral_url = MISTRAL_URL @classmethod def setUpClass(cls): super(SimpleMistralCLITests, cls).setUpClass() def test_workbooks_list(self): workbooks = self.parser.listing(self.mistral('workbook-list')) self.assertTableStruct( workbooks, ['Name', 'Tags', 'Created at', 'Updated at'] ) def test_workflow_list(self): workflows = self.parser.listing(self.mistral('workflow-list')) self.assertTableStruct( workflows, ['ID', 'Name', 'Tags', 'Input', 'Scope', 'Created at', 'Updated at'] ) def test_executions_list(self): executions = self.parser.listing(self.mistral('execution-list')) self.assertTableStruct( executions, ['ID', 'Workflow name', 'Workflow ID', 'State', 'Created at', 'Updated at'] ) def test_tasks_list(self): tasks = self.parser.listing(self.mistral('task-list')) self.assertTableStruct( tasks, ['ID', 'Name', 'Workflow name', 'Workflow Execution ID', 'State'] ) def test_cron_trigger_list(self): triggers = self.parser.listing(self.mistral('cron-trigger-list')) self.assertTableStruct( triggers, ['Name', 'Workflow', 'Pattern', 'Next execution time', 'Remaining executions', 'Created at', 'Updated at'] ) def test_event_trigger_list(self): triggers = self.parser.listing(self.mistral('event-trigger-list')) self.assertTableStruct( triggers, ['ID', 'Name', 'Workflow ID', 'Exchange', 'Topic', 'Event', 'Created at', 'Updated at'] ) def test_actions_list(self): actions = self.parser.listing(self.mistral('action-list')) self.assertTableStruct( actions, ['Name', 'Is system', 'Input', 'Description', 'Tags', 'Created at', 'Updated at'] ) def test_environments_list(self): envs = self.parser.listing(self.mistral('environment-list')) self.assertTableStruct( envs, ['Name', 'Description', 'Scope', 'Created at', 'Updated at'] ) def test_action_execution_list(self): act_execs = self.parser.listing(self.mistral('action-execution-list')) self.assertTableStruct( act_execs, ['ID', 'Name', 'Workflow name', 'State', 'Accepted'] ) def test_action_execution_list_with_limit(self): act_execs = self.parser.listing( self.mistral( 'action-execution-list', params='--limit 1' ) ) self.assertEqual(1, len(act_execs)) class WorkbookCLITests(base_v2.MistralClientTestBase): """Test suite checks commands to work with workbooks.""" @classmethod def setUpClass(cls): super(WorkbookCLITests, cls).setUpClass() def test_workbook_create_delete(self): wb = self.mistral_admin('workbook-create', params=self.wb_def) wb_name = self.get_field_value(wb, "Name") self.assertTableStruct(wb, ['Field', 'Value']) wbs = self.mistral_admin('workbook-list') self.assertIn(wb_name, [w['Name'] for w in wbs]) wbs = self.mistral_admin('workbook-list') self.assertIn(wb_name, [w['Name'] for w in wbs]) self.mistral_admin('workbook-delete', params=wb_name) wbs = self.mistral_admin('workbook-list') self.assertNotIn(wb_name, [w['Name'] for w in wbs]) def test_workbook_create_with_tags(self): wb = self.workbook_create(self.wb_with_tags_def) self.assertIn('tag', self.get_field_value(wb, 'Tags')) def test_workbook_update(self): wb = self.workbook_create(self.wb_def) wb_name = self.get_field_value(wb, "Name") init_update_at = self.get_field_value(wb, "Updated at") tags = self.get_field_value(wb, 'Tags') self.assertNotIn('tag', tags) wb = self.mistral_admin('workbook-update', params=self.wb_def) update_at = self.get_field_value(wb, "Updated at") name = self.get_field_value(wb, 'Name') tags = self.get_field_value(wb, 'Tags') self.assertEqual(wb_name, name) self.assertNotIn('tag', tags) self.assertEqual(init_update_at, update_at) wb = self.mistral_admin( 'workbook-update', params=self.wb_with_tags_def ) self.assertTableStruct(wb, ['Field', 'Value']) update_at = self.get_field_value(wb, "Updated at") name = self.get_field_value(wb, 'Name') tags = self.get_field_value(wb, 'Tags') self.assertEqual(wb_name, name) self.assertIn('tag', tags) self.assertNotEqual(init_update_at, update_at) def test_workbook_get(self): created = self.workbook_create(self.wb_with_tags_def) wb_name = self.get_field_value(created, "Name") fetched = self.mistral_admin('workbook-get', params=wb_name) created_wb_name = self.get_field_value(created, 'Name') fetched_wb_name = self.get_field_value(fetched, 'Name') self.assertEqual(created_wb_name, fetched_wb_name) created_wb_tag = self.get_field_value(created, 'Tags') fetched_wb_tag = self.get_field_value(fetched, 'Tags') self.assertEqual(created_wb_tag, fetched_wb_tag) def test_workbook_get_definition(self): wb = self.workbook_create(self.wb_def) wb_name = self.get_field_value(wb, "Name") definition = self.mistral_admin( 'workbook-get-definition', params=wb_name ) self.assertNotIn('404 Not Found', definition) def test_workbook_validate_with_valid_def(self): wb = self.mistral_admin('workbook-validate', params=self.wb_def) wb_valid = self.get_field_value(wb, 'Valid') wb_error = self.get_field_value(wb, 'Error') self.assertEqual('True', wb_valid) self.assertEqual('None', wb_error) def test_workbook_validate_with_invalid_def(self): self.create_file('wb.yaml', 'name: wb\n') wb = self.mistral_admin('workbook-validate', params='wb.yaml') wb_valid = self.get_field_value(wb, 'Valid') wb_error = self.get_field_value(wb, 'Error') self.assertEqual('False', wb_valid) self.assertNotEqual('None', wb_error) class WorkflowCLITests(base_v2.MistralClientTestBase): """Test suite checks commands to work with workflows.""" @classmethod def setUpClass(cls): super(WorkflowCLITests, cls).setUpClass() def test_workflow_create_delete(self): init_wfs = self.mistral_admin('workflow-create', params=self.wf_def) wf_names = [wf['Name'] for wf in init_wfs] self.assertTableStruct(init_wfs, ['Name', 'Created at', 'Updated at']) wfs = self.mistral_admin('workflow-list') self.assertIn(wf_names[0], [workflow['Name'] for workflow in wfs]) for wf_name in wf_names: self.mistral_admin('workflow-delete', params=wf_name) wfs = self.mistral_admin('workflow-list') for wf in wf_names: self.assertNotIn(wf, [workflow['Name'] for workflow in wfs]) def test_workflow_within_namespace_create_delete(self): params = self.wf_def + ' --namespace abcdef' init_wfs = self.mistral_admin('workflow-create', params=params) wf_names = [wf['Name'] for wf in init_wfs] self.assertTableStruct(init_wfs, ['Name', 'Created at', 'Updated at']) wfs = self.mistral_admin('workflow-list') self.assertIn(wf_names[0], [workflow['Name'] for workflow in wfs]) for wf_name in wf_names: self.mistral_admin( 'workflow-delete', params=wf_name + ' --namespace abcdef' ) wfs = self.mistral_admin('workflow-list') for wf in wf_names: self.assertNotIn(wf, [workflow['Name'] for workflow in wfs]) init_wfs = self.mistral_admin('workflow-create', params=params) wf_ids = [wf['ID'] for wf in init_wfs] for wf_id in wf_ids: self.mistral_admin('workflow-delete', params=wf_id) for wf in wf_names: self.assertNotIn(wf, [workflow['Name'] for workflow in wfs]) def test_create_wf_with_tags(self): init_wfs = self.workflow_create(self.wf_def) wf_name = init_wfs[1]['Name'] self.assertTableStruct( init_wfs, ['Name', 'Created at', 'Updated at', 'Tags'] ) created_wf_info = self.get_item_info( get_from=init_wfs, get_by='Name', value=wf_name ) self.assertEqual('tag', created_wf_info['Tags']) def test_workflow_update(self): wf = self.workflow_create(self.wf_def) wf_name = wf[0]['Name'] wf_id = wf[0]['ID'] created_wf_info = self.get_item_info( get_from=wf, get_by='Name', value=wf_name ) # Update a workflow with definition unchanged. upd_wf = self.mistral_admin( 'workflow-update', params='{0}'.format(self.wf_def) ) self.assertTableStruct(upd_wf, ['Name', 'Created at', 'Updated at']) updated_wf_info = self.get_item_info( get_from=upd_wf, get_by='Name', value=wf_name ) self.assertEqual(wf_name, upd_wf[0]['Name']) self.assertEqual( created_wf_info['Created at'].split(".")[0], updated_wf_info['Created at'] ) self.assertEqual( created_wf_info['Updated at'], updated_wf_info['Updated at'] ) # Update a workflow with definition changed. upd_wf = self.mistral_admin( 'workflow-update', params='{0}'.format(self.wf_with_delay_def) ) self.assertTableStruct(upd_wf, ['Name', 'Created at', 'Updated at']) updated_wf_info = self.get_item_info( get_from=upd_wf, get_by='Name', value=wf_name ) self.assertEqual(wf_name, upd_wf[0]['Name']) self.assertEqual( created_wf_info['Created at'].split(".")[0], updated_wf_info['Created at'] ) self.assertNotEqual( created_wf_info['Updated at'], updated_wf_info['Updated at'] ) # Update a workflow with uuid. upd_wf = self.mistral_admin( 'workflow-update', params='{0} --id {1}'.format(self.wf_with_delay_def, wf_id) ) self.assertTableStruct(upd_wf, ['Name', 'Created at', 'Updated at']) updated_wf_info = self.get_item_info( get_from=upd_wf, get_by='ID', value=wf_id ) self.assertEqual(wf_name, upd_wf[0]['Name']) self.assertEqual( created_wf_info['Created at'].split(".")[0], updated_wf_info['Created at'] ) self.assertNotEqual( created_wf_info['Updated at'], updated_wf_info['Updated at'] ) def test_workflow_update_within_namespace(self): namespace = 'abc' wf = self.workflow_create(self.wf_def, namespace=namespace) wf_name = wf[0]['Name'] wf_id = wf[0]['ID'] wf_namespace = wf[0]['Namespace'] created_wf_info = self.get_item_info( get_from=wf, get_by='Name', value=wf_name ) # Update a workflow with definition unchanged. upd_wf = self.mistral_admin( 'workflow-update', params='{0} --namespace {1}'.format(self.wf_def, namespace) ) self.assertTableStruct(upd_wf, ['Name', 'Created at', 'Updated at']) updated_wf_info = self.get_item_info( get_from=upd_wf, get_by='Name', value=wf_name ) self.assertEqual(wf_name, upd_wf[0]['Name']) self.assertEqual(namespace, wf_namespace) self.assertEqual(wf_namespace, upd_wf[0]['Namespace']) self.assertEqual( created_wf_info['Created at'].split(".")[0], updated_wf_info['Created at'] ) self.assertEqual( created_wf_info['Updated at'], updated_wf_info['Updated at'] ) # Update a workflow with definition changed. upd_wf = self.mistral_admin( 'workflow-update', params='{0} --namespace {1}'.format( self.wf_with_delay_def, namespace ) ) self.assertTableStruct(upd_wf, ['Name', 'Created at', 'Updated at']) updated_wf_info = self.get_item_info( get_from=upd_wf, get_by='Name', value=wf_name ) self.assertEqual(wf_name, upd_wf[0]['Name']) self.assertEqual( created_wf_info['Created at'].split(".")[0], updated_wf_info['Created at'] ) self.assertNotEqual( created_wf_info['Updated at'], updated_wf_info['Updated at'] ) # Update a workflow with uuid. upd_wf = self.mistral_admin( 'workflow-update', params='{0} --id {1}'.format(self.wf_with_delay_def, wf_id) ) self.assertTableStruct(upd_wf, ['Name', 'Created at', 'Updated at']) updated_wf_info = self.get_item_info( get_from=upd_wf, get_by='ID', value=wf_id ) self.assertEqual(wf_name, upd_wf[0]['Name']) self.assertEqual( created_wf_info['Created at'].split(".")[0], updated_wf_info['Created at'] ) self.assertNotEqual( created_wf_info['Updated at'], updated_wf_info['Updated at'] ) def test_workflow_update_truncate_input(self): input_value = "very_long_input_parameter_name_that_should_be_truncated" wf_def = """ version: "2.0" workflow1: input: - {0} tasks: task1: action: std.noop """.format(input_value) self.create_file('wf.yaml', wf_def) self.workflow_create('wf.yaml') updated_wf = self.mistral_admin('workflow-update', params='wf.yaml') updated_wf_info = self.get_item_info( get_from=updated_wf, get_by='Name', value='workflow1' ) self.assertEqual(updated_wf_info['Input'][:-3], input_value[:25]) def test_workflow_get(self): created = self.workflow_create(self.wf_def) wf_name = created[0]['Name'] fetched = self.mistral_admin('workflow-get', params=wf_name) fetched_wf_name = self.get_field_value(fetched, 'Name') self.assertEqual(wf_name, fetched_wf_name) def test_workflow_get_with_id(self): created = self.workflow_create(self.wf_def) wf_name = created[0]['Name'] wf_id = created[0]['ID'] fetched = self.mistral_admin('workflow-get', params=wf_id) fetched_wf_name = self.get_field_value(fetched, 'Name') self.assertEqual(wf_name, fetched_wf_name) def test_workflow_get_definition(self): wf = self.workflow_create(self.wf_def) wf_name = wf[0]['Name'] definition = self.mistral_admin( 'workflow-get-definition', params=wf_name ) self.assertNotIn('404 Not Found', definition) def test_workflow_validate_with_valid_def(self): wf = self.mistral_admin('workflow-validate', params=self.wf_def) wf_valid = self.get_field_value(wf, 'Valid') wf_error = self.get_field_value(wf, 'Error') self.assertEqual('True', wf_valid) self.assertEqual('None', wf_error) def test_workflow_validate_with_invalid_def(self): self.create_file('wf.yaml', 'name: wf\n') wf = self.mistral_admin('workflow-validate', params='wf.yaml') wf_valid = self.get_field_value(wf, 'Valid') wf_error = self.get_field_value(wf, 'Error') self.assertEqual('False', wf_valid) self.assertNotEqual('None', wf_error) def test_workflow_list_with_filter(self): self.workflow_create(self.wf_def) workflows = self.parser.listing(self.mistral('workflow-list')) self.assertTableStruct( workflows, ['ID', 'Name', 'Tags', 'Input', 'Scope', 'Created at', 'Updated at'] ) # We created 2 workflows, so we should have at least 2 self.assertGreaterEqual(len(workflows), 2) # Now let's provide a filter to the list command. workflows = self.parser.listing( self.mistral( 'workflow-list', params='--filter name=eq:wf1' ) ) self.assertTableStruct( workflows, ['ID', 'Name', 'Tags', 'Input', 'Scope', 'Created at', 'Updated at'] ) self.assertEqual(1, len(workflows)) self.assertEqual('wf1', workflows[0]['Name']) class ExecutionCLITests(base_v2.MistralClientTestBase): """Test suite checks commands to work with executions.""" @classmethod def setUpClass(cls): super(ExecutionCLITests, cls).setUpClass() def setUp(self): super(ExecutionCLITests, self).setUp() wfs = self.workflow_create(self.wf_def) self.async_wf = self.workflow_create(self.async_wf_def)[0] self.direct_wf = wfs[0] self.reverse_wf = wfs[1] self.create_file('input', '{\n "farewell": "Bye"\n}\n') self.create_file('task_name', '{\n "task_name": "goodbye"\n}\n') def test_execution_by_id_of_workflow_within_namespace(self): namespace = 'abc' wfs = self.workflow_create(self.lowest_level_wf, namespace=namespace) wf_def_name = wfs[0]['Name'] wf_id = wfs[0]['ID'] execution = self.execution_create(wf_id) self.assertTableStruct(execution, ['Field', 'Value']) wf_name = self.get_field_value(execution, 'Workflow name') wf_namespace = self.get_field_value(execution, 'Workflow namespace') wf_id = self.get_field_value(execution, 'Workflow ID') self.assertEqual(wf_def_name, wf_name) self.assertEqual(namespace, wf_namespace) self.assertIsNotNone(wf_id) def test_execution_within_namespace_create_delete(self): namespace = 'abc' self.workflow_create(self.lowest_level_wf) self.workflow_create(self.lowest_level_wf, namespace=namespace) self.workflow_create(self.middle_wf, namespace=namespace) self.workflow_create(self.top_level_wf) wfs = self.workflow_create(self.top_level_wf, namespace=namespace) top_wf_name = wfs[0]['Name'] execution = self.mistral_admin( 'execution-create', params='{0} --namespace {1}'.format(top_wf_name, namespace) ) exec_id = self.get_field_value(execution, 'ID') self.assertTableStruct(execution, ['Field', 'Value']) wf_name = self.get_field_value(execution, 'Workflow name') wf_namespace = self.get_field_value(execution, 'Workflow namespace') wf_id = self.get_field_value(execution, 'Workflow ID') created_at = self.get_field_value(execution, 'Created at') self.assertEqual(top_wf_name, wf_name) self.assertEqual(namespace, wf_namespace) self.assertIsNotNone(wf_id) self.assertIsNotNone(created_at) execs = self.mistral_admin('execution-list') self.assertIn(exec_id, [ex['ID'] for ex in execs]) self.assertIn(wf_name, [ex['Workflow name'] for ex in execs]) self.assertIn(namespace, [ex['Workflow namespace'] for ex in execs]) params = "{} --force".format(exec_id) self.mistral_admin('execution-delete', params=params) def test_execution_create_delete(self): execution = self.mistral_admin( 'execution-create', params='{0} -d "execution test"'.format(self.direct_wf['Name']) ) exec_id = self.get_field_value(execution, 'ID') self.assertTableStruct(execution, ['Field', 'Value']) wf_name = self.get_field_value(execution, 'Workflow name') wf_id = self.get_field_value(execution, 'Workflow ID') created_at = self.get_field_value(execution, 'Created at') description = self.get_field_value(execution, 'Description') self.assertEqual(self.direct_wf['Name'], wf_name) self.assertIsNotNone(wf_id) self.assertIsNotNone(created_at) self.assertEqual("execution test", description) execs = self.mistral_admin('execution-list') self.assertIn(exec_id, [ex['ID'] for ex in execs]) self.assertIn(wf_name, [ex['Workflow name'] for ex in execs]) params = "{} --force".format(exec_id) self.mistral_admin('execution-delete', params=params) def test_execution_create_with_input_and_start_task(self): execution = self.execution_create( "%s input task_name" % self.reverse_wf['Name'] ) exec_id = self.get_field_value(execution, 'ID') result = self.wait_execution_success(exec_id) self.assertTrue(result) def test_execution_update(self): execution = self.execution_create(self.async_wf['Name']) exec_id = self.get_field_value(execution, 'ID') status = self.get_field_value(execution, 'State') self.assertEqual('RUNNING', status) # Update execution state. execution = self.mistral_admin( 'execution-update', params='{0} -s PAUSED'.format(exec_id)) updated_exec_id = self.get_field_value(execution, 'ID') status = self.get_field_value(execution, 'State') self.assertEqual(exec_id, updated_exec_id) self.assertEqual('PAUSED', status) # Update execution description. execution = self.mistral_admin( 'execution-update', params='{0} -d "execution update test"'.format(exec_id) ) description = self.get_field_value(execution, 'Description') self.assertEqual("execution update test", description) def test_execution_get(self): execution = self.execution_create(self.direct_wf['Name']) exec_id = self.get_field_value(execution, 'ID') execution = self.mistral_admin( 'execution-get', params='{0}'.format(exec_id) ) gotten_id = self.get_field_value(execution, 'ID') wf_name = self.get_field_value(execution, 'Workflow name') wf_id = self.get_field_value(execution, 'Workflow ID') self.assertIsNotNone(wf_id) self.assertEqual(exec_id, gotten_id) self.assertEqual(self.direct_wf['Name'], wf_name) def test_execution_get_input(self): execution = self.execution_create(self.direct_wf['Name']) exec_id = self.get_field_value(execution, 'ID') ex_input = self.mistral_admin('execution-get-input', params=exec_id) self.assertEqual([], ex_input) def test_execution_get_output(self): execution = self.execution_create(self.direct_wf['Name']) exec_id = self.get_field_value(execution, 'ID') ex_output = self.mistral_admin('execution-get-output', params=exec_id) self.assertEqual([], ex_output) def test_executions_list_with_task(self): wrapping_wf = self.workflow_create(self.wf_wrapping_wf) decoy = self.execution_create(wrapping_wf[-1]['Name']) wrapping_wf_ex = self.execution_create(wrapping_wf[-1]['Name']) wrapping_wf_ex_id = self.get_field_value(wrapping_wf_ex, 'ID') self.assertIsNot(wrapping_wf_ex_id, self.get_field_value(decoy, 'ID')) tasks = self.mistral_admin('task-list', params=wrapping_wf_ex_id) wrapping_task_id = tasks[-1]['ID'] wf_execs = self.mistral_cli( True, 'execution-list', params="--task {}".format(wrapping_task_id) ) self.assertEqual(1, len(wf_execs)) wf_exec = wf_execs[0] self.assertEqual(wrapping_task_id, wf_exec['Task Execution ID']) def test_executions_list_with_pagination(self): # Create 2 executions ex1 = self.execution_create( params='{0} -d "a"'.format(self.direct_wf['Name']) ) # We need to sleep at least one sec between the creation of the two # executions because the default sort is by created_at descending # (newest first) # And the tests may fail if the created_at are identical (some sort # of race condition) time.sleep(1) ex2 = self.execution_create( params='{0} -d "b"'.format(self.direct_wf['Name']) ) all_wf_ids = [ self.get_field_value(ex1, 'ID'), self.get_field_value(ex2, 'ID'), ] # List all executions wf_execs = self.mistral_cli(True, 'execution-list') # We are supposed to have 2 self.assertEqual(2, len(wf_execs)) # We are supposed to have the correct IDs self.assertEqual( set(all_wf_ids), set([ex['ID'] for ex in wf_execs]) ) # List executions with limit 1 # NOTE(arnaud) mistral client is returning newest first by default # even if it say differently in help message # See change: I002edd1b10ab281072cfa7501cfa763073a7781c # So here, for the test with marker to work, we need to # specificaly ask for ascending execution list using --oldest wf_execs = self.mistral_cli( True, 'execution-list', params="--oldest --limit 1" ) # We are supposed to have one self.assertEqual(1, len(wf_execs)) # List executions starting after the one we received before not_expected = wf_execs[0]['ID'] expected = [ex for ex in all_wf_ids if ex != wf_execs[0]['ID']][0] wf_execs = self.mistral_cli( True, 'execution-list', params="--marker %s" % not_expected ) # Check if we have correct ex ID self.assertNotIn( not_expected, [ex['ID'] for ex in wf_execs] ) self.assertIn( expected, [ex['ID'] for ex in wf_execs] ) # List sorted by Description wf_execs = self.mistral_cli( True, 'execution-list', params="--sort_keys Description" ) # We are supposed to have both self.assertEqual( set(all_wf_ids), set([ex['ID'] for ex in wf_execs]) ) # Now check if they are correctly ordered # Ascending by default wf_ex1_index = -1 wf_ex2_index = -1 for idx, ex in enumerate(wf_execs): if ex['ID'] == all_wf_ids[0]: wf_ex1_index = idx elif ex['ID'] == all_wf_ids[1]: wf_ex2_index = idx self.assertLess(wf_ex1_index, wf_ex2_index) # Check if descending is working also wf_execs = self.mistral_cli( True, 'execution-list', params="--sort_keys Description --sort_dirs=desc" ) self.assertEqual( set(all_wf_ids), set([ex['ID'] for ex in wf_execs]) ) wf_ex1_index = -1 wf_ex2_index = -1 for idx, ex in enumerate(wf_execs): if ex['ID'] == all_wf_ids[0]: wf_ex1_index = idx elif ex['ID'] == all_wf_ids[1]: wf_ex2_index = idx self.assertGreater(wf_ex1_index, wf_ex2_index) def test_execution_list_with_filter(self): wf_ex1 = self.execution_create( params='{0} -d "a"'.format(self.direct_wf['Name']) ) wf_ex1_id = self.get_field_value(wf_ex1, 'ID') self.execution_create( params='{0} -d "b"'.format(self.direct_wf['Name']) ) # Request a list without filters. wf_execs = self.mistral_cli(True, 'execution-list') self.assertTableStruct( wf_execs, ['ID', 'Workflow name', 'Workflow ID', 'State', 'Created at', 'Updated at'] ) self.assertEqual(2, len(wf_execs)) # Now let's provide a filter. wf_execs = self.mistral_cli( True, 'execution-list', params='--filter description=a' ) self.assertTableStruct( wf_execs, ['ID', 'Workflow name', 'Workflow ID', 'State', 'Created at', 'Updated at'] ) self.assertEqual(1, len(wf_execs)) self.assertEqual(wf_ex1_id, wf_execs[0]['ID']) self.assertEqual('a', wf_execs[0]['Description']) def test_executions_list_with_rootsonly(self): wrapping_wf = self.workflow_create(self.wf_wrapping_wf) wrapping_wf_ex = self.execution_create(wrapping_wf[-1]['Name']) wrapping_wf_ex_id = self.get_field_value(wrapping_wf_ex, 'ID') wf_execs = self.mistral_cli( True, 'execution-list', params="--rootsonly" ) self.assertEqual(1, len(wf_execs)) wf_exec = wf_execs[0] self.assertEqual(wrapping_wf_ex_id, wf_exec['ID']) class CronTriggerCLITests(base_v2.MistralClientTestBase): """Test suite checks commands to work with cron-triggers.""" @classmethod def setUpClass(cls): super(CronTriggerCLITests, cls).setUpClass() def setUp(self): super(CronTriggerCLITests, self).setUp() wf = self.workflow_create(self.wf_def) self.wf_name = wf[0]['Name'] def test_cron_trigger_create_delete(self): trigger = self.mistral_admin( 'cron-trigger-create', params=('trigger %s {} --pattern "5 * * * *" --count 5' ' --first-time "4242-12-25 13:37" --utc' % self.wf_name) ) self.assertTableStruct(trigger, ['Field', 'Value']) tr_name = self.get_field_value(trigger, 'Name') wf_name = self.get_field_value(trigger, 'Workflow') created_at = self.get_field_value(trigger, 'Created at') remain = self.get_field_value(trigger, 'Remaining executions') next_time = self.get_field_value(trigger, 'Next execution time') self.assertEqual('trigger', tr_name) self.assertEqual(self.wf_name, wf_name) self.assertIsNotNone(created_at) self.assertEqual("4242-12-25 13:37:00", next_time) self.assertEqual("5", remain) triggers = self.mistral_admin('cron-trigger-list') self.assertIn(tr_name, [tr['Name'] for tr in triggers]) self.assertIn(wf_name, [tr['Workflow'] for tr in triggers]) self.mistral('cron-trigger-delete', params=tr_name) triggers = self.mistral_admin('cron-trigger-list') self.assertNotIn(tr_name, [tr['Name'] for tr in triggers]) def test_two_cron_triggers_for_one_wf(self): self.cron_trigger_create('trigger1', self.wf_name, '{}', "5 * * * *") self.cron_trigger_create('trigger2', self.wf_name, '{}', "15 * * * *") triggers = self.mistral_admin('cron-trigger-list') self.assertIn("trigger1", [tr['Name'] for tr in triggers]) self.assertIn("trigger2", [tr['Name'] for tr in triggers]) def test_cron_trigger_get(self): trigger = self.cron_trigger_create( 'trigger', self.wf_name, '{}', "5 * * * *" ) self.assertTableStruct(trigger, ['Field', 'Value']) fetched_tr = self.mistral_admin( 'cron-trigger-get', params='trigger' ) self.assertTableStruct(trigger, ['Field', 'Value']) tr_name = self.get_field_value(fetched_tr, 'Name') wf_name = self.get_field_value(fetched_tr, 'Workflow') created_at = self.get_field_value(fetched_tr, 'Created at') self.assertEqual('trigger', tr_name) self.assertEqual(self.wf_name, wf_name) self.assertIsNotNone(created_at) class EventTriggerCLITests(base_v2.MistralClientTestBase): """Test suite checks commands to work with event-triggers.""" @classmethod def setUpClass(cls): super(EventTriggerCLITests, cls).setUpClass() def setUp(self): super(EventTriggerCLITests, self).setUp() wf = self.workflow_create(self.wf_def) self.wf_id = wf[0]['ID'] def test_event_trigger_create_delete(self): trigger = self.mistral_admin( 'event-trigger-create', params=('trigger %s dummy_exchange dummy_topic event.dummy {}' % self.wf_id)) self.assertTableStruct(trigger, ['Field', 'Value']) tr_id = self.get_field_value(trigger, 'ID') tr_name = self.get_field_value(trigger, 'Name') wf_id = self.get_field_value(trigger, 'Workflow ID') created_at = self.get_field_value(trigger, 'Created at') self.assertEqual('trigger', tr_name) self.assertEqual(self.wf_id, wf_id) self.assertIsNotNone(created_at) triggers = self.mistral_admin('event-trigger-list') self.assertIn(tr_name, [tr['Name'] for tr in triggers]) self.assertIn(wf_id, [tr['Workflow ID'] for tr in triggers]) self.mistral('event-trigger-delete', params=tr_id) triggers = self.mistral_admin('event-trigger-list') self.assertNotIn(tr_name, [tr['Name'] for tr in triggers]) def test_two_event_triggers_for_one_wf(self): self.event_trigger_create('trigger1', self.wf_id, 'dummy_exchange', 'dummy_topic', 'event.dummy', '{}') self.event_trigger_create('trigger2', self.wf_id, 'dummy_exchange', 'dummy_topic', 'dummy.event', '{}') triggers = self.mistral_admin('event-trigger-list') self.assertIn('trigger1', [tr['Name'] for tr in triggers]) self.assertIn('trigger2', [tr['Name'] for tr in triggers]) def test_event_trigger_get(self): trigger = self.event_trigger_create('trigger', self.wf_id, 'dummy_exchange', 'dummy_topic', 'event.dummy.other', '{}') self.assertTableStruct(trigger, ['Field', 'Value']) ev_tr_id = self.get_field_value(trigger, 'ID') fetched_tr = self.mistral_admin('event-trigger-get', params=ev_tr_id) self.assertTableStruct(trigger, ['Field', 'Value']) tr_name = self.get_field_value(fetched_tr, 'Name') wf_id = self.get_field_value(fetched_tr, 'Workflow ID') created_at = self.get_field_value(fetched_tr, 'Created at') self.assertEqual('trigger', tr_name) self.assertEqual(self.wf_id, wf_id) self.assertIsNotNone(created_at) class TaskCLITests(base_v2.MistralClientTestBase): """Test suite checks commands to work with tasks.""" def setUp(self): super(TaskCLITests, self).setUp() wfs = self.workflow_create(self.wf_def) self.direct_wf = wfs[0] self.reverse_wf = wfs[1] self.create_file('input', '{\n "farewell": "Bye"\n}\n') self.create_file('task_name', '{\n "task_name": "goodbye"\n}\n') def test_task_get(self): wf_ex = self.execution_create(self.direct_wf['Name']) wf_ex_id = self.get_field_value(wf_ex, 'ID') tasks = self.mistral_admin('task-list', params=wf_ex_id) created_task_id = tasks[-1]['ID'] fetched_task = self.mistral_admin('task-get', params=created_task_id) fetched_task_id = self.get_field_value(fetched_task, 'ID') fetched_task_wf_namespace = self.get_field_value( fetched_task, 'Workflow namespace' ) task_execution_id = self.get_field_value(fetched_task, 'Workflow Execution ID') self.assertEqual(created_task_id, fetched_task_id) self.assertEqual('', fetched_task_wf_namespace) self.assertEqual(wf_ex_id, task_execution_id) def test_task_get_list_within_namespace(self): namespace = 'aaa' self.workflow_create(self.wf_def, namespace=namespace) wf_ex = self.execution_create( self.direct_wf['Name'] + ' --namespace ' + namespace ) wf_ex_id = self.get_field_value(wf_ex, 'ID') tasks = self.mistral_admin('task-list', params=wf_ex_id) created_task_id = tasks[-1]['ID'] created_wf_namespace = tasks[-1]['Workflow namespace'] fetched_task = self.mistral_admin('task-get', params=created_task_id) fetched_task_id = self.get_field_value(fetched_task, 'ID') fetched_task_wf_namespace = self.get_field_value( fetched_task, 'Workflow namespace' ) task_execution_id = self.get_field_value(fetched_task, 'Workflow Execution ID') self.assertEqual(created_task_id, fetched_task_id) self.assertEqual(namespace, created_wf_namespace) self.assertEqual(created_wf_namespace, fetched_task_wf_namespace) self.assertEqual(wf_ex_id, task_execution_id) def test_task_list_with_filter(self): wf_exec = self.execution_create( "%s input task_name" % self.reverse_wf['Name'] ) exec_id = self.get_field_value(wf_exec, 'ID') self.assertTrue(self.wait_execution_success(exec_id)) # Request task executions without filters. tasks = self.parser.listing(self.mistral('task-list')) self.assertTableStruct( tasks, ['ID', 'Name', 'Workflow name', 'Workflow Execution ID', 'State'] ) self.assertEqual(2, len(tasks)) # Now let's provide a filter. tasks = self.parser.listing( self.mistral( 'task-list', params='--filter name=goodbye' ) ) self.assertTableStruct( tasks, ['ID', 'Name', 'Workflow name', 'Workflow Execution ID', 'State'] ) self.assertEqual(1, len(tasks)) self.assertEqual('goodbye', tasks[0]['Name']) def test_task_list_with_limit(self): wf_exec = self.execution_create( "%s input task_name" % self.reverse_wf['Name'] ) exec_id = self.get_field_value(wf_exec, 'ID') self.assertTrue(self.wait_execution_success(exec_id)) tasks = self.parser.listing(self.mistral('task-list')) tasks = self.parser.listing( self.mistral( 'task-list', params='--limit 1' ) ) self.assertEqual(1, len(tasks)) class ActionCLITests(base_v2.MistralClientTestBase): """Test suite checks commands to work with actions.""" @classmethod def setUpClass(cls): super(ActionCLITests, cls).setUpClass() def test_action_create_delete(self): init_acts = self.mistral_admin('action-create', params=self.act_def) self.assertTableStruct( init_acts, [ 'Name', 'Is system', 'Input', 'Description', 'Tags', 'Created at', 'Updated at' ] ) self.assertIn('greeting', [action['Name'] for action in init_acts]) self.assertIn('farewell', [action['Name'] for action in init_acts]) action_1 = self.get_item_info( get_from=init_acts, get_by='Name', value='greeting' ) action_2 = self.get_item_info( get_from=init_acts, get_by='Name', value='farewell' ) self.assertEqual('', action_1['Tags']) self.assertEqual('', action_2['Tags']) self.assertEqual('False', action_1['Is system']) self.assertEqual('False', action_2['Is system']) self.assertEqual('name', action_1['Input']) self.assertEqual('None', action_2['Input']) acts = self.mistral_admin('action-list') self.assertIn(action_1['Name'], [action['Name'] for action in acts]) self.assertIn(action_2['Name'], [action['Name'] for action in acts]) self.mistral_admin( 'action-delete', params='{0}'.format(action_1['Name']) ) self.mistral_admin( 'action-delete', params='{0}'.format(action_2['Name']) ) acts = self.mistral_admin('action-list') self.assertNotIn(action_1['Name'], [action['Name'] for action in acts]) self.assertNotIn(action_2['Name'], [action['Name'] for action in acts]) def test_action_update(self): actions = self.action_create(self.act_def) created_action = self.get_item_info( get_from=actions, get_by='Name', value='greeting' ) actions = self.mistral_admin('action-update', params=self.act_def) updated_action = self.get_item_info( get_from=actions, get_by='Name', value='greeting' ) self.assertEqual( created_action['Created at'].split(".")[0], updated_action['Created at'] ) self.assertEqual(created_action['Name'], updated_action['Name']) self.assertEqual( created_action['Updated at'], updated_action['Updated at'] ) actions = self.mistral_admin('action-update', params=self.act_tag_def) updated_action = self.get_item_info( get_from=actions, get_by='Name', value='greeting' ) self.assertEqual('tag, tag1', updated_action['Tags']) self.assertEqual( created_action['Created at'].split(".")[0], updated_action['Created at'] ) self.assertEqual(created_action['Name'], updated_action['Name']) self.assertNotEqual( created_action['Updated at'], updated_action['Updated at'] ) def test_action_update_with_id(self): acts = self.action_create(self.act_def) created_action = self.get_item_info( get_from=acts, get_by='Name', value='greeting' ) action_id = created_action['ID'] params = '{0} --id {1}'.format(self.act_tag_def, action_id) acts = self.mistral_admin('action-update', params=params) updated_action = self.get_item_info( get_from=acts, get_by='ID', value=action_id ) self.assertEqual( created_action['Created at'].split(".")[0], updated_action['Created at'] ) self.assertEqual(created_action['Name'], updated_action['Name']) self.assertNotEqual( created_action['Updated at'], updated_action['Updated at'] ) def test_action_update_truncate_input(self): input_value = "very_long_input_parameter_name_that_should_be_truncated" act_def = """ version: "2.0" action1: input: - {0} base: std.noop """.format(input_value) self.create_file('action.yaml', act_def) self.action_create('action.yaml') updated_act = self.mistral_admin('action-update', params='action.yaml') updated_act_info = self.get_item_info( get_from=updated_act, get_by='Name', value='action1' ) self.assertEqual(updated_act_info['Input'][:-3], input_value[:25]) def test_action_get_definition(self): self.action_create(self.act_def) definition = self.mistral_admin( 'action-get-definition', params='greeting' ) self.assertNotIn('404 Not Found', definition) def test_action_get_definition_with_namespace(self): self.action_create(self.act_def) definition = self.mistral_admin( 'action-get-definition', params='greeting --namespace test_namespace' ) self.assertNotIn('404 Not Found', definition) def test_action_get_with_name(self): created = self.action_create(self.act_def) action_name = created[0]['Name'] fetched = self.mistral_admin('action-get', params=action_name) fetched_action_name = self.get_field_value(fetched, 'Name') self.assertEqual(action_name, fetched_action_name) def test_action_list_with_filter(self): actions = self.parser.listing(self.mistral('action-list')) self.assertTableStruct( actions, ['Name', 'Is system', 'Input', 'Description', 'Tags', 'Created at', 'Updated at'] ) # NOTE(rakhmerov): This length isn't really a number of actions. # The problem is that one entity in a table may be on more than # one lines depending on their data. For example, for the # workflows that we use in our tests it works fine and parsing # algorithm is able to parse entities correctly even if they are # on multiple lines, but for actions it doesn't. So the only thing # we can do is only check if unfiltered table is bigger than # filtered. # We need to think how to improve it. unfiltered_len = len(actions) self.assertGreater(unfiltered_len, 0) # Now let's provide a filter to the list command. actions = self.parser.listing( self.mistral( 'action-list', params='--filter name=in:std.echo,std.noop' ) ) self.assertTableStruct( actions, ['Name', 'Is system', 'Input', 'Description', 'Tags', 'Created at', 'Updated at'] ) self.assertGreater(unfiltered_len, len(actions)) action_names = [a['Name'] for a in actions] self.assertIn('std.echo', action_names) self.assertIn('std.noop', action_names) self.assertNotIn('std.ssh', action_names) class EnvironmentCLITests(base_v2.MistralClientTestBase): """Test suite checks commands to work with environments.""" def setUp(self): super(EnvironmentCLITests, self).setUp() self.create_file( 'env.yaml', 'name: env\n' 'description: Test env\n' 'variables:\n' ' var: "value"' ) def test_environment_create(self): env = self.mistral_admin('environment-create', params='env.yaml') env_name = self.get_field_value(env, 'Name') env_desc = self.get_field_value(env, 'Description') self.assertTableStruct(env, ['Field', 'Value']) envs = self.mistral_admin('environment-list') self.assertIn(env_name, [en['Name'] for en in envs]) self.assertIn(env_desc, [en['Description'] for en in envs]) self.mistral_admin('environment-delete', params=env_name) envs = self.mistral_admin('environment-list') self.assertNotIn(env_name, [en['Name'] for en in envs]) def test_environment_create_without_description(self): self.create_file( 'env_without_des.yaml', 'name: env\n' 'variables:\n' ' var: "value"' ) env = self.mistral_admin( 'environment-create', params='env_without_des.yaml' ) env_name = self.get_field_value(env, 'Name') env_desc = self.get_field_value(env, 'Description') self.assertTableStruct(env, ['Field', 'Value']) envs = self.mistral_admin('environment-list') self.assertIn(env_name, [en['Name'] for en in envs]) self.assertIn(env_desc, 'None') self.mistral_admin('environment-delete', params='env') envs = self.mistral_admin('environment-list') self.assertNotIn(env_name, [en['Name'] for en in envs]) def test_environment_update(self): env = self.environment_create('env.yaml') env_name = self.get_field_value(env, 'Name') env_desc = self.get_field_value(env, 'Description') env_created_at = self.get_field_value(env, 'Created at') env_updated_at = self.get_field_value(env, 'Updated at') self.assertIsNotNone(env_created_at) self.assertEqual('None', env_updated_at) self.create_file( 'env_upd.yaml', 'name: env\n' 'description: Updated env\n' 'variables:\n' ' var: "value"' ) env = self.mistral_admin('environment-update', params='env_upd.yaml') self.assertTableStruct(env, ['Field', 'Value']) updated_env_name = self.get_field_value(env, 'Name') updated_env_desc = self.get_field_value(env, 'Description') updated_env_created_at = self.get_field_value(env, 'Created at') updated_env_updated_at = self.get_field_value(env, 'Updated at') self.assertEqual(env_name, updated_env_name) self.assertNotEqual(env_desc, updated_env_desc) self.assertEqual('Updated env', updated_env_desc) self.assertEqual(env_created_at.split('.')[0], updated_env_created_at) self.assertIsNotNone(updated_env_updated_at) def test_environment_get(self): env = self.environment_create('env.yaml') env_name = self.get_field_value(env, 'Name') env_desc = self.get_field_value(env, 'Description') env = self.mistral_admin('environment-get', params=env_name) fetched_env_name = self.get_field_value(env, 'Name') fetched_env_desc = self.get_field_value(env, 'Description') self.assertTableStruct(env, ['Field', 'Value']) self.assertEqual(env_name, fetched_env_name) self.assertEqual(env_desc, fetched_env_desc) def test_environment_get_export(self): env = self.environment_create('env.yaml') env_name = self.get_field_value(env, 'Name') env_desc = self.get_field_value(env, 'Description') env = self.mistral_admin('environment-get', params='--export {0}'.format(env_name)) fetched_env_name = self.get_field_value(env, 'name') fetched_env_desc = self.get_field_value(env, 'description') self.assertTableStruct(env, ['Field', 'Value']) self.assertEqual(env_name, fetched_env_name) self.assertEqual(env_desc, fetched_env_desc) class ActionExecutionCLITests(base_v2.MistralClientTestBase): """Test suite checks commands to work with action executions.""" def setUp(self): super(ActionExecutionCLITests, self).setUp() wfs = self.workflow_create(self.wf_def) self.direct_wf = wfs[0] direct_wf_exec = self.execution_create(self.direct_wf['Name']) self.direct_ex_id = self.get_field_value(direct_wf_exec, 'ID') def test_act_execution_get(self): self.wait_execution_success(self.direct_ex_id) task = self.mistral_admin('task-list', params=self.direct_ex_id)[0] act_ex_from_list = self.mistral_admin( 'action-execution-list', params=task['ID'] )[0] act_ex = self.mistral_admin( 'action-execution-get', params=act_ex_from_list['ID'] ) wf_name = self.get_field_value(act_ex, 'Workflow name') state = self.get_field_value(act_ex, 'State') self.assertEqual( act_ex_from_list['ID'], self.get_field_value(act_ex, 'ID') ) self.assertEqual(self.direct_wf['Name'], wf_name) self.assertEqual('SUCCESS', state) def test_act_execution_list_with_limit(self): self.wait_execution_success(self.direct_ex_id) act_execs = self.mistral_admin('action-execution-list') # The workflow execution started in setUp() # generates 2 action executions. self.assertGreater(len(act_execs), 1) act_execs = self.mistral_admin( 'action-execution-list', params="--limit 1" ) self.assertEqual(len(act_execs), 1) act_ex = act_execs[0] self.assertEqual(self.direct_wf['Name'], act_ex['Workflow name']) self.assertEqual('SUCCESS', act_ex['State']) def test_act_execution_get_list_within_namespace(self): namespace = 'bbb' self.workflow_create(self.wf_def, namespace=namespace) wf_ex = self.execution_create( self.direct_wf['Name'] + ' --namespace ' + namespace ) exec_id = self.get_field_value(wf_ex, 'ID') self.wait_execution_success(exec_id) task = self.mistral_admin('task-list', params=exec_id)[0] act_ex_from_list = self.mistral_admin( 'action-execution-list', params=task['ID'] )[0] act_ex = self.mistral_admin( 'action-execution-get', params=act_ex_from_list['ID'] ) wf_name = self.get_field_value(act_ex, 'Workflow name') wf_namespace = self.get_field_value(act_ex, 'Workflow namespace') status = self.get_field_value(act_ex, 'State') self.assertEqual( act_ex_from_list['ID'], self.get_field_value(act_ex, 'ID') ) self.assertEqual(self.direct_wf['Name'], wf_name) self.assertEqual('SUCCESS', status) self.assertEqual(namespace, wf_namespace) self.assertEqual(namespace, act_ex_from_list['Workflow namespace']) def test_act_execution_create_delete(self): action_ex = self.mistral_admin( 'run-action', params="std.echo '{0}' --save-result".format( '{"output": "Hello!"}') ) action_ex_id = self.get_field_value(action_ex, 'ID') self.assertTableStruct(action_ex, ['Field', 'Value']) name = self.get_field_value(action_ex, 'Name') wf_name = self.get_field_value(action_ex, 'Workflow name') task_name = self.get_field_value(action_ex, 'Task name') self.assertEqual('std.echo', name) self.assertEqual('None', wf_name) self.assertEqual('None', task_name) action_exs = self.mistral_admin('action-execution-list') self.assertIn(action_ex_id, [ex['ID'] for ex in action_exs]) self.mistral_admin('action-execution-delete', params=action_ex_id) action_exs = self.mistral_admin('action-execution-list') self.assertNotIn(action_ex_id, [ex['ID'] for ex in action_exs]) class NegativeCLITests(base_v2.MistralClientTestBase): """This class contains negative tests.""" def test_wb_list_extra_param(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-list', params='param' ) def test_wb_get_unexist_wb(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-get', params='wb' ) def test_wb_get_without_param(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-get' ) def test_wb_create_same_name(self): self.workbook_create(self.wb_def) self.assertRaises( exceptions.CommandFailed, self.workbook_create, self.wb_def ) def test_wb_create_with_wrong_path_to_definition(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook_create', 'wb' ) def test_wb_delete_unexist_wb(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-delete', params='wb' ) def test_wb_update_wrong_path_to_def(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-update', params='wb' ) def test_wb_update_nonexistant_wb(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-update', params=self.wb_with_tags_def ) def test_wb_create_empty_def(self): self.create_file('empty') self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-create', params='empty' ) def test_wb_update_empty_def(self): self.create_file('empty') self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-update', params='empty' ) def test_wb_get_definition_unexist_wb(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-get-definition', params='wb' ) def test_wb_create_invalid_def(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-create', params=self.wf_def ) def test_wb_update_invalid_def(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-update', params=self.wf_def ) def test_wb_update_without_def(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workbook-update' ) def test_wf_list_extra_param(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-list', params='param' ) def test_wf_get_unexist_wf(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-get', params='wf' ) def test_wf_get_without_param(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-get' ) def test_wf_create_without_definition(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-create', params='' ) def test_wf_create_with_wrong_path_to_definition(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-create', params='wf' ) def test_wf_delete_unexist_wf(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-delete', params='wf' ) def test_wf_update_unexist_wf(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-update', params='wf' ) def test_wf_get_definition_unexist_wf(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-get-definition', params='wf' ) def test_wf_get_definition_missed_param(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-get-definition' ) def test_wf_create_invalid_def(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-create', params=self.wb_def ) def test_wf_update_invalid_def(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-update', params=self.wb_def ) def test_wf_create_empty_def(self): self.create_file('empty') self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-create', params='empty' ) def test_wf_update_empty_def(self): self.create_file('empty') self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'workflow-update', params='empty' ) def test_ex_list_extra_param(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'execution-list', params='param' ) def test_ex_create_unexist_wf(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'execution-create', params='wf' ) def test_ex_create_unexist_task(self): wf = self.workflow_create(self.wf_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'execution-create', params='%s param {}' % wf[0]['Name'] ) def test_ex_create_with_invalid_input(self): wf = self.workflow_create(self.wf_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'execution-create', params="%s input" % wf[0]['Name'] ) def test_ex_get_nonexist_execution(self): wf = self.workflow_create(self.wf_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'execution-get', params='%s id' % wf[0]['Name'] ) def test_ex_create_without_wf_name(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'execution-create' ) def test_ex_create_reverse_wf_without_start_task(self): wf = self.workflow_create(self.wf_def) self.create_file('input', '{\n "farewell": "Bye"\n}\n') self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'execution-create ', params=wf[1]['Name'] ) def test_ex_create_missed_input(self): self.create_file('empty') wf = self.workflow_create(self.wf_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'execution-create empty', params=wf[1]['Name'] ) def test_ex_update_both_state_and_description(self): wf = self.workflow_create(self.wf_def) execution = self.execution_create(params=wf[0]['Name']) exec_id = self.get_field_value(execution, 'ID') self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'execution-update', params='%s -s ERROR -d update' % exec_id ) def test_ex_delete_nonexistent_execution(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'execution-delete', params='1a2b3c' ) def test_tr_create_without_pattern(self): wf = self.workflow_create(self.wf_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'cron-trigger-create', params='tr %s {}' % wf[0]['Name'] ) def test_tr_create_invalid_pattern(self): wf = self.workflow_create(self.wf_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'cron-trigger-create', params='tr %s {} --pattern "q"' % wf[0]['Name'] ) def test_tr_create_invalid_pattern_value_out_of_range(self): wf = self.workflow_create(self.wf_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'cron-trigger-create', params='tr %s {} --pattern "80 * * * *"' % wf[0]['Name'] ) def test_tr_create_nonexistent_wf(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'cron-trigger-create', params='tr wb.wf1 {} --pattern "* * * * *"' ) def test_tr_delete_nonexistant_tr(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'cron-trigger-delete', params='tr' ) def test_tr_get_nonexistant_tr(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'cron-trigger-get', params='tr' ) def test_tr_create_invalid_count(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'cron-trigger-create', params='tr wb.wf1 {} --pattern "* * * * *" --count q' ) def test_tr_create_negative_count(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'cron-trigger-create', params='tr wb.wf1 {} --pattern "* * * * *" --count -1') def test_tr_create_invalid_first_date(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'cron-trigger-create', params='tr wb.wf1 {} --pattern "* * * * *" --first-date "q"' ) def test_tr_create_count_only(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'cron-trigger-create', params='tr wb.wf1 {} --count 42' ) def test_tr_create_date_and_count_without_pattern(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'cron-trigger-create', params='tr wb.wf1 {} --count 42 --first-time "4242-12-25 13:37"' ) def test_event_tr_create_missing_argument(self): wf = self.workflow_create(self.wf_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'event-trigger-create', params='tr %s exchange topic' % wf[0]['ID'] ) def test_event_tr_create_nonexistent_wf(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'event-trigger-create', params='456 4307362e-4a4a-4021-aa58-0fab23c9c751 ' 'exchange topic event {} ' ) def test_event_tr_delete_nonexistent_tr(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'event-trigger-delete', params='789' ) def test_event_tr_get_nonexistent_tr(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'event-trigger-get', params='789' ) def test_action_get_nonexistent(self): self.assertRaises( exceptions.CommandFailed, self.mistral, 'action-get', params='nonexist' ) def test_action_double_creation(self): self.action_create(self.act_def) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'action-create', params='{0}'.format(self.act_def) ) def test_action_create_without_def(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'action-create', params='' ) def test_action_create_invalid_def(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'action-create', params='{0}'.format(self.wb_def) ) def test_action_delete_nonexistent_act(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'action-delete', params='nonexist' ) def test_action_delete_standard_action(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'action-delete', params='heat.events_get' ) def test_action_get_definition_nonexistent_action(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'action-get-definition', params='nonexist' ) def test_task_get_nonexistent_task(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'task-get', params='nonexist' ) def test_env_get_without_param(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'environment-get' ) def test_env_get_nonexistent(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'environment-get', params='nonexist' ) def test_env_create_same_name(self): self.create_file( 'env.yaml', 'name: env\n' 'description: Test env\n' 'variables:\n' ' var: "value"' ) self.environment_create('env.yaml') self.assertRaises( exceptions.CommandFailed, self.environment_create, 'env.yaml' ) def test_env_create_empty(self): self.create_file('env.yaml') self.assertRaises( exceptions.CommandFailed, self.environment_create, 'env.yaml' ) def test_env_create_with_wrong_path_to_definition(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'execution_create', 'env' ) def test_env_delete_unexist_env(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'environment-delete', params='env' ) def test_env_update_wrong_path_to_def(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'environment-update', params='env' ) def test_env_update_empty(self): self.create_file('env.yaml') self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'environment-update', params='env' ) def test_env_update_nonexistant_env(self): self.create_file( 'env.yaml', 'name: env' 'variables:\n var: "value"' ) self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'environment-update', params='env.yaml' ) def test_env_create_without_name(self): self.create_file('env.yaml', 'variables:\n var: "value"') self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'environment-create', params='env.yaml' ) def test_env_create_without_variables(self): self.create_file('env.yaml', 'name: env') self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'environment-create', params='env.yaml' ) def test_action_execution_get_without_params(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'action-execution-get' ) def test_action_execution_get_unexistent_obj(self): self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'action-execution-get', params='123456' ) def test_action_execution_update(self): wfs = self.workflow_create(self.wf_def) direct_wf_exec = self.execution_create(wfs[0]['Name']) direct_ex_id = self.get_field_value(direct_wf_exec, 'ID') self.assertRaises( exceptions.CommandFailed, self.mistral_admin, 'action-execution-update', params='%s ERROR' % direct_ex_id ) def test_target_action_execution(self): command = ( '--debug ' '--os-target-tenant-name={tenantname} ' '--os-target-username={username} ' '--os-target-password="{password}" ' '--os-target-auth-url="{auth_url}" ' '--target_insecure ' 'run-action std.noop' ).format( tenantname=self.clients.tenant_name, username=self.clients.username, password=self.clients.password, auth_url=self.clients.uri ) self.mistral_alt_user(cmd=command) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8525467 python-mistralclient-5.3.0/mistralclient/tests/unit/0000775000175000017500000000000000000000000022726 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/__init__.py0000664000175000017500000000000000000000000025025 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/base.py0000664000175000017500000000244200000000000024214 0ustar00zuulzuul00000000000000# Copyright 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 unittest import mock from oslotest import base from requests_mock.contrib import fixture class BaseClientTest(base.BaseTestCase): _client = None def setUp(self): super(BaseClientTest, self).setUp() self.requests_mock = self.useFixture(fixture.Fixture()) class BaseCommandTest(base.BaseTestCase): def setUp(self): super(BaseCommandTest, self).setUp() self.app = mock.Mock() self.client = self.app.client_manager.workflow_engine def call(self, command, app_args=(), prog_name=''): cmd = command(self.app, app_args) parsed_args = cmd.get_parser(prog_name).parse_args(app_args) return cmd.take_action(parsed_args) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/base_shell_test.py0000664000175000017500000000272400000000000026445 0ustar00zuulzuul00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 os import sys from oslotest import base from mistralclient import shell class BaseShellTests(base.BaseTestCase): def shell(self, argstr): orig = (sys.stdout, sys.stderr) clean_env = {} _old_env, os.environ = os.environ, clean_env.copy() try: sys.stdout = io.StringIO() sys.stderr = io.StringIO() _shell = shell.MistralShell() _shell.run(argstr.split()) except SystemExit: exc_type, exc_value, exc_traceback = sys.exc_info() self.assertEqual(0, exc_value.code) finally: stdout = sys.stdout.getvalue() stderr = sys.stderr.getvalue() sys.stdout.close() sys.stderr.close() sys.stdout, sys.stderr = orig os.environ = _old_env return stdout, stderr ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8565469 python-mistralclient-5.3.0/mistralclient/tests/unit/resources/0000775000175000017500000000000000000000000024740 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/resources/action_v2.yaml0000664000175000017500000000016300000000000027510 0ustar00zuulzuul00000000000000 --- version: 2.0 my_action: base: std.echo base-input: output: 'Bye!' output: info: <% $.output %> ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/resources/ctx.json0000664000175000017500000000012400000000000026426 0ustar00zuulzuul00000000000000{ "context": { "server": { "name": "name" } } } ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/resources/env_v2.json0000664000175000017500000000022000000000000027024 0ustar00zuulzuul00000000000000{ "name": "env1", "description": "Test Environment #1", "scope": "private", "variables": { "server": "localhost" } }././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/resources/env_v2.yaml0000664000175000017500000000016200000000000027022 0ustar00zuulzuul00000000000000--- "name": "env1" "description": "Test Environment #1" "scope": "private" "variables": "server": "localhost"././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/resources/wb_v2.yaml0000664000175000017500000000046300000000000026646 0ustar00zuulzuul00000000000000 --- version: 2.0 name: wb workflows: wf1: type: direct input: - param1 - param2 tasks: task1: action: std.http url="localhost:8989" on-success: - test_subsequent test_subsequent: action: std.http url="http://some_url" server_id=1 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/resources/wf_v2.yaml0000664000175000017500000000015300000000000026646 0ustar00zuulzuul00000000000000 --- version: 2.0 my_wf: type: direct tasks: task1: action: std.echo output="hello, world" ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/test_client.py0000664000175000017500000002602700000000000025624 0ustar00zuulzuul00000000000000# Copyright 2015 - Huawei Technologies Co., Ltd. # Copyright 2016 - StackStorm, 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 tempfile from unittest import mock from oslo_serialization import jsonutils from oslo_utils import uuidutils from oslotest import base import osprofiler.profiler from mistralclient.api import client AUTH_HTTP_URL_v3 = 'http://localhost:35357/v3' AUTH_HTTP_URL_v2_0 = 'http://localhost:35357/v2.0' AUTH_HTTPS_URL = AUTH_HTTP_URL_v3.replace('http', 'https') MISTRAL_HTTP_URL = 'http://localhost:8989/v2' MISTRAL_HTTPS_URL = MISTRAL_HTTP_URL.replace('http', 'https') PROFILER_HMAC_KEY = 'SECRET_HMAC_KEY' class BaseClientTest(base.BaseTestCase): @staticmethod def setup_keystone_mock(session_mock): keystone_client_instance = session_mock.return_value keystone_client_instance.auth_token = uuidutils.generate_uuid() keystone_client_instance.project_id = uuidutils.generate_uuid() keystone_client_instance.user_id = uuidutils.generate_uuid() keystone_client_instance.auth_ref = str(jsonutils.dumps({})) return keystone_client_instance @mock.patch('keystoneauth1.session.Session') def test_mistral_url_from_catalog_v2(self, session_mock): session = mock.Mock() session_mock.side_effect = [session] get_endpoint = mock.Mock(return_value='http://mistral_host:8989/v2') session.get_endpoint = get_endpoint mistralclient = client.client( username='mistral', project_name='mistral', api_key='password', auth_url=AUTH_HTTP_URL_v2_0, service_type='workflowv2' ) self.assertEqual( 'http://mistral_host:8989/v2', mistralclient.actions.http_client.base_url ) @mock.patch('keystoneauth1.session.Session') def test_mistral_url_from_catalog(self, session_mock): session = mock.Mock() session_mock.side_effect = [session] get_endpoint = mock.Mock(return_value='http://mistral_host:8989/v2') session.get_endpoint = get_endpoint mistralclient = client.client( username='mistral', project_name='mistral', api_key='password', user_domain_name='Default', project_domain_name='Default', auth_url=AUTH_HTTP_URL_v3, service_type='workflowv2' ) self.assertEqual( 'http://mistral_host:8989/v2', mistralclient.actions.http_client.base_url ) @mock.patch('keystoneauth1.session.Session') @mock.patch('mistralclient.api.httpclient.HTTPClient') def test_mistral_url_default(self, http_client_mock, session_mock): session = mock.Mock() session_mock.side_effect = [session] get_endpoint = mock.Mock(side_effect=Exception) session.get_endpoint = get_endpoint client.client( username='mistral', project_name='mistral', api_key='password', user_domain_name='Default', project_domain_name='Default', auth_url=AUTH_HTTP_URL_v3 ) self.assertTrue(http_client_mock.called) mistral_url_for_http = http_client_mock.call_args[0][0] self.assertEqual(MISTRAL_HTTP_URL, mistral_url_for_http) @mock.patch('mistralclient.auth.keystone.KeystoneAuthHandler' '._is_service_catalog_v2', return_value=True) @mock.patch('keystoneauth1.identity.generic.Password') @mock.patch('keystoneauth1.session.Session') @mock.patch('mistralclient.api.httpclient.HTTPClient') def test_target_parameters_processed( self, http_client_mock, session_mock, password_mock, catalog_type_mock ): session = mock.MagicMock() target_session = mock.MagicMock() session_mock.side_effect = [session, target_session] auth = mock.MagicMock() target_auth = mock.MagicMock() target_auth._plugin.auth_url = AUTH_HTTP_URL_v3 password_mock.side_effect = [auth, target_auth] get_endpoint = mock.Mock(return_value='http://mistral_host:8989/v2') session.get_endpoint = get_endpoint target_session.get_project_id = mock.Mock(return_value='projectid') target_session.get_user_id = mock.Mock(return_value='userid') target_session.get_auth_headers = mock.Mock(return_value={ 'X-Auth-Token': 'authtoken' }) mock_access = mock.MagicMock() mock_catalog = mock.MagicMock() mock_catalog.catalog = {} mock_access.service_catalog = mock_catalog auth.get_access = mock.Mock(return_value=mock_access) client.client( auth_url=AUTH_HTTP_URL_v3, username='user', api_key='password', user_domain_name='Default', project_domain_name='Default', target_username='tmistral', target_project_name='tmistralp', target_auth_url=AUTH_HTTP_URL_v3, target_api_key='tpassword', target_user_domain_name='Default', target_project_domain_name='Default', target_region_name='tregion' ) self.assertTrue(http_client_mock.called) mistral_url_for_http = http_client_mock.call_args[0][0] kwargs = http_client_mock.call_args[1] self.assertEqual('http://mistral_host:8989/v2', mistral_url_for_http) expected_values = { 'target_project_id': 'projectid', 'target_auth_token': 'authtoken', 'target_user_id': 'userid', 'target_auth_url': AUTH_HTTP_URL_v3, 'target_project_name': 'tmistralp', 'target_username': 'tmistral', 'target_region_name': 'tregion', 'target_service_catalog': "{}" } for key in expected_values: self.assertEqual(expected_values[key], kwargs[key]) @mock.patch('keystoneauth1.session.Session') @mock.patch('mistralclient.api.httpclient.HTTPClient') def test_mistral_url_https_insecure(self, http_client_mock, session_mock): keystone_client_instance = self.setup_keystone_mock( # noqa session_mock ) expected_args = ( MISTRAL_HTTPS_URL, ) client.client( mistral_url=MISTRAL_HTTPS_URL, username='mistral', project_name='mistral', api_key='password', user_domain_name='Default', project_domain_name='Default', auth_url=AUTH_HTTP_URL_v3, cacert=None, insecure=True ) self.assertTrue(http_client_mock.called) self.assertEqual(http_client_mock.call_args[0], expected_args) self.assertEqual(http_client_mock.call_args[1]['insecure'], True) @mock.patch('keystoneauth1.session.Session') @mock.patch('mistralclient.api.httpclient.HTTPClient') def test_mistral_url_https_secure(self, http_client_mock, session_mock): fd, cert_path = tempfile.mkstemp(suffix='.pem') keystone_client_instance = self.setup_keystone_mock( # noqa session_mock ) expected_args = ( MISTRAL_HTTPS_URL, ) try: client.client( mistral_url=MISTRAL_HTTPS_URL, username='mistral', project_name='mistral', api_key='password', user_domain_name='Default', project_domain_name='Default', auth_url=AUTH_HTTP_URL_v3, cacert=cert_path, insecure=False ) finally: os.close(fd) os.unlink(cert_path) self.assertTrue(http_client_mock.called) self.assertEqual(http_client_mock.call_args[0], expected_args) self.assertEqual(http_client_mock.call_args[1]['cacert'], cert_path) @mock.patch('keystoneauth1.session.Session') def test_mistral_url_https_bad_cacert(self, session_mock): keystone_client_instance = self.setup_keystone_mock( # noqa session_mock ) self.assertRaises( ValueError, client.client, mistral_url=MISTRAL_HTTPS_URL, username='mistral', project_name='mistral', api_key='password', user_domain_name='Default', project_domain_name='Default', auth_url=AUTH_HTTP_URL_v3, cacert='/path/to/foobar', insecure=False ) @mock.patch('logging.Logger.warning') @mock.patch('keystoneauth1.session.Session') def test_mistral_url_https_bad_insecure(self, session_mock, log_warning_mock): fd, path = tempfile.mkstemp(suffix='.pem') keystone_client_instance = self.setup_keystone_mock( session_mock ) try: client.client( mistral_url=MISTRAL_HTTPS_URL, user_id=keystone_client_instance.user_id, project_id=keystone_client_instance.project_id, api_key='password', user_domain_name='Default', project_domain_name='Default', auth_url=AUTH_HTTP_URL_v3, cacert=path, insecure=True ) finally: os.close(fd) os.unlink(path) self.assertTrue(log_warning_mock.called) @mock.patch('keystoneauth1.session.Session') @mock.patch('mistralclient.api.httpclient.HTTPClient') def test_mistral_profile_enabled(self, http_client_mock, session_mock): keystone_client_instance = self.setup_keystone_mock( # noqa session_mock ) client.client( username='mistral', project_name='mistral', api_key='password', user_domain_name='Default', project_domain_name='Default', auth_url=AUTH_HTTP_URL_v3, profile=PROFILER_HMAC_KEY ) self.assertTrue(http_client_mock.called) profiler = osprofiler.profiler.get() self.assertEqual(profiler.hmac_key, PROFILER_HMAC_KEY) @mock.patch('mistralclient.auth.get_auth_handler') def test_mistral_no_auth(self, get_auth_handler_mock): # Test that we don't authenticate if auth url wasn't provided client.client( username='mistral', project_name='mistral', api_key='password', service_type='workflowv2' ) self.assertEqual(0, get_auth_handler_mock.call_count) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/test_httpclient.py0000664000175000017500000002206600000000000026523 0ustar00zuulzuul00000000000000# Copyright 2016 - StackStorm, 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 base64 import copy from unittest import mock from urllib import parse as urlparse from oslo_utils import uuidutils from osprofiler import _utils as osprofiler_utils import osprofiler.profiler from mistralclient.api import httpclient from mistralclient.tests.unit import base API_BASE_URL = 'http://localhost:8989/v2' API_URL = '/executions' EXPECTED_URL = API_BASE_URL + API_URL AUTH_TOKEN = uuidutils.generate_uuid() PROJECT_ID = uuidutils.generate_uuid() USER_ID = uuidutils.generate_uuid() REGION_NAME = 'fake_region' PROFILER_HMAC_KEY = 'SECRET_HMAC_KEY' PROFILER_TRACE_ID = uuidutils.generate_uuid() EXPECTED_AUTH_HEADERS = { 'X-Auth-Token': AUTH_TOKEN, 'X-Project-Id': PROJECT_ID, 'X-User-Id': USER_ID, 'X-Region-Name': REGION_NAME } EXPECTED_REQ_OPTIONS = { 'headers': EXPECTED_AUTH_HEADERS } EXPECTED_BODY = { 'k1': 'abc', 'k2': 123, 'k3': True } class HTTPClientTest(base.BaseClientTest): def setUp(self): super(HTTPClientTest, self).setUp() self.client = httpclient.HTTPClient( API_BASE_URL, auth_token=AUTH_TOKEN, project_id=PROJECT_ID, user_id=USER_ID, region_name=REGION_NAME ) def assertExpectedAuthHeaders(self): headers = self.requests_mock.last_request.headers self.assertEqual(AUTH_TOKEN, headers['X-Auth-Token']) self.assertEqual(PROJECT_ID, headers['X-Project-Id']) self.assertEqual(USER_ID, headers['X-User-Id']) return headers def assertExpectedBody(self): text = self.requests_mock.last_request.text form = urlparse.parse_qs(text, strict_parsing=True) self.assertEqual(len(EXPECTED_BODY), len(form)) for k, v in EXPECTED_BODY.items(): self.assertEqual([str(v)], form[k]) return form def test_get_request_options(self): m = self.requests_mock.get(EXPECTED_URL, text='text') self.client.get(API_URL) self.assertTrue(m.called_once) self.assertExpectedAuthHeaders() @mock.patch.object( osprofiler.profiler._Profiler, 'get_base_id', mock.MagicMock(return_value=PROFILER_TRACE_ID) ) @mock.patch.object( osprofiler.profiler._Profiler, 'get_id', mock.MagicMock(return_value=PROFILER_TRACE_ID) ) def test_get_request_options_with_profile_enabled(self): m = self.requests_mock.get(EXPECTED_URL, text='text') osprofiler.profiler.init(PROFILER_HMAC_KEY) data = {'base_id': PROFILER_TRACE_ID, 'parent_id': PROFILER_TRACE_ID} signed_data = osprofiler_utils.signed_pack(data, PROFILER_HMAC_KEY) headers = { 'X-Trace-Info': signed_data[0], 'X-Trace-HMAC': signed_data[1] } self.client.get(API_URL) self.assertTrue(m.called_once) headers = self.assertExpectedAuthHeaders() self.assertEqual(signed_data[0], headers['X-Trace-Info']) self.assertEqual(signed_data[1], headers['X-Trace-HMAC']) def test_get_request_options_with_headers_for_get(self): m = self.requests_mock.get(EXPECTED_URL, text='text') target_auth_url = uuidutils.generate_uuid() target_auth_token = uuidutils.generate_uuid() target_user_id = 'target_user' target_project_id = 'target_project' target_service_catalog = 'this should be there' target_insecure = 'target insecure' target_region = 'target region name' target_user_domain_name = 'target user domain name' target_project_domain_name = 'target project domain name' target_client = httpclient.HTTPClient( API_BASE_URL, auth_token=AUTH_TOKEN, project_id=PROJECT_ID, user_id=USER_ID, region_name=REGION_NAME, target_auth_url=target_auth_url, target_auth_token=target_auth_token, target_project_id=target_project_id, target_user_id=target_user_id, target_service_catalog=target_service_catalog, target_region_name=target_region, target_user_domain_name=target_user_domain_name, target_project_domain_name=target_project_domain_name, target_insecure=target_insecure ) target_client.get(API_URL) self.assertTrue(m.called_once) headers = self.assertExpectedAuthHeaders() self.assertEqual(target_auth_url, headers['X-Target-Auth-Uri']) self.assertEqual(target_auth_token, headers['X-Target-Auth-Token']) self.assertEqual(target_user_id, headers['X-Target-User-Id']) self.assertEqual(target_project_id, headers['X-Target-Project-Id']) self.assertEqual(str(target_insecure), headers['X-Target-Insecure']) self.assertEqual(target_region, headers['X-Target-Region-Name']) self.assertEqual(target_user_domain_name, headers['X-Target-User-Domain-Name']) self.assertEqual(target_project_domain_name, headers['X-Target-Project-Domain-Name']) catalog = base64.b64encode(target_service_catalog.encode('utf-8')) self.assertEqual(catalog, headers['X-Target-Service-Catalog']) def test_get_request_options_with_headers_for_post(self): m = self.requests_mock.post(EXPECTED_URL, text='text') headers = {'foo': 'bar'} self.client.post(API_URL, EXPECTED_BODY, headers=headers) self.assertTrue(m.called_once) headers = self.assertExpectedAuthHeaders() self.assertEqual('application/json', headers['Content-Type']) self.assertEqual('bar', headers['foo']) self.assertExpectedBody() def test_get_request_options_with_headers_for_put(self): m = self.requests_mock.put(EXPECTED_URL, text='text') headers = {'foo': 'bar'} self.client.put(API_URL, EXPECTED_BODY, headers=headers) self.assertTrue(m.called_once) headers = self.assertExpectedAuthHeaders() self.assertEqual('application/json', headers['Content-Type']) self.assertEqual('bar', headers['foo']) self.assertExpectedBody() def test_get_request_options_with_headers_for_delete(self): m = self.requests_mock.delete(EXPECTED_URL, text='text') headers = {'foo': 'bar'} self.client.delete(API_URL, headers=headers) self.assertTrue(m.called_once) headers = self.assertExpectedAuthHeaders() self.assertEqual('bar', headers['foo']) @mock.patch.object( httpclient.HTTPClient, '_get_request_options', mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS)) ) def test_http_get(self): m = self.requests_mock.get(EXPECTED_URL, text='text') self.client.get(API_URL) httpclient.HTTPClient._get_request_options.assert_called_with( 'get', None ) self.assertTrue(m.called_once) self.assertExpectedAuthHeaders() @mock.patch.object( httpclient.HTTPClient, '_get_request_options', mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS)) ) def test_http_post(self): m = self.requests_mock.post(EXPECTED_URL, status_code=201, text='text') self.client.post(API_URL, EXPECTED_BODY) httpclient.HTTPClient._get_request_options.assert_called_with( 'post', None ) self.assertTrue(m.called_once) self.assertExpectedAuthHeaders() self.assertExpectedBody() @mock.patch.object( httpclient.HTTPClient, '_get_request_options', mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS)) ) def test_http_put(self): m = self.requests_mock.put(EXPECTED_URL, json={}) self.client.put(API_URL, EXPECTED_BODY) httpclient.HTTPClient._get_request_options.assert_called_with( 'put', None ) self.assertTrue(m.called_once) self.assertExpectedAuthHeaders() self.assertExpectedBody() @mock.patch.object( httpclient.HTTPClient, '_get_request_options', mock.MagicMock(return_value=copy.deepcopy(EXPECTED_REQ_OPTIONS)) ) def test_http_delete(self): m = self.requests_mock.delete(EXPECTED_URL, text='text') self.client.delete(API_URL) httpclient.HTTPClient._get_request_options.assert_called_with( 'delete', None ) self.assertTrue(m.called_once) self.assertExpectedAuthHeaders() ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/test_shell.py0000664000175000017500000003221100000000000025445 0ustar00zuulzuul00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock import mistralclient.tests.unit.base_shell_test as base class TestShell(base.BaseShellTests): def test_help(self): """Test that client is not created for help and bash complete""" for command in ('-h', '--help', 'help', 'help workbook-list', 'bash-completion'): with mock.patch('mistralclient.api.client.client') as client_mock: self.shell(command) self.assertFalse(client_mock.called) @mock.patch('mistralclient.api.client.client') def test_command_no_mistral_url(self, client_mock): self.shell( 'workbook-list' ) self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('', params[1]['mistral_url']) @mock.patch('mistralclient.api.client.client') def test_command_interactive_mode(self, client_mock): self.shell('') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('', params[1]['mistral_url']) @mock.patch('mistralclient.api.client.client') def test_command_with_mistral_url(self, client_mock): self.shell( '--os-mistral-url=http://localhost:8989/v2 workbook-list' ) self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('http://localhost:8989/v2', params[1]['mistral_url']) @mock.patch('mistralclient.api.client.determine_client_version') def test_mistral_version(self, client_mock): self.shell( '--os-mistral-version=v1 workbook-list' ) self.assertTrue(client_mock.called) mistral_version = client_mock.call_args self.assertEqual('v1', mistral_version[0][0]) @mock.patch('mistralclient.api.client.determine_client_version') def test_no_mistral_version(self, client_mock): self.shell('workbook-list') self.assertTrue(client_mock.called) mistral_version = client_mock.call_args self.assertEqual('v2', mistral_version[0][0]) @mock.patch('mistralclient.api.client.client') def test_service_type(self, client_mock): self.shell('--os-mistral-service-type=test workbook-list') self.assertTrue(client_mock.called) parmters = client_mock.call_args self.assertEqual('test', parmters[1]['service_type']) @mock.patch('mistralclient.api.client.client') def test_no_service_type(self, client_mock): self.shell('workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('workflowv2', params[1]['service_type']) @mock.patch('mistralclient.api.client.client') def test_endpoint_type(self, client_mock): self.shell('--os-mistral-endpoint-type=adminURL workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('adminURL', params[1]['endpoint_type']) @mock.patch('mistralclient.api.client.client') def test_no_endpoint_type(self, client_mock): self.shell('workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('publicURL', params[1]['endpoint_type']) @mock.patch('mistralclient.api.client.client') def test_auth_url(self, client_mock): self.shell( '--os-auth-url=https://127.0.0.1:35357/v3 ' '--os-username=admin ' '--os-password=1234 ' 'workbook-list' ) self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('https://127.0.0.1:35357/v3', params[1]['auth_url']) @mock.patch('mistralclient.api.client.client') def test_no_auth_url(self, client_mock): self.shell('workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('', params[1]['auth_url']) @mock.patch('mistralclient.api.client.client') def test_default_auth_url_with_os_password(self, client_mock): self.shell('--os-username=admin --os-password=1234 workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('http://localhost:35357/v3', params[1]['auth_url']) @mock.patch('mistralclient.api.client.client') def test_default_auth_url_with_os_auth_token(self, client_mock): self.shell( '--os-auth-token=abcd1234 ' 'workbook-list' ) self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('http://localhost:35357/v3', params[1]['auth_url']) @mock.patch('mistralclient.api.client.client') def test_profile(self, client_mock): self.shell('--profile=SECRET_HMAC_KEY workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('SECRET_HMAC_KEY', params[1]['profile']) @mock.patch('mistralclient.api.client.client') def test_region_name(self, client_mock): self.shell('--os-region-name=RegionOne workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('RegionOne', params[1]['region_name']) @mock.patch('mistralclient.api.client.client') def test_tenant_id_and_tenant_name(self, client_mock): self.shell( '--os-tenant-id=123tenant --os-tenant-name=fake_tenant' ' workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('fake_tenant', params[1]['project_name']) self.assertEqual('123tenant', params[1]['project_id']) @mock.patch('mistralclient.api.client.client') def test_project_id_and_project_name(self, client_mock): self.shell( '--os-project-name=fake_tenant --os-project-id=123tenant' ' workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('fake_tenant', params[1]['project_name']) self.assertEqual('123tenant', params[1]['project_id']) @mock.patch('mistralclient.api.client.client') def test_project_domain_name(self, client_mock): self.shell('--os-project-domain-name=default workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('default', params[1]['project_domain_name']) @mock.patch('mistralclient.api.client.client') def test_project_domain_id(self, client_mock): self.shell('--os-project-domain-id=default workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('default', params[1]['project_domain_id']) @mock.patch('mistralclient.api.client.client') def test_user_domain_name(self, client_mock): self.shell('--os-user-domain-name=default workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('default', params[1]['user_domain_name']) @mock.patch('mistralclient.api.client.client') def test_user_domain_id(self, client_mock): self.shell('--os-user-domain-id=default workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('default', params[1]['user_domain_id']) @mock.patch('mistralclient.api.client.client') def test_target_user_name_and_password(self, client_mock): self.shell( '--os-target-username=admin' ' --os-target-password=secret_pass workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('admin', params[1]['target_username']) self.assertEqual('secret_pass', params[1]['target_api_key']) @mock.patch('mistralclient.api.client.client') def test_target_tenant_name_and_id(self, client_mock): self.shell( '--os-target-tenant-id=123fake' ' --os-target-tenant-name=fake_target workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('123fake', params[1]['target_project_id']) self.assertEqual('fake_target', params[1]['target_project_name']) @mock.patch('mistralclient.api.client.client') def test_target_user_domain_id(self, client_mock): self.shell('--os-target-user-domain-id=default workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('default', params[1]['target_user_domain_id']) @mock.patch('mistralclient.api.client.client') def test_target_user_domain_name(self, client_mock): self.shell('--os-target-user-domain-name=default workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('default', params[1]['target_user_domain_name']) @mock.patch('mistralclient.api.client.client') def test_target_project_domain_id(self, client_mock): self.shell('--os-target-project-domain-id=default workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('default', params[1]['target_project_domain_id']) @mock.patch('mistralclient.api.client.client') def test_target_project_domain_name(self, client_mock): self.shell('--os-target-project-domain-name=default workbook-list') self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('default', params[1]['target_project_domain_name']) @mock.patch('mistralclient.api.client.client') def test_no_domains_keystone_v3(self, client_mock): self.shell( '--os-auth-url=https://127.0.0.1:35357/v3 ' '--os-username=admin ' '--os-password=1234 ' 'workbook-list' ) self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('https://127.0.0.1:35357/v3', params[1]['auth_url']) # For keystone v3 'default' values are automatically substituted for # project_domain_id and user_domain_id, if nothing was provided self.assertEqual('default', params[1]['project_domain_id']) self.assertEqual('default', params[1]['user_domain_id']) self.assertEqual('default', params[1]['target_project_domain_id']) self.assertEqual('default', params[1]['target_user_domain_id']) @mock.patch('mistralclient.api.client.client') def test_with_domain_names_keystone_v3(self, client_mock): self.shell( '--os-auth-url=https://127.0.0.1:35357/v3 ' '--os-username=admin ' '--os-password=1234 ' '--os-project-domain-name=fake_domain ' '--os-user-domain-name=fake_domain ' '--os-target-project-domain-name=fake_domain ' '--os-target-user-domain-name=fake_domain ' 'workbook-list' ) self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('https://127.0.0.1:35357/v3', params[1]['auth_url']) # No need to substitute values for project_domain_id and # user_domain_id if related domain names were provided self.assertEqual('', params[1]['project_domain_id']) self.assertEqual('', params[1]['user_domain_id']) self.assertEqual('fake_domain', params[1]['project_domain_name']) self.assertEqual('fake_domain', params[1]['user_domain_name']) self.assertEqual( 'fake_domain', params[1]['target_project_domain_name'] ) self.assertEqual('fake_domain', params[1]['target_user_domain_name']) @mock.patch('mistralclient.api.client.client') def test_no_domains_keystone_v2(self, client_mock): self.shell( '--os-auth-url=https://127.0.0.1:35357/v2.0 ' '--os-username=admin ' '--os-password=1234 ' 'workbook-list' ) self.assertTrue(client_mock.called) params = client_mock.call_args self.assertEqual('https://127.0.0.1:35357/v2.0', params[1]['auth_url']) # For keystone v2 nothing is substituted self.assertEqual('', params[1]['project_domain_id']) self.assertEqual('', params[1]['user_domain_id']) self.assertEqual('', params[1]['target_project_domain_id']) self.assertEqual('', params[1]['target_user_domain_id']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/test_utils.py0000664000175000017500000000433000000000000025477 0ustar00zuulzuul00000000000000# Copyright 2015 - StackStorm, 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.path import tempfile import yaml from mistralclient import utils from oslo_serialization import jsonutils from oslotest import base ENV_DICT = {'k1': 'abc', 'k2': 123, 'k3': True} ENV_STR = jsonutils.dumps(ENV_DICT) ENV_YAML = yaml.safe_dump(ENV_DICT, default_flow_style=False) class UtilityTest(base.BaseTestCase): def test_load_empty(self): self.assertDictEqual(dict(), utils.load_content(None)) self.assertDictEqual(dict(), utils.load_content('')) self.assertDictEqual(dict(), utils.load_content('{}')) self.assertListEqual(list(), utils.load_content('[]')) def test_load_json_content(self): self.assertDictEqual(ENV_DICT, utils.load_content(ENV_STR)) def test_load_json_file(self): with tempfile.NamedTemporaryFile() as f: f.write(ENV_STR.encode('utf-8')) f.flush() file_path = os.path.abspath(f.name) self.assertDictEqual(ENV_DICT, utils.load_file(file_path)) def test_load_yaml_content(self): self.assertDictEqual(ENV_DICT, utils.load_content(ENV_YAML)) def test_load_yaml_file(self): with tempfile.NamedTemporaryFile() as f: f.write(ENV_YAML.encode('utf-8')) f.flush() file_path = os.path.abspath(f.name) self.assertDictEqual(ENV_DICT, utils.load_file(file_path)) def test_load_json(self): with tempfile.NamedTemporaryFile() as f: f.write(ENV_STR.encode('utf-8')) f.flush() self.assertDictEqual(ENV_DICT, utils.load_json(f.name)) self.assertDictEqual(ENV_DICT, utils.load_json(ENV_STR)) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8605468 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/0000775000175000017500000000000000000000000023255 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/__init__.py0000664000175000017500000000000000000000000025354 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/base.py0000664000175000017500000000321000000000000024535 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock from mistralclient.api.v2 import client from mistralclient.tests.unit import base class BaseClientV2Test(base.BaseClientTest): TEST_URL = 'http://mistral.example.com' def setUp(self): super(BaseClientV2Test, self).setUp() with mock.patch( 'mistralclient.auth.keystone.KeystoneAuthHandler.authenticate', return_value={'session': None}): self._client = client.Client(project_name="test", mistral_url=self.TEST_URL) self.workbooks = self._client.workbooks self.executions = self._client.executions self.tasks = self._client.tasks self.workflows = self._client.workflows self.environments = self._client.environments self.action_executions = self._client.action_executions self.actions = self._client.actions self.services = self._client.services self.members = self._client.members ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_action_executions.py0000664000175000017500000001054200000000000030413 0ustar00zuulzuul00000000000000# Copyright 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 mistralclient.api.v2 import action_executions from mistralclient.tests.unit.v2 import base # TODO(everyone): later we need additional tests verifying all the errors etc. ACTION_EXEC = { 'id': "1", 'name': 'my_action_execution', 'workflow_name': 'my_wf', 'state': 'RUNNING', } URL_TEMPLATE = '/action_executions' URL_TEMPLATE_ID = '/action_executions/%s' class TestActionExecutions(base.BaseClientV2Test): def test_create(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, json=ACTION_EXEC, status_code=201) body = { 'name': ACTION_EXEC['name'] } action_execution = self.action_executions.create( 'my_action_execution', {} ) self.assertIsNotNone(action_execution) self.assertEqual(action_executions.ActionExecution( self.action_executions, ACTION_EXEC ).to_dict(), action_execution.to_dict()) self.assertEqual(body, self.requests_mock.last_request.json()) def test_update(self): url = self.TEST_URL + URL_TEMPLATE_ID % ACTION_EXEC['id'] self.requests_mock.put(url, json=ACTION_EXEC) body = { 'state': ACTION_EXEC['state'] } action_execution = self.action_executions.update( ACTION_EXEC['id'], ACTION_EXEC['state'] ) self.assertIsNotNone(action_execution) expected = action_executions.ActionExecution( self.action_executions, ACTION_EXEC ).to_dict() self.assertEqual( expected, action_execution.to_dict() ) self.assertEqual(body, self.requests_mock.last_request.json()) def test_list(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, json={'action_executions': [ACTION_EXEC]}) action_execution_list = self.action_executions.list() self.assertEqual(1, len(action_execution_list)) action_execution = action_execution_list[0] expected = action_executions.ActionExecution( self.action_executions, ACTION_EXEC ).to_dict() self.assertEqual(expected, action_execution.to_dict()) def test_list_with_limit(self): self.requests_mock.get( self.TEST_URL + URL_TEMPLATE, json={'action_executions': [ACTION_EXEC]} ) action_execution_list = self.action_executions.list(limit=1) self.assertEqual(1, len(action_execution_list)) last_request = self.requests_mock.last_request # Make sure that limit is passed to the server correctly. self.assertEqual(['1'], last_request.qs['limit']) def test_list_with_no_limit(self): self.requests_mock.get( self.TEST_URL + URL_TEMPLATE, json={'action_executions': [ACTION_EXEC]} ) action_execution_list = self.action_executions.list(limit=-1) self.assertEqual(1, len(action_execution_list)) last_request = self.requests_mock.last_request self.assertNotIn('limit', last_request.qs) def test_get(self): url = self.TEST_URL + URL_TEMPLATE_ID % ACTION_EXEC['id'] self.requests_mock.get(url, json=ACTION_EXEC) action_execution = self.action_executions.get(ACTION_EXEC['id']) expected = action_executions.ActionExecution( self.action_executions, ACTION_EXEC ).to_dict() self.assertEqual(expected, action_execution.to_dict()) def test_delete(self): url = self.TEST_URL + URL_TEMPLATE_ID % ACTION_EXEC['id'] self.requests_mock.delete(url, status_code=204) self.action_executions.delete(ACTION_EXEC['id']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_actions.py0000664000175000017500000002565000000000000026336 0ustar00zuulzuul00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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.path import pkg_resources as pkg from urllib import parse from urllib import request from mistralclient.api import base as api_base from mistralclient.api.v2 import actions from mistralclient.tests.unit.v2 import base ACTION_DEF = """ --- version: 2.0 my_action: base: std.echo base-input: output: 'Bye!' output: info: <% $.output %> """ INVALID_ACTION_DEF = """ --- version: 2.0 my_action: base: std.echo unexpected-property: 'this should fail' base-input: output: 'Bye!' output: info: <% $.output %> """ ACTION = { 'id': '123', 'name': 'my_action', 'input': '', 'definition': ACTION_DEF } URL_TEMPLATE = '/actions' URL_TEMPLATE_SCOPE = '/actions?scope=private' URL_TEMPLATE_NAME = '/actions/%s' URL_TEMPLATE_VALIDATE = '/actions/validate' class TestActionsV2(base.BaseClientV2Test): def test_create(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, json={'actions': [ACTION]}, status_code=201) actions = self.actions.create(ACTION_DEF) self.assertIsNotNone(actions) self.assertEqual(ACTION_DEF, actions[0].definition) last_request = self.requests_mock.last_request self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) def test_create_with_namespace(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, json={'actions': [ACTION]}, status_code=201) actions = self.actions.create(ACTION_DEF, namespace='test_namespace') self.assertIsNotNone(actions) self.assertEqual(ACTION_DEF, actions[0].definition) last_request = self.requests_mock.last_request self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) def test_create_with_file(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, json={'actions': [ACTION]}, status_code=201) # The contents of action_v2.yaml must be identical to ACTION_DEF path = pkg.resource_filename( 'mistralclient', 'tests/unit/resources/action_v2.yaml' ) actions = self.actions.create(path) self.assertIsNotNone(actions) self.assertEqual(ACTION_DEF, actions[0].definition) last_request = self.requests_mock.last_request self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) def test_update_with_id(self): self.requests_mock.put(self.TEST_URL + URL_TEMPLATE_NAME % 123, json={'actions': [ACTION]}) actions = self.actions.update(ACTION_DEF, id=123) self.assertIsNotNone(actions) self.assertEqual(ACTION_DEF, actions[0].definition) last_request = self.requests_mock.last_request self.assertEqual('scope=private', last_request.query) self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) def test_update(self): self.requests_mock.put(self.TEST_URL + URL_TEMPLATE, json={'actions': [ACTION]}) actions = self.actions.update(ACTION_DEF) self.assertIsNotNone(actions) self.assertEqual(ACTION_DEF, actions[0].definition) last_request = self.requests_mock.last_request self.assertEqual('scope=private', last_request.query) self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) def test_update_with_namespace(self): self.requests_mock.put(self.TEST_URL + URL_TEMPLATE, json={'actions': [ACTION]}) actions = self.actions.update(ACTION_DEF, namespace='test_namespace') self.assertIsNotNone(actions) self.assertEqual(ACTION_DEF, actions[0].definition) last_request = self.requests_mock.last_request self.assertEqual('scope=private&namespace=test_namespace', last_request.query) self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) def test_update_with_file_uri(self): self.requests_mock.put(self.TEST_URL + URL_TEMPLATE, json={'actions': [ACTION]}) # The contents of action_v2.yaml must be identical to ACTION_DEF path = pkg.resource_filename( 'mistralclient', 'tests/unit/resources/action_v2.yaml' ) path = os.path.abspath(path) # Convert the file path to file URI uri = parse.urljoin('file:', request.pathname2url(path)) actions = self.actions.update(uri) self.assertIsNotNone(actions) self.assertEqual(ACTION_DEF, actions[0].definition) last_request = self.requests_mock.last_request self.assertEqual('scope=private', last_request.query) self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) def test_list(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, json={'actions': [ACTION]}) action_list = self.actions.list() self.assertEqual(1, len(action_list)) action = action_list[0] self.assertEqual( actions.Action(self.actions, ACTION).to_dict(), action.to_dict() ) def test_list_with_pagination(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, json={'actions': [ACTION], 'next': '/actions?fake'}) action_list = self.actions.list( limit=1, sort_keys='created_at', sort_dirs='asc' ) self.assertEqual(1, len(action_list)) last_request = self.requests_mock.last_request # The url param order is unpredictable. self.assertEqual(['1'], last_request.qs['limit']) self.assertEqual(['created_at'], last_request.qs['sort_keys']) self.assertEqual(['asc'], last_request.qs['sort_dirs']) def test_list_with_no_limit(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, json={'actions': [ACTION]}) action_list = self.actions.list(limit=-1) self.assertEqual(1, len(action_list)) last_request = self.requests_mock.last_request self.assertNotIn('limit', last_request.qs) def test_get(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE_NAME % 'action/', json=ACTION) action = self.actions.get('action') self.assertIsNotNone(action) self.assertEqual( actions.Action(self.actions, ACTION).to_dict(), action.to_dict() ) def test_get_with_namespace(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE_NAME % 'action/namespace', json=ACTION) action = self.actions.get('action', 'namespace') self.assertIsNotNone(action) self.assertEqual( actions.Action(self.actions, ACTION).to_dict(), action.to_dict() ) def test_delete(self): url = self.TEST_URL + URL_TEMPLATE_NAME % 'action/' m = self.requests_mock.delete(url, status_code=204) self.actions.delete('action') self.assertEqual(1, m.call_count) def test_delete_with_namespace(self): url = self.TEST_URL + URL_TEMPLATE_NAME % 'action/namespace' m = self.requests_mock.delete(url, status_code=204) self.actions.delete('action', 'namespace') self.assertEqual(1, m.call_count) def test_validate(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, json={'valid': True}) result = self.actions.validate(ACTION_DEF) self.assertIsNotNone(result) self.assertIn('valid', result) self.assertTrue(result['valid']) last_request = self.requests_mock.last_request self.assertEqual(ACTION_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_validate_with_file(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, json={'valid': True}) # The contents of action_v2.yaml must be identical to ACTION_DEF path = pkg.resource_filename( 'mistralclient', 'tests/unit/resources/action_v2.yaml' ) result = self.actions.validate(path) self.assertIsNotNone(result) self.assertIn('valid', result) self.assertTrue(result['valid']) last_request = self.requests_mock.last_request self.assertEqual(ACTION_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_validate_failed(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, json={"valid": False, "error": "mocked error message"}) result = self.actions.validate(INVALID_ACTION_DEF) self.assertIsNotNone(result) self.assertIn('valid', result) self.assertFalse(result['valid']) self.assertIn('error', result) self.assertIn("mocked error message", result['error']) last_request = self.requests_mock.last_request self.assertEqual('text/plain', last_request.headers['content-type']) def test_validate_api_failed(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE_VALIDATE, status_code=500, json={}) self.assertRaises( api_base.APIException, self.actions.validate, ACTION_DEF ) last_request = self.requests_mock.last_request self.assertEqual('text/plain', last_request.headers['content-type']) self.assertEqual(ACTION_DEF, last_request.text) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_action_execs.py0000664000175000017500000001601300000000000030162 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2016 - Brocade Communications Systems, 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 copy import io import sys from unittest import mock from oslo_serialization import jsonutils from mistralclient.api.v2 import action_executions as action_ex from mistralclient.commands.v2 import action_executions as action_ex_cmd from mistralclient.tests.unit import base ACTION_EX_DICT = { 'id': '123', 'name': 'some', 'workflow_name': 'thing', 'workflow_namespace': '', 'task_name': 'task1', 'task_execution_id': "1-2-3-4", 'state': 'RUNNING', 'state_info': 'RUNNING somehow.', 'accepted': True, 'created_at': '1', 'updated_at': '1', } ACTION_EX_RESULT = {"test": "is", "passed": "successfully"} ACTION_EX_INPUT = {"param1": "val1", "param2": 2} ACTION_EX_WITH_OUTPUT_DICT = ACTION_EX_DICT.copy() ACTION_EX_WITH_OUTPUT_DICT.update( {'output': jsonutils.dumps(ACTION_EX_RESULT)}) ACTION_EX_WITH_INPUT_DICT = ACTION_EX_DICT.copy() ACTION_EX_WITH_INPUT_DICT.update({'input': jsonutils.dumps(ACTION_EX_INPUT)}) ACTION_EX = action_ex.ActionExecution(mock, ACTION_EX_DICT) ACTION_EX_WITH_OUTPUT = action_ex.ActionExecution( mock, ACTION_EX_WITH_OUTPUT_DICT ) ACTION_EX_WITH_INPUT = action_ex.ActionExecution( mock, ACTION_EX_WITH_INPUT_DICT ) class TestCLIActionExecutions(base.BaseCommandTest): def test_create(self): (self.client.action_executions.create. return_value) = ACTION_EX_WITH_OUTPUT self.call( action_ex_cmd.Create, app_args=['some', '{"output": "Hello!"}'] ) self.assertDictEqual( ACTION_EX_RESULT, jsonutils.loads(self.app.stdout.write.call_args[0][0]) ) def test_create_save_result(self): (self.client.action_executions.create. return_value) = ACTION_EX_WITH_OUTPUT result = self.call( action_ex_cmd.Create, app_args=[ 'some', '{"output": "Hello!"}', '--save-result' ] ) self.assertEqual( ('123', 'some', 'thing', '', 'task1', '1-2-3-4', 'RUNNING', 'RUNNING somehow.', True, '1', '1'), result[1] ) def test_create_run_sync(self): (self.client.action_executions.create. return_value) = ACTION_EX_WITH_OUTPUT self.call( action_ex_cmd.Create, app_args=[ 'some', '{"output": "Hello!"}', '--run-sync' ] ) self.assertDictEqual( ACTION_EX_RESULT, jsonutils.loads(self.app.stdout.write.call_args[0][0]) ) def test_create_run_sync_and_save_result(self): (self.client.action_executions.create. return_value) = ACTION_EX_WITH_OUTPUT self.call( action_ex_cmd.Create, app_args=[ 'some', '{"output": "Hello!"}', '--save-result', '--run-sync' ] ) self.assertDictEqual( ACTION_EX_RESULT, jsonutils.loads(self.app.stdout.write.call_args[0][0]) ) def test_update(self): states = ['PAUSED', 'RUNNING', 'SUCCESS', 'ERROR', 'CANCELLED'] for state in states: action_ex_dict = copy.deepcopy(ACTION_EX_DICT) action_ex_dict['state'] = state action_ex_dict['state_info'] = 'testing update' action_ex_obj = action_ex.ActionExecution(mock, action_ex_dict) self.client.action_executions.update.return_value = action_ex_obj result = self.call( action_ex_cmd.Update, app_args=['id', '--state', state] ) expected_result = ( action_ex_dict['id'], action_ex_dict['name'], action_ex_dict['workflow_name'], action_ex_dict['workflow_namespace'], action_ex_dict['task_name'], action_ex_dict['task_execution_id'], action_ex_dict['state'], action_ex_dict['state_info'], action_ex_dict['accepted'], action_ex_dict['created_at'], action_ex_dict['updated_at'] ) self.assertEqual(expected_result, result[1]) def test_update_invalid_state(self): states = ['IDLE', 'WAITING', 'DELAYED'] # Redirect the stderr so it doesn't show during tox _stderr = sys.stderr sys.stderr = io.StringIO() for state in states: self.assertRaises( SystemExit, self.call, action_ex_cmd.Update, app_args=['id', '--state', state] ) # Stop the redirection print(sys.stderr.getvalue()) sys.stderr = _stderr def test_list(self): self.client.action_executions.list.return_value = [ACTION_EX] result = self.call(action_ex_cmd.List) self.assertEqual( [('123', 'some', 'thing', '', 'task1', '1-2-3-4', 'RUNNING', True, '1', '1')], result[1] ) def test_get(self): self.client.action_executions.get.return_value = ACTION_EX result = self.call(action_ex_cmd.Get, app_args=['id']) self.assertEqual( ('123', 'some', 'thing', '', 'task1', '1-2-3-4', 'RUNNING', 'RUNNING somehow.', True, '1', '1'), result[1] ) def test_get_output(self): self.client.action_executions.get.return_value = ACTION_EX_WITH_OUTPUT self.call(action_ex_cmd.GetOutput, app_args=['id']) self.assertDictEqual( ACTION_EX_RESULT, jsonutils.loads(self.app.stdout.write.call_args[0][0]) ) def test_get_input(self): self.client.action_executions.get.return_value = ACTION_EX_WITH_INPUT self.call(action_ex_cmd.GetInput, app_args=['id']) self.assertDictEqual( ACTION_EX_INPUT, jsonutils.loads(self.app.stdout.write.call_args[0][0]) ) def test_delete(self): self.call(action_ex_cmd.Delete, app_args=['id']) self.client.action_executions.delete.assert_called_once_with('id') def test_delete_with_multi_names(self): self.call(action_ex_cmd.Delete, app_args=['id1', 'id2']) self.assertEqual(2, self.client.action_executions.delete.call_count) self.assertEqual( [mock.call('id1'), mock.call('id2')], self.client.action_executions.delete.call_args_list ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_actions.py0000664000175000017500000001600000000000000027152 0ustar00zuulzuul00000000000000# Copyright 2014 Mirantis, Inc. # Copyright 2020 Nokia Software. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from unittest import mock from mistralclient.api.v2 import actions from mistralclient.commands.v2 import actions as action_cmd from mistralclient.commands.v2 import base as cmd_base from mistralclient.tests.unit import base ACTION_DICT = { 'id': '1234-4567-7894-7895', 'name': 'a', 'is_system': True, 'input': "param1", 'description': 'My cool action', 'tags': ['test'], 'created_at': '1', 'updated_at': '1', 'namespace': 'test_namespace' } ACTION_DEF = """ --- version: '2.0' base: std.echo base-parameters: output: "<% $.str1 %><% $.str2 %>" output: "<% $ %><% $ %>" """ ACTION_WITH_DEF_DICT = ACTION_DICT.copy() ACTION_WITH_DEF_DICT.update({'definition': ACTION_DEF}) ACTION = actions.Action(mock, ACTION_DICT) ACTION_WITH_DEF = actions.Action(mock, ACTION_WITH_DEF_DICT) class TestCLIActionsV2(base.BaseCommandTest): @mock.patch('argparse.open', create=True) def test_create(self, mock_open): self.client.actions.create.return_value = [ACTION] result = self.call(action_cmd.Create, app_args=['1.txt']) self.assertEqual( [('1234-4567-7894-7895', 'a', True, "param1", 'My cool action', 'test', '1', '1')], result[1] ) @mock.patch('argparse.open', create=True) def test_create_public(self, mock_open): self.client.actions.create.return_value = [ACTION] result = self.call( action_cmd.Create, app_args=['1.txt', '--public'] ) self.assertEqual( [('1234-4567-7894-7895', 'a', True, "param1", 'My cool action', 'test', '1', '1')], result[1] ) self.assertEqual( 'public', self.client.actions.create.call_args[1]['scope'] ) @mock.patch('argparse.open', create=True) def test_create_long_input(self, mock_open): action_long_input_dict = ACTION_DICT.copy() long_input = ', '.join( ['var%s' % i for i in range(10)] ) action_long_input_dict['input'] = long_input workflow_long_input = actions.Action( mock.Mock(), action_long_input_dict ) self.client.actions.create.return_value = [workflow_long_input] result = self.call(action_cmd.Create, app_args=['1.txt']) self.assertEqual( [('1234-4567-7894-7895', 'a', True, cmd_base.cut(long_input), 'My cool action', 'test', '1', '1')], result[1] ) @mock.patch('argparse.open', create=True) def test_update(self, mock_open): self.client.actions.update.return_value = [ACTION] result = self.call(action_cmd.Update, app_args=['my_action.yaml']) self.assertEqual( [('1234-4567-7894-7895', 'a', True, "param1", 'My cool action', 'test', '1', '1')], result[1] ) @mock.patch('argparse.open', create=True) def test_update_public(self, mock_open): self.client.actions.update.return_value = [ACTION] result = self.call( action_cmd.Update, app_args=['my_action.yaml', '--public'] ) self.assertEqual( [('1234-4567-7894-7895', 'a', True, "param1", 'My cool action', 'test', '1', '1')], result[1] ) self.assertEqual( 'public', self.client.actions.update.call_args[1]['scope'] ) def test_list(self): self.client.actions.list.return_value = [ACTION] result = self.call(action_cmd.List) self.assertEqual( [('1234-4567-7894-7895', 'a', True, "param1", 'My cool action', 'test', '1', '1')], result[1] ) def test_get(self): self.client.actions.get.return_value = ACTION result = self.call(action_cmd.Get, app_args=['name']) self.assertEqual( ('1234-4567-7894-7895', 'a', True, "param1", 'My cool action', 'test', '1', '1'), result[1] ) def test_delete(self): self.call(action_cmd.Delete, app_args=['name']) self.client.actions.delete.assert_called_once_with('name', namespace='') def test_delete_with_namespace(self): self.call(action_cmd.Delete, app_args=['name', '--namespace', 'test_namespace'] ) self.client.actions.delete.assert_called_once_with( 'name', namespace='test_namespace') def test_delete_with_multi_names_and_namespace(self): self.call(action_cmd.Delete, app_args=['name1', 'name2', '--namespace', 'test_namespace']) self.assertEqual(2, self.client.actions.delete.call_count) self.assertEqual( [mock.call('name1', namespace='test_namespace'), mock.call('name2', namespace='test_namespace')], self.client.actions.delete.call_args_list ) def test_delete_with_multi_names(self): self.call(action_cmd.Delete, app_args=['name1', 'name2']) self.assertEqual(2, self.client.actions.delete.call_count) self.assertEqual( [mock.call('name1', namespace=''), mock.call('name2', namespace='')], self.client.actions.delete.call_args_list ) def test_get_definition(self): self.client.actions.get.return_value = ACTION_WITH_DEF self.call(action_cmd.GetDefinition, app_args=['name']) self.app.stdout.write.assert_called_with(ACTION_DEF) @mock.patch('argparse.open', create=True) def test_validate(self, mock_open): self.client.actions.validate.return_value = {'valid': True} result = self.call(action_cmd.Validate, app_args=['action.yaml']) self.assertEqual((True, None), result[1]) @mock.patch('argparse.open', create=True) def test_validate_failed(self, mock_open): self.client.actions.validate.return_value = { 'valid': False, 'error': 'Invalid DSL...' } result = self.call(action_cmd.Validate, app_args=['action.yaml']) self.assertEqual((False, 'Invalid DSL...'), result[1]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_bash_completion.py0000664000175000017500000000163300000000000030666 0ustar00zuulzuul00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 mistralclient.tests.unit.base_shell_test as base class TestCLIBashCompletionV2(base.BaseShellTests): def test_bash_completion(self): bash_completion, stderr = self.shell('bash-completion') self.assertIn('bash-completion', bash_completion) self.assertFalse(stderr) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_code_sources.py0000664000175000017500000001172700000000000030202 0ustar00zuulzuul00000000000000# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 mistralclient.api.v2 import code_sources from mistralclient.commands.v2 import code_sources as code_src_cmd from mistralclient.tests.unit import base CODE_SRC_CONTENT = """ from mistral_lib import actions class HelloAction(actions.Action): def __init__(self, f_name, l_name): super(HelloAction, self).__init__() self._f_name = f_name self._l_name = l_name def run(self, context): return 'Hello %s %s!' % (self._f_name, self._l_name) """ CODE_SRC_DICT = { 'id': '1-2-3-4', 'name': 'hello_module', 'namespace': '', 'project_id': '12345', 'scope': 'private', 'content': CODE_SRC_CONTENT, 'created_at': '1', 'updated_at': '1' } CODE_SRC = code_sources.CodeSource(mock, CODE_SRC_DICT) class TestCLICodeSources(base.BaseCommandTest): @mock.patch('argparse.open', create=True) def test_create(self, mock_open): self.client.code_sources.create.return_value = CODE_SRC result = self.call( code_src_cmd.Create, app_args=['hello_module', 'hello.py'] ) self.assertEqual( ('1-2-3-4', 'hello_module', '', '12345', 'private', '1', '1'), result[1] ) @mock.patch('argparse.open', create=True) def test_create_public(self, mock_open): code_src_dict = CODE_SRC_DICT.copy() code_src_dict['scope'] = 'public' code_src = code_sources.CodeSource(mock, code_src_dict) self.client.code_sources.create.return_value = code_src result = self.call( code_src_cmd.Create, app_args=['hello_module', '1.txt', '--public'] ) self.assertEqual( ('1-2-3-4', 'hello_module', '', '12345', 'public', '1', '1'), result[1] ) self.assertEqual( 'public', self.client.code_sources.create.call_args[1]['scope'] ) @mock.patch('argparse.open', create=True) def test_update(self, mock_open): self.client.code_sources.update.return_value = CODE_SRC result = self.call( code_src_cmd.Update, app_args=['hello_module', 'hello.py'] ) self.assertEqual( ('1-2-3-4', 'hello_module', '', '12345', 'private', '1', '1'), result[1] ) @mock.patch('argparse.open', create=True) def test_update_public(self, mock_open): code_src_dict = CODE_SRC_DICT.copy() code_src_dict['scope'] = 'public' code_src = code_sources.CodeSource(mock, code_src_dict) self.client.code_sources.update.return_value = code_src result = self.call( code_src_cmd.Update, app_args=['hello_module', 'hello.py', '--public'] ) self.assertEqual( ('1-2-3-4', 'hello_module', '', '12345', 'public', '1', '1'), result[1] ) self.assertEqual( 'public', self.client.code_sources.update.call_args[1]['scope'] ) @mock.patch('argparse.open', create=True) def test_update_with_id(self, mock_open): self.client.code_sources.update.return_value = CODE_SRC result = self.call( code_src_cmd.Update, app_args=['1-2-3-4', 'hello.py'] ) self.assertEqual( ('1-2-3-4', 'hello_module', '', '12345', 'private', '1', '1'), result[1] ) def test_list(self): self.client.code_sources.list.return_value = [CODE_SRC] result = self.call(code_src_cmd.List) self.assertEqual( [('1-2-3-4', 'hello_module', '', '12345', 'private', '1', '1')], result[1] ) def test_get(self): self.client.code_sources.get.return_value = CODE_SRC result = self.call(code_src_cmd.Get, app_args=['hello_module']) self.assertEqual( ('1-2-3-4', 'hello_module', '', '12345', 'private', '1', '1'), result[1] ) def test_delete(self): self.call(code_src_cmd.Delete, app_args=['hello_module']) self.client.code_sources.delete.assert_called_once_with( 'hello_module', None ) def test_get_content(self): self.client.code_sources.get.return_value = CODE_SRC self.call(code_src_cmd.GetContent, app_args=['hello_module']) self.app.stdout.write.assert_called_with(CODE_SRC_CONTENT) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_cron_triggers.py0000664000175000017500000001370000000000000030365 0ustar00zuulzuul00000000000000# Copyright 2014 Mirantis, Inc. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from unittest import mock from mistralclient.api.v2 import cron_triggers from mistralclient.commands.v2 import cron_triggers as cron_triggers_cmd from mistralclient.tests.unit import base TRIGGER_DICT = { 'name': 'my_trigger', 'workflow_name': 'flow1', 'workflow_input': {}, 'workflow_params': {}, 'pattern': '* * * * *', 'next_execution_time': '4242-12-20 13:37', 'remaining_executions': 5, 'created_at': '1', 'updated_at': '1' } TRIGGER = cron_triggers.CronTrigger(mock, TRIGGER_DICT) class TestCLITriggersV2(base.BaseCommandTest): @mock.patch('mistralclient.commands.v2.cron_triggers.Create.' '_convert_time_string_to_utc') @mock.patch('argparse.open', create=True) def test_create(self, mock_open, mock_convert): self.client.cron_triggers.create.return_value = TRIGGER mock_open.return_value = mock.MagicMock(spec=open) result = self.call( cron_triggers_cmd.Create, app_args=['my_trigger', 'flow1', '--pattern', '* * * * *', '--params', '{}', '--count', '5', '--first-time', '4242-12-20 13:37', '--utc'] ) mock_convert.assert_not_called() self.assertEqual( ( 'my_trigger', 'flow1', {}, '* * * * *', '4242-12-20 13:37', 5, '1', '1' ), result[1] ) @mock.patch('mistralclient.commands.v2.cron_triggers.Create.' '_convert_time_string_to_utc') @mock.patch('argparse.open', create=True) def test_create_no_utc(self, mock_open, mock_convert): self.client.cron_triggers.create.return_value = TRIGGER mock_open.return_value = mock.MagicMock(spec=open) mock_convert.return_value = '4242-12-20 18:37' result = self.call( cron_triggers_cmd.Create, app_args=['my_trigger', 'flow1', '--pattern', '* * * * *', '--params', '{}', '--count', '5', '--first-time', '4242-12-20 13:37'] ) mock_convert.assert_called_once_with('4242-12-20 13:37') self.client.cron_triggers.create.assert_called_once_with( 'my_trigger', 'flow1', {}, {}, '* * * * *', '4242-12-20 18:37', 5) self.assertEqual( ( 'my_trigger', 'flow1', {}, '* * * * *', '4242-12-20 13:37', 5, '1', '1' ), result[1] ) @mock.patch('mistralclient.commands.v2.cron_triggers.time') def test_convert_time_string_to_utc_from_utc(self, mock_time): cmd = cron_triggers_cmd.Create(self.app, None) mock_time.daylight = 0 mock_time.altzone = 0 mock_time.timezone = 0 mock_localtime = mock.Mock() mock_localtime.tm_isdst = 0 mock_time.localtime.return_value = mock_localtime utc_value = cmd._convert_time_string_to_utc('4242-12-20 13:37') expected_time = '4242-12-20 13:37' self.assertEqual(expected_time, utc_value) @mock.patch('mistralclient.commands.v2.cron_triggers.time') def test_convert_time_string_to_utc_from_dst(self, mock_time): cmd = cron_triggers_cmd.Create(self.app, None) mock_time.daylight = 1 mock_time.altzone = (4 * 60 * 60) mock_time.timezone = (5 * 60 * 60) mock_localtime = mock.Mock() mock_localtime.tm_isdst = 1 mock_time.localtime.return_value = mock_localtime utc_value = cmd._convert_time_string_to_utc('4242-12-20 13:37') expected_time = '4242-12-20 17:37' self.assertEqual(expected_time, utc_value) @mock.patch('mistralclient.commands.v2.cron_triggers.time') def test_convert_time_string_to_utc_no_dst(self, mock_time): cmd = cron_triggers_cmd.Create(self.app, None) mock_time.daylight = 1 mock_time.altzone = (4 * 60 * 60) mock_time.timezone = (5 * 60 * 60) mock_localtime = mock.Mock() mock_localtime.tm_isdst = 0 mock_time.localtime.return_value = mock_localtime utc_value = cmd._convert_time_string_to_utc('4242-12-20 13:37') expected_time = '4242-12-20 18:37' self.assertEqual(expected_time, utc_value) def test_list(self): self.client.cron_triggers.list.return_value = [TRIGGER] result = self.call(cron_triggers_cmd.List) self.assertEqual( [( 'my_trigger', 'flow1', {}, '* * * * *', '4242-12-20 13:37', 5, '1', '1' )], result[1] ) def test_get(self): self.client.cron_triggers.get.return_value = TRIGGER result = self.call(cron_triggers_cmd.Get, app_args=['name']) self.assertEqual( ( 'my_trigger', 'flow1', {}, '* * * * *', '4242-12-20 13:37', 5, '1', '1' ), result[1] ) def test_delete(self): self.call(cron_triggers_cmd.Delete, app_args=['name']) self.client.cron_triggers.delete.assert_called_once_with('name') def test_delete_with_multi_names(self): self.call(cron_triggers_cmd.Delete, app_args=['name1', 'name2']) self.assertEqual(2, self.client.cron_triggers.delete.call_count) self.assertEqual( [mock.call('name1'), mock.call('name2')], self.client.cron_triggers.delete.call_args_list ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_dynamic_actions.py0000664000175000017500000001356000000000000030666 0ustar00zuulzuul00000000000000# Copyright 2020 Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 mistralclient.api.v2 import dynamic_actions from mistralclient.commands.v2 import dynamic_actions as dyn_actions_cmd from mistralclient.tests.unit import base DYN_ACTION_DICT = { 'id': '1-2-3-4', 'name': 'my_action', 'class_name': 'MyAction', 'code_source_name': 'my_module', 'code_source_id': '2-3-4-5', 'namespace': '', 'project_id': '12345', 'scope': 'private', 'created_at': '1', 'updated_at': '1' } DYN_ACTION = dynamic_actions.DynamicAction(mock, DYN_ACTION_DICT) class TestCLIDynamicActions(base.BaseCommandTest): @mock.patch('argparse.open', create=True) def test_create(self, mock_open): self.client.dynamic_actions.create.return_value = DYN_ACTION result = self.call( dyn_actions_cmd.Create, app_args=['my_action', 'MyAction', 'my_module'] ) self.assertEqual( ( '1-2-3-4', 'my_action', 'MyAction', '2-3-4-5', 'my_module', '12345', 'private', '1', '1' ), result[1] ) @mock.patch('argparse.open', create=True) def test_create_public(self, mock_open): dyn_action_dict = DYN_ACTION_DICT.copy() dyn_action_dict['scope'] = 'public' dyn_action = dynamic_actions.DynamicAction(mock, dyn_action_dict) self.client.dynamic_actions.create.return_value = dyn_action result = self.call( dyn_actions_cmd.Create, app_args=['my_action', 'MyAction', 'my_module', '--public'] ) self.assertEqual( ( '1-2-3-4', 'my_action', 'MyAction', '2-3-4-5', 'my_module', '12345', 'public', '1', '1' ), result[1] ) self.assertEqual( 'public', self.client.dynamic_actions.create.call_args[1]['scope'] ) @mock.patch('argparse.open', create=True) def test_update(self, mock_open): dyn_action_dict = DYN_ACTION_DICT.copy() dyn_action_dict['class_name'] = 'MyNewAction' dyn_action = dynamic_actions.DynamicAction(mock, dyn_action_dict) self.client.dynamic_actions.update.return_value = dyn_action result = self.call( dyn_actions_cmd.Update, app_args=['my_action', '--class-name', 'MyNewAction'] ) self.assertEqual( ( '1-2-3-4', 'my_action', 'MyNewAction', '2-3-4-5', 'my_module', '12345', 'private', '1', '1' ), result[1] ) @mock.patch('argparse.open', create=True) def test_update_with_id(self, mock_open): dyn_action_dict = DYN_ACTION_DICT.copy() dyn_action_dict['class_name'] = 'MyNewAction' dyn_action = dynamic_actions.DynamicAction(mock, dyn_action_dict) self.client.dynamic_actions.update.return_value = dyn_action result = self.call( dyn_actions_cmd.Update, app_args=['1-2-3-4', '--class-name', 'MyNewAction'] ) self.assertEqual( ( '1-2-3-4', 'my_action', 'MyNewAction', '2-3-4-5', 'my_module', '12345', 'private', '1', '1' ), result[1] ) def test_list(self): self.client.dynamic_actions.list.return_value = [DYN_ACTION] result = self.call(dyn_actions_cmd.List) self.assertEqual( [ ( '1-2-3-4', 'my_action', 'MyAction', '2-3-4-5', 'my_module', '12345', 'private', '1', '1' ) ], result[1] ) def test_get(self): self.client.dynamic_actions.get.return_value = DYN_ACTION result = self.call(dyn_actions_cmd.Get, app_args=['name']) self.assertEqual( ( '1-2-3-4', 'my_action', 'MyAction', '2-3-4-5', 'my_module', '12345', 'private', '1', '1' ), result[1] ) def test_delete(self): self.call(dyn_actions_cmd.Delete, app_args=['my_action']) self.client.dynamic_actions.delete.assert_called_once_with( 'my_action', namespace='' ) def test_delete_with_multi_names(self): self.call(dyn_actions_cmd.Delete, app_args=['name1', 'name2']) self.assertEqual(2, self.client.dynamic_actions.delete.call_count) self.assertEqual( [ mock.call('name1', namespace=''), mock.call('name2', namespace='') ], self.client.dynamic_actions.delete.call_args_list ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_environments.py0000664000175000017500000001131400000000000030244 0ustar00zuulzuul00000000000000# Copyright 2015 - StackStorm, 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 copy import datetime import os import tempfile from unittest import mock from oslo_serialization import jsonutils import yaml from mistralclient.api.v2 import environments from mistralclient.commands.v2 import environments as environment_cmd from mistralclient.tests.unit import base ENVIRONMENT_DICT = { 'name': 'env1', 'description': 'Test Environment #1', 'scope': 'private', 'variables': { 'server': 'localhost', 'database': 'test', 'timeout': 600, 'verbose': True }, 'created_at': str(datetime.datetime.utcnow()), 'updated_at': str(datetime.datetime.utcnow()) } ENVIRONMENT = environments.Environment(mock, ENVIRONMENT_DICT) EXPECTED_RESULT = (ENVIRONMENT_DICT['name'], ENVIRONMENT_DICT['description'], jsonutils.dumps(ENVIRONMENT_DICT['variables'], indent=4), ENVIRONMENT_DICT['scope'], ENVIRONMENT_DICT['created_at'], ENVIRONMENT_DICT['updated_at']) EXPECTED_EXPORT_RESULT = (ENVIRONMENT_DICT['name'], ENVIRONMENT_DICT['description'], ENVIRONMENT_DICT['scope'], jsonutils.dumps(ENVIRONMENT_DICT['variables'])) class TestCLIEnvironmentsV2(base.BaseCommandTest): def _test_create(self, content): self.client.environments.create.return_value = ENVIRONMENT with tempfile.NamedTemporaryFile() as f: f.write(content.encode('utf-8')) f.flush() file_path = os.path.abspath(f.name) result = self.call(environment_cmd.Create, app_args=[file_path]) self.assertEqual(EXPECTED_RESULT, result[1]) def test_create_from_json(self): self._test_create(jsonutils.dumps(ENVIRONMENT_DICT, indent=4)) def test_create_from_yaml(self): yml = yaml.dump(ENVIRONMENT_DICT, default_flow_style=False) self._test_create(yml) def _test_update(self, content): self.client.environments.update.return_value = ENVIRONMENT with tempfile.NamedTemporaryFile() as f: f.write(content.encode('utf-8')) f.flush() file_path = os.path.abspath(f.name) result = self.call(environment_cmd.Update, app_args=[file_path]) self.assertEqual(EXPECTED_RESULT, result[1]) def test_update_from_json(self): env = copy.deepcopy(ENVIRONMENT_DICT) del env['created_at'] del env['updated_at'] self._test_update(jsonutils.dumps(env, indent=4)) def test_update_from_yaml(self): env = copy.deepcopy(ENVIRONMENT_DICT) del env['created_at'] del env['updated_at'] yml = yaml.dump(env, default_flow_style=False) self._test_update(yml) def test_list(self): self.client.environments.list.return_value = [ENVIRONMENT] expected = (ENVIRONMENT_DICT['name'], ENVIRONMENT_DICT['description'], ENVIRONMENT_DICT['scope'], ENVIRONMENT_DICT['created_at'], ENVIRONMENT_DICT['updated_at']) result = self.call(environment_cmd.List) self.assertListEqual([expected], result[1]) def test_get(self): self.client.environments.get.return_value = ENVIRONMENT result = self.call(environment_cmd.Get, app_args=['name']) self.assertEqual(EXPECTED_RESULT, result[1]) def test_get_with_export(self): self.client.environments.get.return_value = ENVIRONMENT result = self.call(environment_cmd.Get, app_args=['--export', 'name']) self.assertEqual(EXPECTED_EXPORT_RESULT, result[1]) def test_delete(self): self.call(environment_cmd.Delete, app_args=['name']) self.client.environments.delete.assert_called_once_with('name') def test_delete_with_multi_names(self): self.call(environment_cmd.Delete, app_args=['name1', 'name2']) self.assertEqual(2, self.client.environments.delete.call_count) self.assertEqual( [mock.call('name1'), mock.call('name2')], self.client.environments.delete.call_args_list ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_event_triggers.py0000664000175000017500000000625000000000000030547 0ustar00zuulzuul00000000000000# Copyright 2014 Mirantis, Inc. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from unittest import mock from mistralclient.api.v2 import event_triggers from mistralclient.commands.v2 import event_triggers as event_triggers_cmd from mistralclient.tests.unit import base TRIGGER_DICT = { 'id': '456', 'name': 'my_trigger', 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', 'workflow_input': {}, 'workflow_params': {}, 'exchange': 'dummy_exchange', 'topic': 'dummy_topic', 'event': 'event.dummy', 'created_at': '1', 'updated_at': '1' } TRIGGER = event_triggers.EventTrigger(mock, TRIGGER_DICT) class TestCLITriggersV2(base.BaseCommandTest): @mock.patch('argparse.open', create=True) def test_create(self, mock_open): self.client.event_triggers.create.return_value = TRIGGER mock_open.return_value = mock.MagicMock(spec=open) result = self.call( event_triggers_cmd.Create, app_args=['my_trigger', '123e4567-e89b-12d3-a456-426655440000', 'dummy_exchange', 'dummy_topic', 'event.dummy', '--params', '{}'] ) self.assertEqual( ( '456', 'my_trigger', '123e4567-e89b-12d3-a456-426655440000', {}, 'dummy_exchange', 'dummy_topic', 'event.dummy', '1', '1' ), result[1] ) def test_list(self): self.client.event_triggers.list.return_value = [TRIGGER] result = self.call(event_triggers_cmd.List) self.assertEqual( [( '456', 'my_trigger', '123e4567-e89b-12d3-a456-426655440000', {}, 'dummy_exchange', 'dummy_topic', 'event.dummy', '1', '1' )], result[1] ) def test_get(self): self.client.event_triggers.get.return_value = TRIGGER result = self.call(event_triggers_cmd.Get, app_args=['id']) self.assertEqual( ( '456', 'my_trigger', '123e4567-e89b-12d3-a456-426655440000', {}, 'dummy_exchange', 'dummy_topic', 'event.dummy', '1', '1' ), result[1] ) def test_delete(self): self.call(event_triggers_cmd.Delete, app_args=['id']) self.client.event_triggers.delete.assert_called_once_with('id') def test_delete_with_multi_names(self): self.call(event_triggers_cmd.Delete, app_args=['id1', 'id2']) self.assertEqual(2, self.client.event_triggers.delete.call_count) self.assertEqual( [mock.call('id1'), mock.call('id2')], self.client.event_triggers.delete.call_args_list ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_executions.py0000664000175000017500000002514300000000000027710 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. # Copyright 2016 - Brocade Communications Systems, Inc. # Copyright 2020 - Nokia Software. # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # 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 pkg_resources as pkg from unittest import mock from oslo_serialization import jsonutils from mistralclient.api.v2 import executions from mistralclient.commands.v2 import executions as execution_cmd from mistralclient.tests.unit import base EXEC_DICT = { 'id': '123', 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', 'workflow_name': 'some', 'workflow_namespace': '', 'root_execution_id': '', 'description': '', 'state': 'SUCCESS', 'state_info': None, 'created_at': '2020-02-07 08:10:32', 'updated_at': '2020-02-07 08:10:41', 'task_execution_id': None } EXEC = executions.Execution(mock, EXEC_DICT) SUB_WF_EXEC = executions.Execution( mock, { 'id': '456', 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', 'workflow_name': 'some_sub_wf', 'workflow_namespace': '', 'root_execution_id': 'ROOT_EXECUTION_ID', 'description': '', 'state': 'ERROR', 'state_info': None, 'created_at': '2020-02-07 08:10:32', 'updated_at': '2020-02-07 08:10:41', 'task_execution_id': 'abc' } ) EX_RESULT = ( '123', '123e4567-e89b-12d3-a456-426655440000', 'some', '', '', '', '', 'SUCCESS', None, '2020-02-07 08:10:32', '2020-02-07 08:10:41', '0:00:09' ) SUB_WF_EX_RESULT = ( '456', '123e4567-e89b-12d3-a456-426655440000', 'some_sub_wf', '', '', 'abc', 'ROOT_EXECUTION_ID', 'ERROR', None, '2020-02-07 08:10:32', '2020-02-07 08:10:41', '0:00:09' ) EXECS_LIST = [EXEC, SUB_WF_EXEC] EXEC_PUBLISHED = {"bar1": "val1", "var2": 2} EXEC_WITH_PUBLISHED_DICT = EXEC_DICT.copy() EXEC_WITH_PUBLISHED_DICT.update( {'published_global': jsonutils.dumps(EXEC_PUBLISHED)}) EXEC_WITH_PUBLISHED = executions.Execution(mock, EXEC_WITH_PUBLISHED_DICT) class TestCLIExecutionsV2(base.BaseCommandTest): def setUp(self): super(TestCLIExecutionsV2, self).setUp() def tearDown(self): super(TestCLIExecutionsV2, self).tearDown() def test_create_wf_input_string(self): self.client.executions.create.return_value = EXEC result = self.call( execution_cmd.Create, app_args=['id', '{ "context": true }'] ) self.assertEqual(EX_RESULT, result[1]) def test_create_wf_input_file(self): self.client.executions.create.return_value = EXEC path = pkg.resource_filename( 'mistralclient', 'tests/unit/resources/ctx.json' ) result = self.call( execution_cmd.Create, app_args=['id', path] ) self.assertEqual(EX_RESULT, result[1]) def test_create_with_description(self): self.client.executions.create.return_value = EXEC result = self.call( execution_cmd.Create, app_args=['id', '{ "context": true }', '-d', ''] ) self.assertEqual(EX_RESULT, result[1]) def test_update_state(self): states = ['RUNNING', 'SUCCESS', 'PAUSED', 'ERROR', 'CANCELLED'] for state in states: self.client.executions.update.return_value = executions.Execution( mock, { 'id': '123', 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', 'workflow_name': 'some', 'workflow_namespace': '', 'root_execution_id': '', 'description': '', 'state': state, 'state_info': None, 'created_at': '2020-02-07 08:10:32', 'updated_at': '2020-02-07 08:10:41', 'task_execution_id': None } ) ex_result = list(EX_RESULT) ex_result[7] = state # We'll ignore "duration" since for not terminal states # it is unpredictable. del ex_result[11] ex_result = tuple(ex_result) result = self.call( execution_cmd.Update, app_args=['id', '-s', state] ) result_ex = list(result[1]) del result_ex[11] result_ex = tuple(result_ex) self.assertEqual(ex_result, result_ex) def test_update_invalid_state(self): states = ['IDLE', 'WAITING', 'DELAYED'] for state in states: self.assertRaises( SystemExit, self.call, execution_cmd.Update, app_args=['id', '-s', state] ) def test_resume_update_env(self): self.client.executions.update.return_value = EXEC result = self.call( execution_cmd.Update, app_args=['id', '-s', 'RUNNING', '--env', '{"k1": "foobar"}'] ) self.assertEqual(EX_RESULT, result[1]) def test_update_description(self): self.client.executions.update.return_value = EXEC result = self.call( execution_cmd.Update, app_args=['id', '-d', 'foobar'] ) self.assertEqual(EX_RESULT, result[1]) def test_list(self): self.client.executions.list.return_value = [SUB_WF_EXEC, EXEC] result = self.call(execution_cmd.List) self.assertEqual( [EX_RESULT, SUB_WF_EX_RESULT], result[1] ) def test_sub_executions(self): self.client.executions.get_ex_sub_executions.return_value = \ EXECS_LIST result = self.call( execution_cmd.SubExecutionsLister, app_args=[EXEC_DICT['id']] ) self.assertEqual([EX_RESULT, SUB_WF_EX_RESULT], result[1]) self.assertEqual( 1, self.client.executions.get_ex_sub_executions.call_count ) self.assertEqual( [mock.call(EXEC_DICT['id'], errors_only='', max_depth=-1)], self.client.executions.get_ex_sub_executions.call_args_list ) def test_sub_executions_errors_only(self): self.client.executions.get_ex_sub_executions.return_value = \ EXECS_LIST self.call( execution_cmd.SubExecutionsLister, app_args=[EXEC_DICT['id'], '--errors-only'] ) self.assertEqual( 1, self.client.executions.get_ex_sub_executions.call_count ) self.assertEqual( [mock.call(EXEC_DICT['id'], errors_only=True, max_depth=-1)], self.client.executions.get_ex_sub_executions.call_args_list ) def test_sub_executions_with_max_depth(self): self.client.executions.get_ex_sub_executions.return_value = \ EXECS_LIST self.call( execution_cmd.SubExecutionsLister, app_args=[EXEC_DICT['id'], '--max-depth', '3'] ) self.assertEqual( 1, self.client.executions.get_ex_sub_executions.call_count ) self.assertEqual( [mock.call(EXEC_DICT['id'], errors_only='', max_depth=3)], self.client.executions.get_ex_sub_executions.call_args_list ) def test_list_with_pagination(self): self.client.executions.list.return_value = [EXEC] self.call(execution_cmd.List) self.client.executions.list.assert_called_once_with( fields=execution_cmd.ExecutionFormatter.fields(), limit=100, marker='', nulls='', sort_dirs='desc', sort_keys='created_at', task=None ) self.call( execution_cmd.List, app_args=[ '--oldest' ] ) self.client.executions.list.assert_called_with( fields=execution_cmd.ExecutionFormatter.fields(), limit=100, marker='', nulls='', sort_keys='created_at', sort_dirs='asc', task=None ) self.call( execution_cmd.List, app_args=[ '--limit', '5', '--sort_keys', 'id, Workflow', '--sort_dirs', 'desc', '--marker', 'abc' ] ) self.client.executions.list.assert_called_with( fields=execution_cmd.ExecutionFormatter.fields(), limit=5, marker='abc', nulls='', sort_keys='id, Workflow', sort_dirs='desc', task=None ) def test_get(self): self.client.executions.get.return_value = EXEC result = self.call(execution_cmd.Get, app_args=['id']) self.assertEqual(EX_RESULT, result[1]) def test_get_sub_wf_ex(self): self.client.executions.get.return_value = SUB_WF_EXEC result = self.call(execution_cmd.Get, app_args=['id']) self.assertEqual(SUB_WF_EX_RESULT, result[1]) def test_delete(self): self.call(execution_cmd.Delete, app_args=['id']) self.client.executions.delete.assert_called_once_with( 'id', force=False ) def test_delete_with_force(self): self.call(execution_cmd.Delete, app_args=['id', '--force']) self.client.executions.delete.assert_called_once_with( 'id', force=True ) def test_delete_with_multi_names(self): self.call(execution_cmd.Delete, app_args=['id1', 'id2']) self.assertEqual(2, self.client.executions.delete.call_count) self.assertEqual( [mock.call('id1', force=False), mock.call('id2', force=False)], self.client.executions.delete.call_args_list ) def test_get_published(self): self.client.executions.get.return_value = EXEC_WITH_PUBLISHED self.call(execution_cmd.GetPublished, app_args=['id']) self.assertDictEqual( EXEC_PUBLISHED, jsonutils.loads(self.app.stdout.write.call_args[0][0]) ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_members.py0000664000175000017500000000604100000000000027150 0ustar00zuulzuul00000000000000# Copyright 2016 Catalyst IT Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 mistralclient.api.v2 import members from mistralclient.commands.v2 import members as member_cmd from mistralclient.tests.unit import base MEMBER_DICT = { 'id': '123', 'resource_id': '456', 'resource_type': 'workflow', 'project_id': '1111', 'member_id': '2222', 'status': 'pending', 'created_at': '1', 'updated_at': '1' } MEMBER = members.Member(mock, MEMBER_DICT) class TestCLIWorkflowMembers(base.BaseCommandTest): def test_create(self): self.client.members.create.return_value = MEMBER result = self.call( member_cmd.Create, app_args=[MEMBER_DICT['resource_id'], MEMBER_DICT['resource_type'], MEMBER_DICT['member_id']] ) self.assertEqual( ('456', 'workflow', '1111', '2222', 'pending', '1', '1'), result[1] ) def test_update(self): self.client.members.update.return_value = MEMBER result = self.call( member_cmd.Update, app_args=[MEMBER_DICT['resource_id'], MEMBER_DICT['resource_type'], '-m', MEMBER_DICT['member_id']] ) self.assertEqual( ('456', 'workflow', '1111', '2222', 'pending', '1', '1'), result[1] ) def test_list(self): self.client.members.list.return_value = [MEMBER] result = self.call( member_cmd.List, app_args=[MEMBER_DICT['resource_id'], MEMBER_DICT['resource_type']] ) self.assertListEqual( [('456', 'workflow', '1111', '2222', 'pending', '1', '1')], result[1] ) def test_get(self): self.client.members.get.return_value = MEMBER result = self.call( member_cmd.Get, app_args=[MEMBER_DICT['resource_id'], MEMBER_DICT['resource_type'], '-m', MEMBER_DICT['member_id']] ) self.assertEqual( ('456', 'workflow', '1111', '2222', 'pending', '1', '1'), result[1] ) def test_delete(self): self.call( member_cmd.Delete, app_args=[MEMBER_DICT['resource_id'], MEMBER_DICT['resource_type'], MEMBER_DICT['member_id']] ) self.client.members.delete.assert_called_once_with( MEMBER_DICT['resource_id'], MEMBER_DICT['resource_type'], MEMBER_DICT['member_id'] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_services.py0000664000175000017500000000230000000000000027333 0ustar00zuulzuul00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 mistralclient.api.v2 import services from mistralclient.commands.v2 import services as service_cmd from mistralclient.tests.unit import base SERVICE_DICT = { 'name': 'service_name', 'type': 'service_type', } SERVICE = services.Service(mock, SERVICE_DICT) class TestCLIServicesV2(base.BaseCommandTest): def test_list(self): self.client.services.list.return_value = [SERVICE] expected = (SERVICE_DICT['name'], SERVICE_DICT['type'],) result = self.call(service_cmd.List) self.assertListEqual([expected], result[1]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_tasks.py0000664000175000017500000001513100000000000026643 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. # All Rights Reserved # # Licensed under the Apache License, Version 2.0 (the "License"); you may # not use this file except in compliance with the License. You may obtain # a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, WITHOUT # WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the # License for the specific language governing permissions and limitations # under the License. # from unittest import mock from oslo_serialization import jsonutils from mistralclient.api.v2.executions import Execution from mistralclient.api.v2 import tasks from mistralclient.commands.v2 import tasks as task_cmd from mistralclient.tests.unit import base TASK_DICT = { 'id': '123', 'name': 'some', 'workflow_name': 'thing', 'workflow_namespace': '', 'workflow_execution_id': '321', 'state': 'SUCCESS', 'state_info': None, 'created_at': '2020-02-07 08:10:32', 'started_at': '2020-02-07 08:10:32', 'finished_at': '2020-02-07 08:10:41' } TASK_SUB_WF_EXEC = Execution( mock, { 'id': '456', 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', 'workflow_name': 'some_sub_wf', 'workflow_namespace': '', 'root_execution_id': 'ROOT_EXECUTION_ID', 'description': '', 'state': 'ERROR', 'state_info': None, 'created_at': '2020-02-07 08:10:32', 'updated_at': '2020-02-07 08:10:41', 'task_execution_id': '123' } ) TASK_SUB_WF_EX_RESULT = ( '456', '123e4567-e89b-12d3-a456-426655440000', 'some_sub_wf', '', '', '123', 'ROOT_EXECUTION_ID', 'ERROR', None, '2020-02-07 08:10:32', '2020-02-07 08:10:41', '0:00:09' ) TASK_RESULT = {"test": "is", "passed": "successfully"} TASK_PUBLISHED = {"bar1": "val1", "var2": 2} TASK_WITH_RESULT_DICT = TASK_DICT.copy() TASK_WITH_RESULT_DICT.update({'result': jsonutils.dumps(TASK_RESULT)}) TASK_WITH_PUBLISHED_DICT = TASK_DICT.copy() TASK_WITH_PUBLISHED_DICT.update({'published': jsonutils.dumps(TASK_PUBLISHED)}) TASK = tasks.Task(mock, TASK_DICT) TASK_WITH_RESULT = tasks.Task(mock, TASK_WITH_RESULT_DICT) TASK_WITH_PUBLISHED = tasks.Task(mock, TASK_WITH_PUBLISHED_DICT) EXPECTED_TASK_RESULT = ( '123', 'some', 'thing', '', '321', 'SUCCESS', None, '2020-02-07 08:10:32', '2020-02-07 08:10:32', '2020-02-07 08:10:41', '0:00:09' ) class TestCLITasksV2(base.BaseCommandTest): def test_list(self): self.client.tasks.list.return_value = [TASK] result = self.call(task_cmd.List) self.assertEqual([EXPECTED_TASK_RESULT], result[1]) self.assertEqual( self.client.tasks.list.call_args[1]["fields"], task_cmd.TaskFormatter.fields() ) def test_list_with_workflow_execution(self): self.client.tasks.list.return_value = [TASK] result = self.call(task_cmd.List, app_args=['workflow_execution']) self.assertEqual([EXPECTED_TASK_RESULT], result[1]) def test_get(self): self.client.tasks.get.return_value = TASK result = self.call(task_cmd.Get, app_args=['id']) self.assertEqual(EXPECTED_TASK_RESULT, result[1]) def test_get_result(self): self.client.tasks.get.return_value = TASK_WITH_RESULT self.call(task_cmd.GetResult, app_args=['id']) self.assertDictEqual( TASK_RESULT, jsonutils.loads(self.app.stdout.write.call_args[0][0]) ) def test_get_published(self): self.client.tasks.get.return_value = TASK_WITH_PUBLISHED self.call(task_cmd.GetPublished, app_args=['id']) self.assertDictEqual( TASK_PUBLISHED, jsonutils.loads(self.app.stdout.write.call_args[0][0]) ) def test_rerun(self): self.client.tasks.rerun.return_value = TASK result = self.call(task_cmd.Rerun, app_args=['id']) self.assertEqual(EXPECTED_TASK_RESULT, result[1]) def test_rerun_no_reset(self): self.client.tasks.rerun.return_value = TASK result = self.call(task_cmd.Rerun, app_args=['id', '--resume']) self.assertEqual(EXPECTED_TASK_RESULT, result[1]) def test_rerun_update_env(self): self.client.tasks.rerun.return_value = TASK result = self.call( task_cmd.Rerun, app_args=['id', '--env', '{"k1": "foobar"}'] ) self.assertEqual(EXPECTED_TASK_RESULT, result[1]) def test_rerun_no_reset_update_env(self): self.client.tasks.rerun.return_value = TASK result = self.call( task_cmd.Rerun, app_args=['id', '--resume', '--env', '{"k1": "foobar"}'] ) self.assertEqual(EXPECTED_TASK_RESULT, result[1]) def test_sub_executions(self): self.client.tasks.get_task_sub_executions.return_value = \ TASK_SUB_WF_EXEC result = self.call( task_cmd.SubExecutionsLister, app_args=[TASK_DICT['id']] ) self.assertEqual([TASK_SUB_WF_EX_RESULT], result[1]) self.assertEqual( 1, self.client.tasks.get_task_sub_executions.call_count ) self.assertEqual( [mock.call(TASK_DICT['id'], errors_only='', max_depth=-1)], self.client.tasks.get_task_sub_executions.call_args_list ) def test_sub_executions_errors_only(self): self.client.tasks.get_task_sub_executions.return_value = \ TASK_SUB_WF_EXEC self.call( task_cmd.SubExecutionsLister, app_args=[TASK_DICT['id'], '--errors-only'] ) self.assertEqual( 1, self.client.tasks.get_task_sub_executions.call_count ) self.assertEqual( [mock.call(TASK_DICT['id'], errors_only=True, max_depth=-1)], self.client.tasks.get_task_sub_executions.call_args_list ) def test_sub_executions_with_max_depth(self): self.client.tasks.get_task_sub_executions.return_value = \ TASK_SUB_WF_EXEC self.call( task_cmd.SubExecutionsLister, app_args=[TASK_DICT['id'], '--max-depth', '3'] ) self.assertEqual( 1, self.client.tasks.get_task_sub_executions.call_count ) self.assertEqual( [mock.call(TASK_DICT['id'], errors_only='', max_depth=3)], self.client.tasks.get_task_sub_executions.call_args_list ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_workbooks.py0000664000175000017500000001164400000000000027543 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock from mistralclient.api.v2 import workbooks from mistralclient.commands.v2 import workbooks as workbook_cmd from mistralclient.tests.unit import base WORKBOOK_DICT = { 'name': 'a', 'namespace': '', 'tags': ['a', 'b'], 'scope': 'private', 'created_at': '1', 'updated_at': '1', } WB_DEF = """ --- version: '2.0 name: wb workflows: wf1: tasks: task1: action: nova.servers_get server="1" """ WB_WITH_DEF_DICT = WORKBOOK_DICT.copy() WB_WITH_DEF_DICT.update({'definition': WB_DEF}) WORKBOOK = workbooks.Workbook(mock, WORKBOOK_DICT) WORKBOOK_WITH_DEF = workbooks.Workbook(mock, WB_WITH_DEF_DICT) class TestCLIWorkbooksV2(base.BaseCommandTest): @mock.patch('argparse.open', create=True) def test_create(self, mock_open): self.client.workbooks.create.return_value = WORKBOOK result = self.call(workbook_cmd.Create, app_args=['wb.yaml']) self.assertEqual(('a', '', 'a, b', 'private', '1', '1'), result[1]) @mock.patch('argparse.open', create=True) def test_create_public(self, mock_open): wb_public_dict = WORKBOOK_DICT.copy() wb_public_dict['scope'] = 'public' workbook_public = workbooks.Workbook(mock, wb_public_dict) self.client.workbooks.create.return_value = workbook_public result = self.call( workbook_cmd.Create, app_args=['wb.yaml', '--public'] ) self.assertEqual(('a', '', 'a, b', 'public', '1', '1'), result[1]) self.assertEqual( 'public', self.client.workbooks.create.call_args[1]['scope'] ) @mock.patch('argparse.open', create=True) def test_update(self, mock_open): self.client.workbooks.update.return_value = WORKBOOK result = self.call(workbook_cmd.Update, app_args=['definition']) self.assertEqual(('a', '', 'a, b', 'private', '1', '1'), result[1]) @mock.patch('argparse.open', create=True) def test_update_public(self, mock_open): wb_public_dict = WORKBOOK_DICT.copy() wb_public_dict['scope'] = 'public' workbook_public = workbooks.Workbook(mock, wb_public_dict) self.client.workbooks.update.return_value = workbook_public result = self.call( workbook_cmd.Update, app_args=['definition', '--public'] ) self.assertEqual(('a', '', 'a, b', 'public', '1', '1'), result[1]) self.assertEqual( 'public', self.client.workbooks.update.call_args[1]['scope'] ) def test_list(self): self.client.workbooks.list.return_value = [WORKBOOK] result = self.call(workbook_cmd.List) self.assertEqual([('a', '', 'a, b', 'private', '1', '1')], result[1]) def test_get(self): self.client.workbooks.get.return_value = WORKBOOK result = self.call(workbook_cmd.Get, app_args=['name']) self.assertEqual(('a', '', 'a, b', 'private', '1', '1'), result[1]) def test_delete(self): self.call(workbook_cmd.Delete, app_args=['name']) self.client.workbooks.delete.assert_called_once_with('name', None) def test_delete_with_multi_names(self): self.call(workbook_cmd.Delete, app_args=['name1', 'name2']) self.assertEqual(2, self.client.workbooks.delete.call_count) self.assertEqual( [mock.call('name1', None), mock.call('name2', None)], self.client.workbooks.delete.call_args_list ) def test_get_definition(self): self.client.workbooks.get.return_value = WORKBOOK_WITH_DEF self.call(workbook_cmd.GetDefinition, app_args=['name']) self.app.stdout.write.assert_called_with(WB_DEF) @mock.patch('argparse.open', create=True) def test_validate(self, mock_open): self.client.workbooks.validate.return_value = {'valid': True} result = self.call(workbook_cmd.Validate, app_args=['wb.yaml']) self.assertEqual((True, None), result[1]) @mock.patch('argparse.open', create=True) def test_validate_failed(self, mock_open): self.client.workbooks.validate.return_value = { 'valid': False, 'error': 'Invalid DSL...' } result = self.call(workbook_cmd.Validate, app_args=['wb.yaml']) self.assertEqual((False, 'Invalid DSL...'), result[1]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_cli_workflows.py0000664000175000017500000001457700000000000027570 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, Inc. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. from unittest import mock from mistralclient.api.v2 import workflows from mistralclient.commands.v2 import base as cmd_base from mistralclient.commands.v2 import workflows as workflow_cmd from mistralclient.tests.unit import base WORKFLOW_DICT = { 'id': '1-2-3-4', 'name': 'a', 'namespace': '', 'project_id': '12345', 'tags': ['a', 'b'], 'input': 'param', 'scope': 'private', 'created_at': '1', 'updated_at': '1' } WF_DEF = """ version: '2.0' flow: tasks: task1: action: nova.servers_get server="1" """ WF_WITH_DEF_DICT = WORKFLOW_DICT.copy() WF_WITH_DEF_DICT.update({'definition': WF_DEF}) WORKFLOW = workflows.Workflow(mock, WORKFLOW_DICT) WORKFLOW_WITH_DEF = workflows.Workflow(mock, WF_WITH_DEF_DICT) class TestCLIWorkflowsV2(base.BaseCommandTest): @mock.patch('argparse.open', create=True) def test_create(self, mock_open): self.client.workflows.create.return_value = [WORKFLOW] result = self.call(workflow_cmd.Create, app_args=['1.txt']) self.assertEqual( [('1-2-3-4', 'a', '', '12345', 'a, b', 'param', 'private', '1', '1')], result[1] ) @mock.patch('argparse.open', create=True) def test_create_public(self, mock_open): wf_public_dict = WORKFLOW_DICT.copy() wf_public_dict['scope'] = 'public' workflow_public = workflows.Workflow(mock, wf_public_dict) self.client.workflows.create.return_value = [workflow_public] result = self.call( workflow_cmd.Create, app_args=['1.txt', '--public'] ) self.assertEqual( [('1-2-3-4', 'a', '', '12345', 'a, b', 'param', 'public', '1', '1')], result[1] ) self.assertEqual( 'public', self.client.workflows.create.call_args[1]['scope'] ) @mock.patch('argparse.open', create=True) def test_create_long_input(self, mock_open): wf_long_input_dict = WORKFLOW_DICT.copy() long_input = ', '.join( ['var%s' % i for i in range(10)] ) wf_long_input_dict['input'] = long_input workflow_long_input = workflows.Workflow(mock, wf_long_input_dict) self.client.workflows.create.return_value = [workflow_long_input] result = self.call(workflow_cmd.Create, app_args=['1.txt']) self.assertEqual( [('1-2-3-4', 'a', '', '12345', 'a, b', cmd_base.cut(long_input), 'private', '1', '1')], result[1] ) @mock.patch('argparse.open', create=True) def test_update(self, mock_open): self.client.workflows.update.return_value = [WORKFLOW] result = self.call(workflow_cmd.Update, app_args=['1.txt']) self.assertEqual( [('1-2-3-4', 'a', '', '12345', 'a, b', 'param', 'private', '1', '1')], result[1] ) @mock.patch('argparse.open', create=True) def test_update_public(self, mock_open): self.client.workflows.update.return_value = [WORKFLOW] result = self.call( workflow_cmd.Update, app_args=['1.txt', '--public'] ) self.assertEqual( [('1-2-3-4', 'a', '', '12345', 'a, b', 'param', 'private', '1', '1')], result[1] ) self.assertEqual( 'public', self.client.workflows.update.call_args[1]['scope'] ) @mock.patch('argparse.open', create=True) def test_update_with_id(self, mock_open): self.client.workflows.update.return_value = WORKFLOW result = self.call( workflow_cmd.Update, app_args=['1.txt', '--id', '1-2-3-4'] ) self.assertEqual( [('1-2-3-4', 'a', '', '12345', 'a, b', 'param', 'private', '1', '1')], result[1] ) def test_list(self): self.client.workflows.list.return_value = [WORKFLOW] result = self.call(workflow_cmd.List) self.assertEqual( [('1-2-3-4', 'a', '', '12345', 'a, b', 'param', 'private', '1', '1')], result[1] ) def test_get(self): self.client.workflows.get.return_value = WORKFLOW result = self.call(workflow_cmd.Get, app_args=['name']) self.assertEqual( ('1-2-3-4', 'a', '', '12345', 'a, b', 'param', 'private', '1', '1'), result[1] ) def test_delete(self): self.call(workflow_cmd.Delete, app_args=['name']) self.client.workflows.delete.assert_called_once_with('name', None) def test_delete_with_multi_names(self): self.call(workflow_cmd.Delete, app_args=['name1', 'name2']) self.assertEqual(2, self.client.workflows.delete.call_count) self.assertEqual( [mock.call('name1', None), mock.call('name2', None)], self.client.workflows.delete.call_args_list ) def test_get_definition(self): self.client.workflows.get.return_value = WORKFLOW_WITH_DEF self.call(workflow_cmd.GetDefinition, app_args=['name']) self.app.stdout.write.assert_called_with(WF_DEF) @mock.patch('argparse.open', create=True) def test_validate(self, mock_open): self.client.workflows.validate.return_value = {'valid': True} result = self.call(workflow_cmd.Validate, app_args=['wf.yaml']) self.assertEqual((True, None), result[1]) @mock.patch('argparse.open', create=True) def test_validate_failed(self, mock_open): self.client.workflows.validate.return_value = { 'valid': False, 'error': 'Invalid DSL...' } result = self.call(workflow_cmd.Validate, app_args=['wf.yaml']) self.assertEqual((False, 'Invalid DSL...'), result[1]) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_environments.py0000664000175000017500000001302500000000000027416 0ustar00zuulzuul00000000000000# Copyright 2015 - StackStorm, 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 copy import os.path from oslo_serialization import jsonutils import pkg_resources as pkg from urllib import parse from urllib import request from mistralclient.api import base as api_base from mistralclient.api.v2 import environments from mistralclient.tests.unit.v2 import base from mistralclient import utils ENVIRONMENT = { 'name': 'env1', 'description': 'Test Environment #1', 'scope': 'private', 'variables': { 'server': 'localhost' } } URL_TEMPLATE = '/environments' URL_TEMPLATE_NAME = '/environments/%s' class TestEnvironmentsV2(base.BaseClientV2Test): def test_create(self): data = copy.deepcopy(ENVIRONMENT) self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, status_code=201, json=data) env = self.environments.create(**data) self.assertIsNotNone(env) expected_data = copy.deepcopy(data) expected_data['variables'] = jsonutils.dumps( expected_data['variables']) self.assertEqual(expected_data, self.requests_mock.last_request.json()) def test_create_with_json_file_uri(self): # The contents of env_v2.json must be equivalent to ENVIRONMENT path = pkg.resource_filename( 'mistralclient', 'tests/unit/resources/env_v2.json' ) path = os.path.abspath(path) # Convert the file path to file URI uri = parse.urljoin('file:', request.pathname2url(path)) data = collections.OrderedDict( utils.load_content( utils.get_contents_if_file(uri) ) ) self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, status_code=201, json=data) file_input = {'file': uri} env = self.environments.create(**file_input) self.assertIsNotNone(env) expected_data = copy.deepcopy(data) expected_data['variables'] = jsonutils.dumps( expected_data['variables']) self.assertEqual(expected_data, self.requests_mock.last_request.json()) def test_create_without_name(self): data = copy.deepcopy(ENVIRONMENT) data.pop('name') e = self.assertRaises(api_base.APIException, self.environments.create, **data) self.assertEqual(400, e.error_code) def test_update(self): data = copy.deepcopy(ENVIRONMENT) self.requests_mock.put(self.TEST_URL + URL_TEMPLATE, json=data) env = self.environments.update(**data) self.assertIsNotNone(env) expected_data = copy.deepcopy(data) expected_data['variables'] = jsonutils.dumps( expected_data['variables']) self.assertEqual(expected_data, self.requests_mock.last_request.json()) def test_update_with_yaml_file(self): # The contents of env_v2.json must be equivalent to ENVIRONMENT path = pkg.resource_filename( 'mistralclient', 'tests/unit/resources/env_v2.json' ) data = collections.OrderedDict( utils.load_content( utils.get_contents_if_file(path) ) ) self.requests_mock.put(self.TEST_URL + URL_TEMPLATE, json=data) file_input = {'file': path} env = self.environments.update(**file_input) self.assertIsNotNone(env) expected_data = copy.deepcopy(data) expected_data['variables'] = jsonutils.dumps( expected_data['variables']) self.assertEqual(expected_data, self.requests_mock.last_request.json()) def test_update_without_name(self): data = copy.deepcopy(ENVIRONMENT) data.pop('name') e = self.assertRaises(api_base.APIException, self.environments.update, **data) self.assertEqual(400, e.error_code) def test_list(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, json={'environments': [ENVIRONMENT]}) environment_list = self.environments.list() self.assertEqual(1, len(environment_list)) env = environment_list[0] self.assertDictEqual( environments.Environment(self.environments, ENVIRONMENT).to_dict(), env.to_dict() ) def test_get(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE_NAME % 'env', json=ENVIRONMENT) env = self.environments.get('env') self.assertIsNotNone(env) self.assertDictEqual( environments.Environment(self.environments, ENVIRONMENT).to_dict(), env.to_dict() ) def test_delete(self): self.requests_mock.delete(self.TEST_URL + URL_TEMPLATE_NAME % 'env', status_code=204) self.environments.delete('env') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_executions.py0000664000175000017500000002316300000000000027061 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, 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 from mistralclient.api import base as api_base from mistralclient.api.v2 import executions from mistralclient.tests.unit.v2 import base # TODO(everyone): Later we need additional tests verifying all the errors etc. EXEC = { 'id': "123", 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', 'workflow_name': 'my_wf', 'workflow_namespace': '', 'description': '', 'state': 'RUNNING', 'input': { "person": { "first_name": "John", "last_name": "Doe" } } } SUB_WF_EXEC = { 'id': "456", 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', 'workflow_name': 'my_sub_wf', 'workflow_namespace': '', 'task_execution_id': "abc", 'description': '', 'state': 'RUNNING', 'input': { "person": { "first_name": "John", "last_name": "Doe" } } } ERROR_SUB_WF_EXEC = { 'id': "456", 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', 'workflow_name': 'my_sub_wf', 'workflow_namespace': '', 'task_execution_id': "abc", 'description': '', 'state': 'ERROR', 'input': {} } SOURCE_EXEC = EXEC SOURCE_EXEC['source_execution_id'] = EXEC['workflow_id'] URL_TEMPLATE = '/executions' URL_TEMPLATE_ID = '/executions/%s' URL_TEMPLATE_SUB_EXECUTIONS = '/executions/%s/executions%s' class TestExecutionsV2(base.BaseClientV2Test): def test_create(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, json=EXEC, status_code=201) body = { 'workflow_name': EXEC['workflow_name'], 'description': '', 'input': jsonutils.dumps(EXEC['input']) } ex = self.executions.create( EXEC['workflow_name'], EXEC['workflow_namespace'], EXEC['input'] ) self.assertIsNotNone(ex) self.assertDictEqual( executions.Execution(self.executions, EXEC).to_dict(), ex.to_dict() ) self.assertDictEqual(body, self.requests_mock.last_request.json()) def test_create_with_workflow_id(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, json=EXEC, status_code=201) body = { 'workflow_id': EXEC['workflow_id'], 'description': '', 'input': jsonutils.dumps(EXEC['input']) } ex = self.executions.create( EXEC['workflow_id'], workflow_input=EXEC['input'] ) self.assertIsNotNone(ex) self.assertDictEqual( executions.Execution(self.executions, EXEC).to_dict(), ex.to_dict() ) self.assertDictEqual(body, self.requests_mock.last_request.json()) def test_create_with_source_execution_id(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, json=SOURCE_EXEC, status_code=201) body = { 'description': '', 'source_execution_id': SOURCE_EXEC['source_execution_id'] } ex = self.executions.create( source_execution_id=SOURCE_EXEC['source_execution_id'] ) self.assertIsNotNone(ex) self.assertDictEqual( executions.Execution(self.executions, SOURCE_EXEC).to_dict(), ex.to_dict() ) self.assertDictEqual(body, self.requests_mock.last_request.json()) def test_create_failure1(self): self.requests_mock.post(self.TEST_URL + URL_TEMPLATE, json=EXEC, status_code=201) self.assertRaises(api_base.APIException, self.executions.create, '') def test_update(self): url = self.TEST_URL + URL_TEMPLATE_ID % EXEC['id'] self.requests_mock.put(url, json=EXEC) body = { 'state': EXEC['state'] } ex = self.executions.update(EXEC['id'], EXEC['state']) self.assertIsNotNone(ex) self.assertDictEqual( executions.Execution(self.executions, EXEC).to_dict(), ex.to_dict() ) self.assertDictEqual(body, self.requests_mock.last_request.json()) def test_update_env(self): url = self.TEST_URL + URL_TEMPLATE_ID % EXEC['id'] self.requests_mock.put(url, json=EXEC) body = { 'state': EXEC['state'], 'params': { 'env': {'k1': 'foobar'} } } ex = self.executions.update( EXEC['id'], EXEC['state'], env={'k1': 'foobar'} ) self.assertIsNotNone(ex) self.assertDictEqual( executions.Execution(self.executions, EXEC).to_dict(), ex.to_dict() ) self.assertDictEqual(body, self.requests_mock.last_request.json()) def test_list(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, json={'executions': [EXEC, SUB_WF_EXEC]}) execution_list = self.executions.list() self.assertEqual(2, len(execution_list)) self.assertDictEqual( executions.Execution(self.executions, EXEC).to_dict(), execution_list[0].to_dict() ) self.assertDictEqual( executions.Execution(self.executions, SUB_WF_EXEC).to_dict(), execution_list[1].to_dict() ) def test_list_with_pagination(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, json={'executions': [EXEC], 'next': '/executions?fake'}) execution_list = self.executions.list( limit=1, sort_keys='created_at', sort_dirs='asc' ) self.assertEqual(1, len(execution_list)) last_request = self.requests_mock.last_request # The url param order is unpredictable. self.assertEqual(['1'], last_request.qs['limit']) self.assertEqual(['created_at'], last_request.qs['sort_keys']) self.assertEqual(['asc'], last_request.qs['sort_dirs']) def test_list_with_no_limit(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, json={'executions': [EXEC]}) execution_list = self.executions.list(limit=-1) self.assertEqual(1, len(execution_list)) last_request = self.requests_mock.last_request self.assertNotIn('limit', last_request.qs) def test_get(self): url = self.TEST_URL + URL_TEMPLATE_ID % EXEC['id'] self.requests_mock.get(url, json=EXEC) ex = self.executions.get(EXEC['id']) self.assertDictEqual( executions.Execution(self.executions, EXEC).to_dict(), ex.to_dict() ) def test_get_sub_wf_ex(self): url = self.TEST_URL + URL_TEMPLATE_ID % SUB_WF_EXEC['id'] self.requests_mock.get(url, json=SUB_WF_EXEC) ex = self.executions.get(SUB_WF_EXEC['id']) self.assertDictEqual( executions.Execution(self.executions, SUB_WF_EXEC).to_dict(), ex.to_dict() ) def test_delete_with_force(self): url = self.TEST_URL + URL_TEMPLATE_ID % EXEC['id'] self.requests_mock.delete(url, status_code=204) self.executions.delete(EXEC['id'], force=True) def test_delete(self): url = self.TEST_URL + URL_TEMPLATE_ID % EXEC['id'] self.requests_mock.delete(url, status_code=204) self.executions.delete(EXEC['id']) def test_report_statistics_only(self): url = self.TEST_URL + URL_TEMPLATE_ID % EXEC['id'] \ + '/report?statistics_only=True' expected_json = { 'statistics': {} } self.requests_mock.get(url, json=expected_json) report = self.executions.get_report(EXEC['id'], statistics_only=True) self.assertDictEqual(expected_json, report) def test_report(self): url = self.TEST_URL + URL_TEMPLATE_ID % EXEC['id'] + '/report' expected_json = { 'root_workflow_execution': {}, 'statistics': {} } self.requests_mock.get(url, json=expected_json) report = self.executions.get_report(EXEC['id']) self.assertDictEqual(expected_json, report) def test_get_sub_executions(self): url = self.TEST_URL + URL_TEMPLATE_SUB_EXECUTIONS \ % (EXEC['id'], '?max_depth=-1&errors_only=') self.requests_mock.get(url, json={'executions': [EXEC, SUB_WF_EXEC]}) sub_execution_list = self.executions.get_ex_sub_executions(EXEC['id']) self.assertEqual(2, len(sub_execution_list)) self.assertDictEqual( executions.Execution(self.executions, EXEC).to_dict(), sub_execution_list[0].to_dict() ) self.assertDictEqual( executions.Execution(self.executions, SUB_WF_EXEC).to_dict(), sub_execution_list[1].to_dict() ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_keystone.py0000664000175000017500000000505600000000000026535 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 mistralclient.auth.keystone from mistralclient.tests.unit.v2 import base class TestKeystone(base.BaseClientV2Test): def setUp(self): super(TestKeystone, self).setUp() self.keystone = mistralclient.auth.keystone.KeystoneAuthHandler() def test_get_auth_token(self): auth = self.keystone._get_auth( auth_token="token", auth_url="url", project_id="project_id", ) self.assertEqual("url", auth.auth_url) elements = auth.get_cache_id_elements() self.assertIsNotNone(elements["token"]) self.assertIsNotNone(elements["project_id"]) def test_remove_domain(self): params = { "param1": "p", "target_param2": "p2", "user_domain_param3": "p3", "target_project_domain_param4": "p4" } dedomained = self.keystone._remove_domain(params) self.assertIn("param1", dedomained) self.assertIn("target_param2", dedomained) self.assertNotIn("user_domain_param3", dedomained) self.assertNotIn("target_project_domain_param4", dedomained) def test_separate_target_reqs(self): params = { "a": 1, "target_b": 2, "c": 3, "target_d": 4, "target_target": 5, "param_target": 6 } nontarget, target = self.keystone._separate_target_reqs(params) self.assertIn("a", nontarget) self.assertIn("c", nontarget) self.assertIn("param_target", nontarget) self.assertIn("b", target) self.assertIn("d", target) self.assertIn("target", target) def test_verify(self): self.assertTrue(self.keystone._verification_needed("", False)) self.assertFalse(self.keystone._verification_needed("", True)) self.assertFalse(self.keystone._verification_needed("cert", True)) self.assertEqual( self.keystone._verification_needed("cert", False), "cert" ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_members.py0000664000175000017500000000575200000000000026331 0ustar00zuulzuul00000000000000# Copyright 2016 Catalyst IT Limited # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. import copy from mistralclient.tests.unit.v2 import base MEMBER = { 'id': '123', 'resource_id': '456', 'resource_type': 'workflow', 'project_id': 'dc4ffdee54d74028b19b1b90e77aa84f', 'member_id': '04f61e967fa14cd49950ffe2319317ad', 'status': 'pending', } WORKFLOW_MEMBERS_URL = '/workflows/%s/members' % (MEMBER['resource_id']) WORKFLOW_MEMBER_URL = '/workflows/%s/members/%s' % ( MEMBER['resource_id'], MEMBER['member_id'] ) class TestWorkflowMembers(base.BaseClientV2Test): def test_create(self): self.requests_mock.post(self.TEST_URL + WORKFLOW_MEMBERS_URL, json=MEMBER, status_code=201) mb = self.members.create( MEMBER['resource_id'], MEMBER['resource_type'], MEMBER['member_id'] ) self.assertIsNotNone(mb) self.assertDictEqual({'member_id': MEMBER['member_id']}, self.requests_mock.last_request.json()) def test_update(self): updated_member = copy.copy(MEMBER) updated_member['status'] = 'accepted' self.requests_mock.put(self.TEST_URL + WORKFLOW_MEMBER_URL, json=updated_member) mb = self.members.update( MEMBER['resource_id'], MEMBER['resource_type'], MEMBER['member_id'] ) self.assertIsNotNone(mb) self.assertDictEqual({"status": "accepted"}, self.requests_mock.last_request.json()) def test_list(self): self.requests_mock.get(self.TEST_URL + WORKFLOW_MEMBERS_URL, json={'members': [MEMBER]}) mbs = self.members.list(MEMBER['resource_id'], MEMBER['resource_type']) self.assertEqual(1, len(mbs)) def test_get(self): self.requests_mock.get(self.TEST_URL + WORKFLOW_MEMBER_URL, json=MEMBER) mb = self.members.get( MEMBER['resource_id'], MEMBER['resource_type'], MEMBER['member_id'] ) self.assertIsNotNone(mb) def test_delete(self): self.requests_mock.delete(self.TEST_URL + WORKFLOW_MEMBER_URL, status_code=204) self.members.delete( MEMBER['resource_id'], MEMBER['resource_type'], MEMBER['member_id'] ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_services.py0000664000175000017500000000235100000000000026512 0ustar00zuulzuul00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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 mistralclient.api.v2 import services from mistralclient.tests.unit.v2 import base SERVICE = { 'name': 'service_name', 'type': 'service_type', } URL_TEMPLATE = '/services' class TestServicesV2(base.BaseClientV2Test): def test_list(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, json={'services': [SERVICE]}) service_list = self.services.list() self.assertEqual(1, len(service_list)) srv = service_list[0] self.assertDictEqual( services.Service(self.services, SERVICE).to_dict(), srv.to_dict() ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_tasks.py0000664000175000017500000001150500000000000026015 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, 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 from mistralclient.api.v2.executions import Execution from mistralclient.api.v2 import tasks from mistralclient.tests.unit.v2 import base # TODO(everyone): later we need additional tests verifying all the errors etc. TASK = { 'id': "1", 'workflow_execution_id': '123', 'name': 'my_task', 'workflow_name': 'my_wf', 'state': 'RUNNING', 'tags': ['deployment', 'demo'], 'result': {'some': 'result'} } SUB_WF_EXEC = { 'id': "456", 'workflow_id': '123e4567-e89b-12d3-a456-426655440000', 'workflow_name': 'my_sub_wf', 'workflow_namespace': '', 'task_execution_id': "1", 'description': '', 'state': 'RUNNING', 'input': {} } URL_TEMPLATE = '/tasks' URL_TEMPLATE_ID = '/tasks/%s' URL_TEMPLATE_SUB_EXECUTIONS = '/tasks/%s/executions%s' class TestTasksV2(base.BaseClientV2Test): def test_list(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, json={'tasks': [TASK]}) task_list = self.tasks.list() self.assertEqual(1, len(task_list)) task = task_list[0] self.assertEqual( tasks.Task(self.tasks, TASK).to_dict(), task.to_dict() ) def test_list_with_fields(self): field_params = "?fields=id,name" self.requests_mock.get(self.TEST_URL + URL_TEMPLATE + field_params, json={'tasks': [TASK]}) self.tasks.list(fields=["id,name"]) self.assertTrue(self.requests_mock.called_once) def test_list_with_no_limit(self): self.requests_mock.get(self.TEST_URL + URL_TEMPLATE, json={'tasks': [TASK]}) task_list = self.tasks.list(limit=-1) self.assertEqual(1, len(task_list)) last_request = self.requests_mock.last_request self.assertNotIn('limit', last_request.qs) def test_get(self): url = self.TEST_URL + URL_TEMPLATE_ID % TASK['id'] self.requests_mock.get(url, json=TASK) task = self.tasks.get(TASK['id']) self.assertEqual( tasks.Task(self.tasks, TASK).to_dict(), task.to_dict() ) def test_rerun(self): url = self.TEST_URL + URL_TEMPLATE_ID % TASK['id'] self.requests_mock.put(url, json=TASK) task = self.tasks.rerun(TASK['id']) self.assertDictEqual( tasks.Task(self.tasks, TASK).to_dict(), task.to_dict() ) body = { 'reset': True, 'state': 'RUNNING', 'id': TASK['id'] } self.assertDictEqual(body, self.requests_mock.last_request.json()) def test_rerun_no_reset(self): url = self.TEST_URL + URL_TEMPLATE_ID % TASK['id'] self.requests_mock.put(url, json=TASK) task = self.tasks.rerun(TASK['id'], reset=False) self.assertDictEqual( tasks.Task(self.tasks, TASK).to_dict(), task.to_dict() ) body = { 'reset': False, 'state': 'RUNNING', 'id': TASK['id'] } self.assertDictEqual(body, self.requests_mock.last_request.json()) def test_rerun_update_env(self): url = self.TEST_URL + URL_TEMPLATE_ID % TASK['id'] self.requests_mock.put(url, json=TASK) task = self.tasks.rerun(TASK['id'], env={'k1': 'foobar'}) self.assertDictEqual( tasks.Task(self.tasks, TASK).to_dict(), task.to_dict() ) body = { 'reset': True, 'state': 'RUNNING', 'id': TASK['id'], 'env': jsonutils.dumps({'k1': 'foobar'}) } self.assertDictEqual(body, self.requests_mock.last_request.json()) def test_get_sub_executions(self): url = self.TEST_URL + URL_TEMPLATE_SUB_EXECUTIONS \ % (TASK['id'], '?max_depth=-1&errors_only=') self.requests_mock.get(url, json={'executions': [SUB_WF_EXEC]}) sub_execution_list = self.tasks.get_task_sub_executions(TASK['id']) self.assertEqual(1, len(sub_execution_list)) self.assertDictEqual( Execution(self.executions, SUB_WF_EXEC).to_dict(), sub_execution_list[0].to_dict() ) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_workbooks.py0000664000175000017500000001652100000000000026713 0ustar00zuulzuul00000000000000# Copyright 2014 - Mirantis, Inc. # Copyright 2015 - StackStorm, 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.path import pkg_resources as pkg from urllib import parse from urllib import request from mistralclient.api import base as api_base from mistralclient.api.v2 import workbooks from mistralclient.tests.unit.v2 import base # TODO(everyone): later we need additional tests verifying all the errors etc. WB_DEF = """ --- version: 2.0 name: wb workflows: wf1: type: direct input: - param1 - param2 tasks: task1: action: std.http url="localhost:8989" on-success: - test_subsequent test_subsequent: action: std.http url="http://some_url" server_id=1 """ INVALID_WB_DEF = """ version: 2.0 name: wb workflows: wf1: type: direct tasks: task1: action: std.http url="localhost:8989" workflow: wf2 """ WORKBOOK = {'definition': WB_DEF} URL_TEMPLATE = '/workbooks' URL_TEMPLATE_NAME = '/workbooks/%s' URL_TEMPLATE_VALIDATE = '/workbooks/validate' class TestWorkbooksV2(base.BaseClientV2Test): def test_create(self): self.requests_mock.post( self.TEST_URL + URL_TEMPLATE, json=WORKBOOK, status_code=201 ) wb = self.workbooks.create(WB_DEF) self.assertIsNotNone(wb) self.assertEqual(WB_DEF, wb.definition) last_request = self.requests_mock.last_request self.assertEqual(WB_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_create_with_file_uri(self): self.requests_mock.post( self.TEST_URL + URL_TEMPLATE, json=WORKBOOK, status_code=201 ) # The contents of wb_v2.yaml must be identical to WB_DEF path = pkg.resource_filename( 'mistralclient', 'tests/unit/resources/wb_v2.yaml' ) path = os.path.abspath(path) # Convert the file path to file URI uri = parse.urljoin('file:', request.pathname2url(path)) wb = self.workbooks.create(uri) self.assertIsNotNone(wb) self.assertEqual(WB_DEF, wb.definition) last_request = self.requests_mock.last_request self.assertEqual(WB_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_update(self): self.requests_mock.put(self.TEST_URL + URL_TEMPLATE, json=WORKBOOK) wb = self.workbooks.update(WB_DEF) self.assertIsNotNone(wb) self.assertEqual(WB_DEF, wb.definition) last_request = self.requests_mock.last_request self.assertEqual(WB_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_update_with_file(self): self.requests_mock.put(self.TEST_URL + URL_TEMPLATE, json=WORKBOOK) # The contents of wb_v2.yaml must be identical to WB_DEF path = pkg.resource_filename( 'mistralclient', 'tests/unit/resources/wb_v2.yaml' ) wb = self.workbooks.update(path) self.assertIsNotNone(wb) self.assertEqual(WB_DEF, wb.definition) last_request = self.requests_mock.last_request self.assertEqual(WB_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_list(self): self.requests_mock.get( self.TEST_URL + URL_TEMPLATE, json={'workbooks': [WORKBOOK]} ) workbook_list = self.workbooks.list() self.assertEqual(1, len(workbook_list)) wb = workbook_list[0] self.assertEqual( workbooks.Workbook(self.workbooks, WORKBOOK).to_dict(), wb.to_dict() ) def test_get(self): self.requests_mock.get( self.TEST_URL + URL_TEMPLATE_NAME % 'wb', json=WORKBOOK ) wb = self.workbooks.get('wb') self.assertIsNotNone(wb) self.assertEqual( workbooks.Workbook(self.workbooks, WORKBOOK).to_dict(), wb.to_dict() ) def test_delete(self): url = self.TEST_URL + URL_TEMPLATE_NAME % 'wb' self.requests_mock.delete(url, status_code=204) self.workbooks.delete('wb') def test_validate(self): self.requests_mock.post( self.TEST_URL + URL_TEMPLATE_VALIDATE, json={'valid': True} ) result = self.workbooks.validate(WB_DEF) self.assertIsNotNone(result) self.assertIn('valid', result) self.assertTrue(result['valid']) last_request = self.requests_mock.last_request self.assertEqual(WB_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_validate_with_file(self): self.requests_mock.post( self.TEST_URL + URL_TEMPLATE_VALIDATE, json={'valid': True} ) # The contents of wb_v2.yaml must be identical to WB_DEF path = pkg.resource_filename( 'mistralclient', 'tests/unit/resources/wb_v2.yaml' ) result = self.workbooks.validate(path) self.assertIsNotNone(result) self.assertIn('valid', result) self.assertTrue(result['valid']) last_request = self.requests_mock.last_request self.assertEqual(WB_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_validate_failed(self): mock_result = { "valid": False, "error": "Task properties 'action' and 'workflow' " "can't be specified both" } self.requests_mock.post( self.TEST_URL + URL_TEMPLATE_VALIDATE, json=mock_result ) result = self.workbooks.validate(INVALID_WB_DEF) self.assertIsNotNone(result) self.assertIn('valid', result) self.assertFalse(result['valid']) self.assertIn('error', result) self.assertIn( "Task properties 'action' and 'workflow' " "can't be specified both", result['error'] ) last_request = self.requests_mock.last_request self.assertEqual(INVALID_WB_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_validate_api_failed(self): self.requests_mock.post( self.TEST_URL + URL_TEMPLATE_VALIDATE, status_code=500 ) self.assertRaises( api_base.APIException, self.workbooks.validate, WB_DEF ) last_request = self.requests_mock.last_request self.assertEqual(WB_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/tests/unit/v2/test_workflows.py0000664000175000017500000001424200000000000026726 0ustar00zuulzuul00000000000000# Copyright 2015 Huawei Technologies Co., Ltd. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT 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.path import pkg_resources as pkg from urllib import parse from urllib import request from mistralclient.api.v2 import workflows from mistralclient.tests.unit.v2 import base WF_DEF = """ --- version: 2.0 my_wf: type: direct tasks: task1: action: std.echo output="hello, world" """ WORKFLOW = { 'id': '123', 'name': 'my_wf', 'input': '', 'definition': WF_DEF } URL_TEMPLATE = '/workflows' URL_TEMPLATE_SCOPE = '/workflows?scope=private' URL_TEMPLATE_NAME = '/workflows/%s' class TestWorkflowsV2(base.BaseClientV2Test): def test_create(self): self.requests_mock.post( self.TEST_URL + URL_TEMPLATE_SCOPE, json={'workflows': [WORKFLOW]}, status_code=201 ) wfs = self.workflows.create(WF_DEF) self.assertIsNotNone(wfs) self.assertEqual(WF_DEF, wfs[0].definition) last_request = self.requests_mock.last_request self.assertEqual(WF_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_create_with_file(self): self.requests_mock.post( self.TEST_URL + URL_TEMPLATE_SCOPE, json={'workflows': [WORKFLOW]}, status_code=201 ) # The contents of wf_v2.yaml must be identical to WF_DEF path = pkg.resource_filename( 'mistralclient', 'tests/unit/resources/wf_v2.yaml' ) wfs = self.workflows.create(path) self.assertIsNotNone(wfs) self.assertEqual(WF_DEF, wfs[0].definition) last_request = self.requests_mock.last_request self.assertEqual(WF_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_update(self): self.requests_mock.put( self.TEST_URL + URL_TEMPLATE_SCOPE, json={'workflows': [WORKFLOW]} ) wfs = self.workflows.update(WF_DEF) self.assertIsNotNone(wfs) self.assertEqual(WF_DEF, wfs[0].definition) last_request = self.requests_mock.last_request self.assertEqual(WF_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_update_with_id(self): self.requests_mock.put( self.TEST_URL + URL_TEMPLATE_NAME % '123', json=WORKFLOW ) wf = self.workflows.update(WF_DEF, id='123') self.assertIsNotNone(wf) self.assertEqual(WF_DEF, wf.definition) last_request = self.requests_mock.last_request self.assertEqual('namespace=&scope=private', last_request.query) self.assertEqual(WF_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_update_with_file_uri(self): self.requests_mock.put( self.TEST_URL + URL_TEMPLATE_SCOPE, json={'workflows': [WORKFLOW]} ) # The contents of wf_v2.yaml must be identical to WF_DEF path = pkg.resource_filename( 'mistralclient', 'tests/unit/resources/wf_v2.yaml' ) path = os.path.abspath(path) # Convert the file path to file URI uri = parse.urljoin('file:', request.pathname2url(path)) wfs = self.workflows.update(uri) self.assertIsNotNone(wfs) self.assertEqual(WF_DEF, wfs[0].definition) last_request = self.requests_mock.last_request self.assertEqual(WF_DEF, last_request.text) self.assertEqual('text/plain', last_request.headers['content-type']) def test_list(self): self.requests_mock.get( self.TEST_URL + URL_TEMPLATE, json={'workflows': [WORKFLOW]} ) workflows_list = self.workflows.list() self.assertEqual(1, len(workflows_list)) wf = workflows_list[0] self.assertEqual( workflows.Workflow(self.workflows, WORKFLOW).to_dict(), wf.to_dict() ) def test_list_with_pagination(self): self.requests_mock.get( self.TEST_URL + URL_TEMPLATE, json={ 'workflows': [WORKFLOW], 'next': '/workflows?fake' } ) workflows_list = self.workflows.list( limit=1, sort_keys='created_at', sort_dirs='asc' ) self.assertEqual(1, len(workflows_list)) last_request = self.requests_mock.last_request # The url param order is unpredictable. self.assertEqual(['1'], last_request.qs['limit']) self.assertEqual(['created_at'], last_request.qs['sort_keys']) self.assertEqual(['asc'], last_request.qs['sort_dirs']) def test_list_with_no_limit(self): self.requests_mock.get( self.TEST_URL + URL_TEMPLATE, json={'workflows': [WORKFLOW]} ) workflows_list = self.workflows.list(limit=-1) self.assertEqual(1, len(workflows_list)) last_request = self.requests_mock.last_request self.assertNotIn('limit', last_request.qs) def test_get(self): url = self.TEST_URL + URL_TEMPLATE_NAME % 'wf' self.requests_mock.get(url, json=WORKFLOW) wf = self.workflows.get('wf') self.assertIsNotNone(wf) self.assertEqual( workflows.Workflow(self.workflows, WORKFLOW).to_dict(), wf.to_dict() ) def test_delete(self): self.requests_mock.delete( self.TEST_URL + URL_TEMPLATE_NAME % 'wf', status_code=204 ) self.workflows.delete('wf') ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/mistralclient/utils.py0000664000175000017500000000514600000000000022325 0ustar00zuulzuul00000000000000# Copyright 2015 - Huawei Technologies Co. Ltd # Copyright 2015 - StackStorm, 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 yaml from oslo_serialization import jsonutils from urllib import parse from urllib import request from mistralclient import exceptions def do_action_on_many(action, resources, success_msg, error_msg): """Helper to run an action on many resources.""" failure_flag = False for resource in resources: try: action(resource) print(success_msg % resource) except Exception as e: failure_flag = True print(e) if failure_flag: raise exceptions.MistralClientException(error_msg) def load_content(content): if content is None or content == '': return dict() try: data = yaml.safe_load(content) except Exception: data = jsonutils.loads(content) return data def load_file(path): with open(path, 'r') as f: return load_content(f.read()) def get_contents_if_file(contents_or_file_name): """Get the contents of a file. If the value passed in is a file name or file URI, return the contents. If not, or there is an error reading the file contents, return the value passed in as the contents. For example, a workflow definition will be returned if either the workflow definition file name, or file URI are passed in, or the actual workflow definition itself is passed in. """ try: if parse.urlparse(contents_or_file_name).scheme: definition_url = contents_or_file_name else: path = os.path.abspath(contents_or_file_name) definition_url = parse.urljoin('file:', request.pathname2url(path)) return request.urlopen(definition_url).read().decode('utf8') except Exception: return contents_or_file_name def load_json(input_string): try: # binary mode is needed due to bug/1515231 with open(input_string, 'r+b') as fh: return jsonutils.load(fh) except IOError: return jsonutils.loads(input_string) ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8605468 python-mistralclient-5.3.0/python_mistralclient.egg-info/0000775000175000017500000000000000000000000023700 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030372.0 python-mistralclient-5.3.0/python_mistralclient.egg-info/PKG-INFO0000664000175000017500000001134200000000000024776 0ustar00zuulzuul00000000000000Metadata-Version: 1.2 Name: python-mistralclient Version: 5.3.0 Summary: Mistral Client Library Home-page: https://docs.openstack.org/python-mistralclient/latest/ Author: OpenStack Author-email: openstack-discuss@lists.openstack.org License: Apache Software License Description: ======================== Team and repository tags ======================== .. image:: https://governance.openstack.org/tc/badges/python-mistralclient.svg :target: https://governance.openstack.org/tc/reference/tags/index.html Mistral ======= .. image:: https://img.shields.io/pypi/v/python-mistralclient.svg :target: https://pypi.org/project/python-mistralclient/ :alt: Latest Version Mistral is a workflow service. Most business processes consist of multiple distinct interconnected steps that need to be executed in a particular order in a distributed environment. A user can describe such a process as a set of tasks and their transitions. After that, it is possible to upload such a description to Mistral, which will take care of state management, correct execution order, parallelism, synchronization and high availability. Mistral also provides flexible task scheduling so that it can run a process according to a specified schedule (for example, every Sunday at 4.00pm) instead of running it immediately. In Mistral terminology such a set of tasks and relations between them is called a workflow. Mistral client ============== Python client for Mistral REST API. Includes python library for Mistral API and Command Line Interface (CLI) library. Installation ------------ First of all, clone the repo and go to the repo directory:: $ git clone https://opendev.org/openstack/python-mistralclient.git $ cd python-mistralclient Then just run:: $ pip install -e . or:: $ pip install -r requirements.txt $ python setup.py install Running Mistral client ---------------------- If Mistral authentication is enabled, provide the information about OpenStack auth to environment variables. Type:: $ export OS_AUTH_URL=http://:5000/v2.0 $ export OS_USERNAME=admin $ export OS_TENANT_NAME=tenant $ export OS_PASSWORD=secret $ export OS_MISTRAL_URL=http://:8989/v2 (optional, by default URL=http://localhost:8989/v2) and in the case that you are authenticating against keystone over https: $ export OS_CACERT= .. note:: In client, we can use both Keystone auth versions - v2.0 and v3. But server supports only v3.* To make sure Mistral client works, type:: $ mistral workbook-list You can see the list of available commands typing:: $ mistral --help Useful Links ============ * `PyPi`_ - package installation * `Launchpad project`_ - release management * `Blueprints`_ - feature specifications * `Bugs`_ - issue tracking * `Source`_ * `Specs`_ * `How to Contribute`_ .. _PyPi: https://pypi.org/project/python-mistralclient .. _Launchpad project: https://launchpad.net/python-mistralclient .. _Blueprints: https://blueprints.launchpad.net/python-mistralclient .. _Bugs: https://bugs.launchpad.net/python-mistralclient .. _Source: https://opendev.org/openstack/python-mistralclient .. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html .. _Specs: https://specs.openstack.org/openstack/mistral-specs/ .. _Release Notes: https://docs.openstack.org/releasenotes/python-mistralclient Platform: UNKNOWN Classifier: Programming Language :: Python Classifier: Programming Language :: Python :: Implementation :: CPython Classifier: Programming Language :: Python :: 3 :: Only Classifier: Programming Language :: Python :: 3 Classifier: Programming Language :: Python :: 3.8 Classifier: Programming Language :: Python :: 3.9 Classifier: Environment :: OpenStack Classifier: Intended Audience :: Information Technology Classifier: Intended Audience :: System Administrators Classifier: License :: OSI Approved :: Apache Software License Classifier: Operating System :: POSIX :: Linux Requires-Python: >=3.8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030372.0 python-mistralclient-5.3.0/python_mistralclient.egg-info/SOURCES.txt0000664000175000017500000001551600000000000025574 0ustar00zuulzuul00000000000000.coveragerc .stestr.conf .zuul.yaml AUTHORS CONTRIBUTING.rst ChangeLog LICENSE README.rst functional_creds.conf.sample lower-constraints.txt requirements.txt run_functional_tests.sh setup.cfg setup.py test-requirements.txt tox.ini doc/requirements.txt doc/source/class_reference.rst doc/source/conf.py doc/source/index.rst doc/source/cli/cli_usage_source_execution.rst doc/source/cli/cli_usage_targeting_workflows.rst doc/source/cli/cli_usage_with_keycloak.rst doc/source/cli/cli_usage_with_openstack.rst doc/source/contributor/contributing.rst functionaltests/resources/v2/action_v2.yaml functionaltests/resources/v2/action_v2_tags.yaml functionaltests/resources/v2/async.yaml functionaltests/resources/v2/wb_v2.yaml functionaltests/resources/v2/wb_with_tags_v2.yaml functionaltests/resources/v2/wf_delay_v2.yaml functionaltests/resources/v2/wf_single_v2.yaml functionaltests/resources/v2/wf_v2.yaml functionaltests/resources/v2/wf_wrapping_wf_v2.yaml functionaltests/resources/v2/for_namespaces/lowest_level_wf.yaml functionaltests/resources/v2/for_namespaces/middle_wf.yaml functionaltests/resources/v2/for_namespaces/top_level_wf.yaml mistralclient/__init__.py mistralclient/exceptions.py mistralclient/i18n.py mistralclient/shell.py mistralclient/utils.py mistralclient/api/__init__.py mistralclient/api/base.py mistralclient/api/client.py mistralclient/api/httpclient.py mistralclient/api/releasenotes/notes/fix-cli-error-messages-2f59329557a5734f.yaml mistralclient/api/v2/__init__.py mistralclient/api/v2/action_executions.py mistralclient/api/v2/actions.py mistralclient/api/v2/client.py mistralclient/api/v2/code_sources.py mistralclient/api/v2/cron_triggers.py mistralclient/api/v2/dynamic_actions.py mistralclient/api/v2/environments.py mistralclient/api/v2/event_triggers.py mistralclient/api/v2/executions.py mistralclient/api/v2/members.py mistralclient/api/v2/services.py mistralclient/api/v2/tasks.py mistralclient/api/v2/workbooks.py mistralclient/api/v2/workflows.py mistralclient/auth/__init__.py mistralclient/auth/auth_types.py mistralclient/auth/keycloak.py mistralclient/auth/keystone.py mistralclient/commands/__init__.py mistralclient/commands/v2/__init__.py mistralclient/commands/v2/action_executions.py mistralclient/commands/v2/actions.py mistralclient/commands/v2/base.py mistralclient/commands/v2/code_sources.py mistralclient/commands/v2/cron_triggers.py mistralclient/commands/v2/dynamic_actions.py mistralclient/commands/v2/environments.py mistralclient/commands/v2/event_triggers.py mistralclient/commands/v2/executions.py mistralclient/commands/v2/members.py mistralclient/commands/v2/services.py mistralclient/commands/v2/tasks.py mistralclient/commands/v2/workbooks.py mistralclient/commands/v2/workflows.py mistralclient/osc/__init__.py mistralclient/osc/plugin.py mistralclient/tests/__init__.py mistralclient/tests/functional/__init__.py mistralclient/tests/functional/cli/__init__.py mistralclient/tests/functional/cli/base.py mistralclient/tests/functional/cli/v2/__init__.py mistralclient/tests/functional/cli/v2/base_v2.py mistralclient/tests/functional/cli/v2/test_cli_multi_tenancy.py mistralclient/tests/functional/cli/v2/test_cli_v2.py mistralclient/tests/unit/__init__.py mistralclient/tests/unit/base.py mistralclient/tests/unit/base_shell_test.py mistralclient/tests/unit/test_client.py mistralclient/tests/unit/test_httpclient.py mistralclient/tests/unit/test_shell.py mistralclient/tests/unit/test_utils.py mistralclient/tests/unit/resources/action_v2.yaml mistralclient/tests/unit/resources/ctx.json mistralclient/tests/unit/resources/env_v2.json mistralclient/tests/unit/resources/env_v2.yaml mistralclient/tests/unit/resources/wb_v2.yaml mistralclient/tests/unit/resources/wf_v2.yaml mistralclient/tests/unit/v2/__init__.py mistralclient/tests/unit/v2/base.py mistralclient/tests/unit/v2/test_action_executions.py mistralclient/tests/unit/v2/test_actions.py mistralclient/tests/unit/v2/test_cli_action_execs.py mistralclient/tests/unit/v2/test_cli_actions.py mistralclient/tests/unit/v2/test_cli_bash_completion.py mistralclient/tests/unit/v2/test_cli_code_sources.py mistralclient/tests/unit/v2/test_cli_cron_triggers.py mistralclient/tests/unit/v2/test_cli_dynamic_actions.py mistralclient/tests/unit/v2/test_cli_environments.py mistralclient/tests/unit/v2/test_cli_event_triggers.py mistralclient/tests/unit/v2/test_cli_executions.py mistralclient/tests/unit/v2/test_cli_members.py mistralclient/tests/unit/v2/test_cli_services.py mistralclient/tests/unit/v2/test_cli_tasks.py mistralclient/tests/unit/v2/test_cli_workbooks.py mistralclient/tests/unit/v2/test_cli_workflows.py mistralclient/tests/unit/v2/test_environments.py mistralclient/tests/unit/v2/test_executions.py mistralclient/tests/unit/v2/test_keystone.py mistralclient/tests/unit/v2/test_members.py mistralclient/tests/unit/v2/test_services.py mistralclient/tests/unit/v2/test_tasks.py mistralclient/tests/unit/v2/test_workbooks.py mistralclient/tests/unit/v2/test_workflows.py python_mistralclient.egg-info/PKG-INFO python_mistralclient.egg-info/SOURCES.txt python_mistralclient.egg-info/dependency_links.txt python_mistralclient.egg-info/entry_points.txt python_mistralclient.egg-info/not-zip-safe python_mistralclient.egg-info/pbr.json python_mistralclient.egg-info/requires.txt python_mistralclient.egg-info/top_level.txt releasenotes/notes/.placeholder releasenotes/notes/add-namespaces-to-cli-9d20e7fcb07c223f.yaml releasenotes/notes/add_execution_get_report_command-b91b0a0db71716cd.yaml releasenotes/notes/add_namespace_option_to_actions.yaml releasenotes/notes/add_namespace_option_to_workbook_command-202c40625dd24ecb.yaml releasenotes/notes/add_sub_executions_new_commands.yaml releasenotes/notes/drop-py-2-7-9c369125c06e32bd.yaml releasenotes/notes/enforce_raw_definitions-94433339a63d15f8.yaml releasenotes/notes/execution-list-default-behavior-change-225010204e32bc89.yaml releasenotes/notes/fix-region-name-2031ff4b83b6308e.yaml releasenotes/notes/fix-regression-with-execution-force-delete-af8d1968cb2673ef.yaml releasenotes/notes/force-delete-executions-d08ce88a5deb3291.yaml releasenotes/notes/remove-keystoneclient-dependency-f2981f29e6673f71.yaml releasenotes/notes/set-default-limit-value-7e293d843d6d85ac.yaml releasenotes/source/2024.1.rst releasenotes/source/conf.py releasenotes/source/index.rst releasenotes/source/liberty.rst releasenotes/source/mitaka.rst releasenotes/source/newton.rst releasenotes/source/ocata.rst releasenotes/source/pike.rst releasenotes/source/queens.rst releasenotes/source/rocky.rst releasenotes/source/stein.rst releasenotes/source/train.rst releasenotes/source/unreleased.rst releasenotes/source/ussuri.rst releasenotes/source/victoria.rst releasenotes/source/wallaby.rst releasenotes/source/yoga.rst releasenotes/source/zed.rst releasenotes/source/_static/.placeholder releasenotes/source/_templates/.placeholder tools/mistral.bash_completion tools/run_pep8 tools/update_env_deps tools/config/generate_sample.sh././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030372.0 python-mistralclient-5.3.0/python_mistralclient.egg-info/dependency_links.txt0000664000175000017500000000000100000000000027746 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030372.0 python-mistralclient-5.3.0/python_mistralclient.egg-info/entry_points.txt0000664000175000017500000001204600000000000027201 0ustar00zuulzuul00000000000000[console_scripts] mistral = mistralclient.shell:main [mistralclient.auth] keycloak-oidc = mistralclient.auth.keycloak:KeycloakAuthHandler keystone = mistralclient.auth.keystone:KeystoneAuthHandler [openstack.cli.extension] workflow_engine = mistralclient.osc.plugin [openstack.workflow_engine.v2] action_definition_create = mistralclient.commands.v2.actions:Create action_definition_definition_show = mistralclient.commands.v2.actions:GetDefinition action_definition_delete = mistralclient.commands.v2.actions:Delete action_definition_list = mistralclient.commands.v2.actions:List action_definition_show = mistralclient.commands.v2.actions:Get action_definition_update = mistralclient.commands.v2.actions:Update action_execution_delete = mistralclient.commands.v2.action_executions:Delete action_execution_input_show = mistralclient.commands.v2.action_executions:GetInput action_execution_list = mistralclient.commands.v2.action_executions:List action_execution_output_show = mistralclient.commands.v2.action_executions:GetOutput action_execution_run = mistralclient.commands.v2.action_executions:Create action_execution_show = mistralclient.commands.v2.action_executions:Get action_execution_update = mistralclient.commands.v2.action_executions:Update code_source_content_show = mistralclient.commands.v2.code_sources:GetContent code_source_create = mistralclient.commands.v2.code_sources:Create code_source_delete = mistralclient.commands.v2.code_sources:Delete code_source_list = mistralclient.commands.v2.code_sources:List code_source_show = mistralclient.commands.v2.code_sources:Get code_source_update = mistralclient.commands.v2.code_sources:Update cron_trigger_create = mistralclient.commands.v2.cron_triggers:Create cron_trigger_delete = mistralclient.commands.v2.cron_triggers:Delete cron_trigger_list = mistralclient.commands.v2.cron_triggers:List cron_trigger_show = mistralclient.commands.v2.cron_triggers:Get dynamic_action_create = mistralclient.commands.v2.dynamic_actions:Create dynamic_action_delete = mistralclient.commands.v2.dynamic_actions:Delete dynamic_action_list = mistralclient.commands.v2.dynamic_actions:List dynamic_action_show = mistralclient.commands.v2.dynamic_actions:Get dynamic_action_update = mistralclient.commands.v2.dynamic_actions:Update event_trigger_create = mistralclient.commands.v2.event_triggers:Create event_trigger_delete = mistralclient.commands.v2.event_triggers:Delete event_trigger_list = mistralclient.commands.v2.event_triggers:List event_trigger_show = mistralclient.commands.v2.event_triggers:Get resource_member_create = mistralclient.commands.v2.members:Create resource_member_delete = mistralclient.commands.v2.members:Delete resource_member_list = mistralclient.commands.v2.members:List resource_member_show = mistralclient.commands.v2.members:Get resource_member_update = mistralclient.commands.v2.members:Update task_execution_list = mistralclient.commands.v2.tasks:List task_execution_published_show = mistralclient.commands.v2.tasks:GetPublished task_execution_rerun = mistralclient.commands.v2.tasks:Rerun task_execution_result_show = mistralclient.commands.v2.tasks:GetResult task_execution_show = mistralclient.commands.v2.tasks:Get workbook_create = mistralclient.commands.v2.workbooks:Create workbook_definition_show = mistralclient.commands.v2.workbooks:GetDefinition workbook_delete = mistralclient.commands.v2.workbooks:Delete workbook_list = mistralclient.commands.v2.workbooks:List workbook_show = mistralclient.commands.v2.workbooks:Get workbook_update = mistralclient.commands.v2.workbooks:Update workbook_validate = mistralclient.commands.v2.workbooks:Validate workflow_create = mistralclient.commands.v2.workflows:Create workflow_definition_show = mistralclient.commands.v2.workflows:GetDefinition workflow_delete = mistralclient.commands.v2.workflows:Delete workflow_engine_service_list = mistralclient.commands.v2.services:List workflow_env_create = mistralclient.commands.v2.environments:Create workflow_env_delete = mistralclient.commands.v2.environments:Delete workflow_env_list = mistralclient.commands.v2.environments:List workflow_env_show = mistralclient.commands.v2.environments:Get workflow_env_update = mistralclient.commands.v2.environments:Update workflow_execution_create = mistralclient.commands.v2.executions:Create workflow_execution_delete = mistralclient.commands.v2.executions:Delete workflow_execution_input_show = mistralclient.commands.v2.executions:GetInput workflow_execution_list = mistralclient.commands.v2.executions:List workflow_execution_output_show = mistralclient.commands.v2.executions:GetOutput workflow_execution_published_show = mistralclient.commands.v2.executions:GetPublished workflow_execution_report_show = mistralclient.commands.v2.executions:GetReport workflow_execution_show = mistralclient.commands.v2.executions:Get workflow_execution_update = mistralclient.commands.v2.executions:Update workflow_list = mistralclient.commands.v2.workflows:List workflow_show = mistralclient.commands.v2.workflows:Get workflow_update = mistralclient.commands.v2.workflows:Update workflow_validate = mistralclient.commands.v2.workflows:Validate ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030372.0 python-mistralclient-5.3.0/python_mistralclient.egg-info/not-zip-safe0000664000175000017500000000000100000000000026126 0ustar00zuulzuul00000000000000 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030372.0 python-mistralclient-5.3.0/python_mistralclient.egg-info/pbr.json0000664000175000017500000000005600000000000025357 0ustar00zuulzuul00000000000000{"git_version": "926301e", "is_release": true}././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030372.0 python-mistralclient-5.3.0/python_mistralclient.egg-info/requires.txt0000664000175000017500000000030500000000000026276 0ustar00zuulzuul00000000000000PyYAML>=3.13 cliff!=2.9.0,>=2.8.0 keystoneauth1>=3.4.0 osc-lib>=1.8.0 oslo.i18n>=3.15.3 oslo.serialization!=2.19.1,>=2.18.0 oslo.utils>=3.33.0 pbr!=2.1.0,>=2.0.0 requests>=2.14.2 stevedore>=1.20.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030372.0 python-mistralclient-5.3.0/python_mistralclient.egg-info/top_level.txt0000664000175000017500000000001600000000000026427 0ustar00zuulzuul00000000000000mistralclient ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8405466 python-mistralclient-5.3.0/releasenotes/0000775000175000017500000000000000000000000020424 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8605468 python-mistralclient-5.3.0/releasenotes/notes/0000775000175000017500000000000000000000000021554 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/.placeholder0000664000175000017500000000000000000000000024025 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/add-namespaces-to-cli-9d20e7fcb07c223f.yaml0000664000175000017500000000102700000000000030764 0ustar00zuulzuul00000000000000--- features: - | Using namespaces is now supported from the client. It is possible to use the namespace flag with all workflows commands and when creating a workflow execution. Namespace feature allows a user to add two workflows with the same name to Mistral as long as they are within two different namespaces. Namespace will also be visible in the table printed when using the CLI to get info about workflows. Plus, the CLI will print the workflow namespace along with the workflow name where needed. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/add_execution_get_report_command-b91b0a0db71716cd.yaml0000664000175000017500000000125500000000000033465 0ustar00zuulzuul00000000000000--- features: - | Added the new CLI command "execution-get-report" that prints information about the entire workflow execution tree, including its task executions, action executions and nested workflow executions. The command currently has filters "--errors-only" that allows to find only ERROR paths of the execution tree (enabled by default), "--no-errors-only" that allows to print all tree regardless of the elements' state, "--max-depth" that allows to limit the depth of the tree that needs to be printed. This command should be especially useful for debugging failure situations when it's not easy to manually track down to the root cause. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/add_namespace_option_to_actions.yaml0000664000175000017500000000025100000000000031014 0ustar00zuulzuul00000000000000--- features: - | Add namespace parameter to action commands. Namespace parameter allows to create multiple actions with same name under different namespaces. ././@PaxHeader0000000000000000000000000000021400000000000011452 xustar0000000000000000118 path=python-mistralclient-5.3.0/releasenotes/notes/add_namespace_option_to_workbook_command-202c40625dd24ecb.yaml 22 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/add_namespace_option_to_workbook_command-202c40625dd240000664000175000017500000000025500000000000033475 0ustar00zuulzuul00000000000000--- features: - | Add namespace parameter to workbook commands. Namespace parameter allows to create multiple workbooks with same name under different namespaces. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/add_sub_executions_new_commands.yaml0000664000175000017500000000062300000000000031042 0ustar00zuulzuul00000000000000--- features: - | Added 2 new CLI commands, "execution-get-sub-executions" returns sub-executions of a given execution id and "task-get-sub-executions" returns the sub-executions of a given task execution id. Both commands have the options "--max-depth" which is the max depth of a sub-execution, and "--errors-only" that allows to find only ERROR paths of the execution tree. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/drop-py-2-7-9c369125c06e32bd.yaml0000664000175000017500000000031600000000000026471 0ustar00zuulzuul00000000000000--- upgrade: - | Python 2.7 support has been dropped. Last release of python-mistralclient to support python 2.7 is OpenStack Train. The minimum version of Python now supported is Python 3.6. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/enforce_raw_definitions-94433339a63d15f8.yaml0000664000175000017500000000045100000000000031332 0ustar00zuulzuul00000000000000--- features: - | The new ``enforce_raw_definitions`` variable has been added to the ``Client`` class and each ``ResourceManager`` class. This variable is set to False by default. Setting this parameter to True enforces the client to use ``definition`` passed in as raw content. ././@PaxHeader0000000000000000000000000000021200000000000011450 xustar0000000000000000116 path=python-mistralclient-5.3.0/releasenotes/notes/execution-list-default-behavior-change-225010204e32bc89.yaml 22 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/execution-list-default-behavior-change-225010204e32bc80000664000175000017500000000104400000000000033000 0ustar00zuulzuul00000000000000--- critical: - | The default behavior of the action-execution-list, execution-list and task-list commands has been changed. Instead of returning the oldest N records (default 100 or --limit specified value) by default, they now return the most recent N records, when no other sort_dir, sort_key or marker values are provided. If the user specifies --oldest or any of the --marker, --sort_key or --sort_dir options, the new behavior is disabled and the commands will work according to the user-supplied options. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/fix-region-name-2031ff4b83b6308e.yaml0000664000175000017500000000030700000000000027555 0ustar00zuulzuul00000000000000--- fixes: - | ``--os-region-name`` or ``OS_REGION_NAME`` is fully supported now and will be passed to Mistral service in order to get OpenStack service client for the specific region. ././@PaxHeader0000000000000000000000000000021600000000000011454 xustar0000000000000000120 path=python-mistralclient-5.3.0/releasenotes/notes/fix-regression-with-execution-force-delete-af8d1968cb2673ef.yaml 22 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/fix-regression-with-execution-force-delete-af8d1968cb20000664000175000017500000000052400000000000033511 0ustar00zuulzuul00000000000000--- fixes: - | mistralclient 3.5.0 introduced a new --force option to delete executions that are still running. However, this had the unintended impact of passing force=false when it wasn't provided. This is incompatible with previous releases of the Mistral API which reject requests as they don't recognise "force". ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/force-delete-executions-d08ce88a5deb3291.yaml0000664000175000017500000000043200000000000031462 0ustar00zuulzuul00000000000000--- features: - | Adding a --force optional parameter to delete excetutions. Without it only finished executions can be deleted. If --force is passed the execution will be deleted but mistral will generate some errors as expected objects in memory no longer exist ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/remove-keystoneclient-dependency-f2981f29e6673f71.yaml0000664000175000017500000000035500000000000033213 0ustar00zuulzuul00000000000000--- other: - The dependency to python-keystoneclient was removed. Relying solely on keystoneauth1. - The user has to set the "OS_USER_DOMAIN_NAME" and "OS_PROJECT_DOMAIN_NAME" explicitly if keystone v3 version is being used. ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/notes/set-default-limit-value-7e293d843d6d85ac.yaml0000664000175000017500000000041200000000000031331 0ustar00zuulzuul00000000000000--- critical: - | The "--limit" parameter of heavy CLI commands like "task-list" and "execution-list" and "action-execution-list" is set to "100" by default to avoid the huge load on server. To fetch the full result set, user may use "--limit -1". ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8645468 python-mistralclient-5.3.0/releasenotes/source/0000775000175000017500000000000000000000000021724 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/2024.1.rst0000664000175000017500000000020200000000000023176 0ustar00zuulzuul00000000000000=========================== 2024.1 Series Release Notes =========================== .. release-notes:: :branch: stable/2024.1 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8645468 python-mistralclient-5.3.0/releasenotes/source/_static/0000775000175000017500000000000000000000000023352 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/_static/.placeholder0000664000175000017500000000000000000000000025623 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8645468 python-mistralclient-5.3.0/releasenotes/source/_templates/0000775000175000017500000000000000000000000024061 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/_templates/.placeholder0000664000175000017500000000000000000000000026332 0ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/conf.py0000664000175000017500000002033700000000000023230 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. # # Mistral documentation build configuration file, created by # sphinx-quickstart on Fri Nov 1 02:06:28 2013. # # This file is execfile()d with the current directory set to its containing # dir. # # Note that not all possible configuration values are present in this # autogenerated file. # # All configuration values have a default; values that are commented out # serve to show the default. # import sys, os # If extensions (or modules to document with autodoc) are in another directory, # add these directories to sys.path here. If the directory is relative to the # documentation root, use os.path.abspath to make it absolute, like shown here. # sys.path.insert(0, os.path.abspath('.')) # -- General configuration --------------------------------------------------- # If your documentation needs a minimal Sphinx version, state it here. # needs_sphinx = '1.0' # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. extensions = [ 'openstackdocstheme', 'reno.sphinxext', ] # Add any paths that contain templates here, relative to this directory. # templates_path = ['_templates'] # The suffix of source filenames. source_suffix = '.rst' # The encoding of source files. # source_encoding = 'utf-8-sig' # The master toctree document. master_doc = 'index' # General information about the project. project = 'Mistral Client Release Notes' copyright = '2016, Mistral developers' # Release notes are version independent. # The short X.Y version. version = '' # The full version, including alpha/beta/rc tags. release = '' # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. # language = None # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. exclude_patterns = [] # The reST default role (used for this markup: `text`) to use for all # documents. # default_role = None # If true, '()' will be appended to :func: etc. cross-reference text. # add_function_parentheses = True # If true, the current module name will be prepended to all description # unit titles (such as .. function::). # add_module_names = True # If true, sectionauthor and moduleauthor directives will be shown in the # output. They are ignored by default. # show_authors = False # The name of the Pygments (syntax highlighting) style to use. pygments_style = 'native' # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] # -- Options for HTML output ------------------------------------------------- # The theme to use for HTML and HTML Help pages. See the documentation for # a list of builtin themes. html_theme = 'openstackdocs' # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. # html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # html_theme_path = [] # The name for this set of Sphinx documents. If None, it defaults to # " v documentation". # html_title = None # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None # The name of an image file (relative to this directory) to place at the top # of the sidebar. # html_logo = None # The name of an image file (within the static path) to use as favicon of the # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 # pixels large. # html_favicon = None # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". # html_static_path = ['_static'] # If true, SmartyPants will be used to convert quotes and dashes to # typographically correct entities. # html_use_smartypants = True # Custom sidebar templates, maps document names to template names. # html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. # html_additional_pages = {} # If false, no module index is generated. # html_domain_indices = True # If false, no index is generated. # html_use_index = True # If true, the index is split into individual pages for each letter. # html_split_index = False # If true, links to the reST sources are added to the pages. # html_show_sourcelink = True # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. # html_show_sphinx = True # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. # html_show_copyright = True # If true, an OpenSearch description file will be output, and all pages will # contain a tag referring to it. The value of this option must be the # base URL from which the finished HTML is served. # html_use_opensearch = '' # This is the file name suffix for HTML files (e.g. ".xhtml"). # html_file_suffix = None # Output file base name for HTML help builder. htmlhelp_basename = 'MistralreleaseNotesdoc' # -- Options for LaTeX output ------------------------------------------------ latex_elements = { # The paper size ('letterpaper' or 'a4paper'). # 'papersize': 'letterpaper', # The font size ('10pt', '11pt' or '12pt'). # 'pointsize': '10pt', # Additional stuff for the LaTeX preamble. # 'preamble': '', } # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, # documentclass [howto/manual]). latex_documents = [ ('index', 'MistralClientReleaseNotes.tex', 'Mistral Client Release Notes Documentation', 'Mistral developers', 'manual'), ] # The name of an image file (relative to this directory) to place at the top of # the title page. # latex_logo = None # For "manual" documents, if this is true, then toplevel headings are parts, # not chapters. # latex_use_parts = False # If true, show page references after internal links. # latex_show_pagerefs = False # If true, show URL addresses after external links. # latex_show_urls = False # Documents to append as an appendix to all manuals. # latex_appendices = [] # If false, no module index is generated. # latex_domain_indices = True # -- Options for manual page output ------------------------------------------ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ ('index', 'mistral_clientreleasenotes', 'Mistral Client Release Notes Documentation', ['Mistral developers'], 1) ] # If true, show URL addresses after external links. # man_show_urls = False # -- Options for Texinfo output ---------------------------------------------- # Grouping the document tree into Texinfo files. List of tuples # (source start file, target name, title, author, # dir menu entry, description, category) texinfo_documents = [ ('index', 'MistralClientReleaseNotes', 'Mistral Client Release Notes Documentation', 'Mistral developers', 'MistralClientReleaseNotes', 'One line description of project.', 'Miscellaneous'), ] # Documents to append as an appendix to all manuals. # texinfo_appendices = [] # If false, no module index is generated. # texinfo_domain_indices = True # How to display URL addresses: 'footnote', 'no', or 'inline'. # texinfo_show_urls = 'footnote' # -- Options for Internationalization output ------------------------------ locale_dirs = ['locale/'] # -- Options for openstackdocstheme ------------------------------------------- openstackdocs_repo_name = 'openstack/python-mistralclient' openstackdocs_bug_project = 'python-mistralclient' openstackdocs_bug_tag = '' openstackdocs_auto_name = False ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/index.rst0000664000175000017500000000041100000000000023561 0ustar00zuulzuul00000000000000Mistral Client Release Notes ============================ Contents ======== .. toctree:: :maxdepth: 2 unreleased 2024.1 zed yoga wallaby victoria ussuri train stein rocky queens pike ocata newton mitaka liberty ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/liberty.rst0000664000175000017500000000021500000000000024126 0ustar00zuulzuul00000000000000============================ Liberty Series Release Notes ============================ .. release-notes:: :branch: origin/stable/liberty ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/mitaka.rst0000664000175000017500000000021100000000000023716 0ustar00zuulzuul00000000000000=========================== Mitaka Series Release Notes =========================== .. release-notes:: :branch: origin/stable/mitaka ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/newton.rst0000664000175000017500000000021100000000000023762 0ustar00zuulzuul00000000000000=========================== Newton Series Release Notes =========================== .. release-notes:: :branch: origin/stable/newton ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/ocata.rst0000664000175000017500000000023000000000000023540 0ustar00zuulzuul00000000000000=================================== Ocata Series Release Notes =================================== .. release-notes:: :branch: origin/stable/ocata ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/pike.rst0000664000175000017500000000021700000000000023406 0ustar00zuulzuul00000000000000=================================== Pike Series Release Notes =================================== .. release-notes:: :branch: stable/pike ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/queens.rst0000664000175000017500000000022300000000000023753 0ustar00zuulzuul00000000000000=================================== Queens Series Release Notes =================================== .. release-notes:: :branch: stable/queens ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/rocky.rst0000664000175000017500000000022100000000000023600 0ustar00zuulzuul00000000000000=================================== Rocky Series Release Notes =================================== .. release-notes:: :branch: stable/rocky ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/stein.rst0000664000175000017500000000022100000000000023573 0ustar00zuulzuul00000000000000=================================== Stein Series Release Notes =================================== .. release-notes:: :branch: stable/stein ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/train.rst0000664000175000017500000000017600000000000023577 0ustar00zuulzuul00000000000000========================== Train Series Release Notes ========================== .. release-notes:: :branch: stable/train ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/unreleased.rst0000664000175000017500000000015300000000000024604 0ustar00zuulzuul00000000000000============================ Current Series Release Notes ============================ .. release-notes:: ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/ussuri.rst0000664000175000017500000000020200000000000024002 0ustar00zuulzuul00000000000000=========================== Ussuri Series Release Notes =========================== .. release-notes:: :branch: stable/ussuri ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/victoria.rst0000664000175000017500000000021200000000000024271 0ustar00zuulzuul00000000000000============================= Victoria Series Release Notes ============================= .. release-notes:: :branch: stable/victoria ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/wallaby.rst0000664000175000017500000000020600000000000024107 0ustar00zuulzuul00000000000000============================ Wallaby Series Release Notes ============================ .. release-notes:: :branch: stable/wallaby ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/yoga.rst0000664000175000017500000000017200000000000023415 0ustar00zuulzuul00000000000000========================= Yoga Series Release Notes ========================= .. release-notes:: :branch: stable/yoga ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/releasenotes/source/zed.rst0000664000175000017500000000016600000000000023243 0ustar00zuulzuul00000000000000======================== Zed Series Release Notes ======================== .. release-notes:: :branch: stable/zed ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/requirements.txt0000664000175000017500000000102200000000000021212 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. cliff!=2.9.0,>=2.8.0 # Apache-2.0 osc-lib>=1.8.0 # Apache-2.0 oslo.utils>=3.33.0 # Apache-2.0 oslo.i18n>=3.15.3 # Apache-2.0 oslo.serialization!=2.19.1,>=2.18.0 # Apache-2.0 pbr!=2.1.0,>=2.0.0 # Apache-2.0 keystoneauth1>=3.4.0 # Apache-2.0 PyYAML>=3.13 # MIT requests>=2.14.2 # Apache-2.0 stevedore>=1.20.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/run_functional_tests.sh0000775000175000017500000000233500000000000022545 0ustar00zuulzuul00000000000000#! /usr/bin/env bash ARG=$1 export USER_AUTH_SETTING=$(echo $OS_AUTH_URL) function pre_hook() { export WITHOUT_AUTH="True" IS_TEMPEST=$(pip freeze | grep tempest) if [ -z "$IS_TEMPEST" ] then echo "$(tput setaf 4)No such module 'tempest' in the system. Before running this script please install tempest module using : pip install git+http://github.com/openstack/tempest.git$(tput sgr 0)" fi } function run_tests_by_version() { export OS_AUTH_URL="" echo "$(tput setaf 4)Running integration CLI and python-mistralclient tests for v$1$(tput sgr 0)" export VERSION="v$1" nosetests -v mistralclient/tests/functional/cli/v$1/ nosetests -v mistralclient/tests/functional/client/v$1/ unset VERSION } function run_tests() { if [ -z "$ARG" ] then run_tests_by_version 1 run_tests_by_version 2 elif [ "$ARG" == "v1" ] then run_tests_by_version 1 elif [ "$ARG" == "v2" ] then run_tests_by_version 2 fi } function post_hook () { unset LOCAL_RUN export OS_AUTH_URL=$USER_AUTH_SETTING unset USER_AUTH_SETTING } #----------main-part---------- echo "$(tput setaf 4)Preparation for tests running...$(tput sgr 0)" pre_hook echo "$(tput setaf 4)Running tests...$(tput sgr 0)" run_tests post_hook ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8685467 python-mistralclient-5.3.0/setup.cfg0000664000175000017500000001412600000000000017560 0ustar00zuulzuul00000000000000[metadata] name = python-mistralclient summary = Mistral Client Library description-file = README.rst license = Apache Software License python-requires = >=3.8 classifiers = Programming Language :: Python Programming Language :: Python :: Implementation :: CPython Programming Language :: Python :: 3 :: Only Programming Language :: Python :: 3 Programming Language :: Python :: 3.8 Programming Language :: Python :: 3.9 Environment :: OpenStack Intended Audience :: Information Technology Intended Audience :: System Administrators License :: OSI Approved :: Apache Software License Operating System :: POSIX :: Linux author = OpenStack author-email = openstack-discuss@lists.openstack.org home-page = https://docs.openstack.org/python-mistralclient/latest/ [files] packages = mistralclient [entry_points] console_scripts = mistral = mistralclient.shell:main openstack.cli.extension = workflow_engine = mistralclient.osc.plugin openstack.workflow_engine.v2 = workbook_list = mistralclient.commands.v2.workbooks:List workbook_show = mistralclient.commands.v2.workbooks:Get workbook_create = mistralclient.commands.v2.workbooks:Create workbook_delete = mistralclient.commands.v2.workbooks:Delete workbook_update = mistralclient.commands.v2.workbooks:Update workbook_definition_show = mistralclient.commands.v2.workbooks:GetDefinition workbook_validate = mistralclient.commands.v2.workbooks:Validate workflow_list = mistralclient.commands.v2.workflows:List workflow_show = mistralclient.commands.v2.workflows:Get workflow_create = mistralclient.commands.v2.workflows:Create workflow_delete = mistralclient.commands.v2.workflows:Delete workflow_update = mistralclient.commands.v2.workflows:Update workflow_definition_show = mistralclient.commands.v2.workflows:GetDefinition workflow_validate = mistralclient.commands.v2.workflows:Validate workflow_env_create = mistralclient.commands.v2.environments:Create workflow_env_delete = mistralclient.commands.v2.environments:Delete workflow_env_update = mistralclient.commands.v2.environments:Update workflow_env_list = mistralclient.commands.v2.environments:List workflow_env_show = mistralclient.commands.v2.environments:Get action_execution_run = mistralclient.commands.v2.action_executions:Create action_execution_list = mistralclient.commands.v2.action_executions:List action_execution_show = mistralclient.commands.v2.action_executions:Get action_execution_input_show = mistralclient.commands.v2.action_executions:GetInput action_execution_output_show = mistralclient.commands.v2.action_executions:GetOutput action_execution_update = mistralclient.commands.v2.action_executions:Update action_execution_delete = mistralclient.commands.v2.action_executions:Delete workflow_execution_create = mistralclient.commands.v2.executions:Create workflow_execution_delete = mistralclient.commands.v2.executions:Delete workflow_execution_update = mistralclient.commands.v2.executions:Update workflow_execution_list = mistralclient.commands.v2.executions:List workflow_execution_show = mistralclient.commands.v2.executions:Get workflow_execution_input_show = mistralclient.commands.v2.executions:GetInput workflow_execution_output_show = mistralclient.commands.v2.executions:GetOutput workflow_execution_report_show = mistralclient.commands.v2.executions:GetReport workflow_execution_published_show = mistralclient.commands.v2.executions:GetPublished task_execution_list = mistralclient.commands.v2.tasks:List task_execution_show = mistralclient.commands.v2.tasks:Get task_execution_published_show = mistralclient.commands.v2.tasks:GetPublished task_execution_result_show = mistralclient.commands.v2.tasks:GetResult task_execution_rerun = mistralclient.commands.v2.tasks:Rerun action_definition_list = mistralclient.commands.v2.actions:List action_definition_show = mistralclient.commands.v2.actions:Get action_definition_create = mistralclient.commands.v2.actions:Create action_definition_delete = mistralclient.commands.v2.actions:Delete action_definition_update = mistralclient.commands.v2.actions:Update action_definition_definition_show = mistralclient.commands.v2.actions:GetDefinition cron_trigger_list = mistralclient.commands.v2.cron_triggers:List cron_trigger_show = mistralclient.commands.v2.cron_triggers:Get cron_trigger_create = mistralclient.commands.v2.cron_triggers:Create cron_trigger_delete = mistralclient.commands.v2.cron_triggers:Delete event_trigger_list = mistralclient.commands.v2.event_triggers:List event_trigger_show = mistralclient.commands.v2.event_triggers:Get event_trigger_create = mistralclient.commands.v2.event_triggers:Create event_trigger_delete = mistralclient.commands.v2.event_triggers:Delete workflow_engine_service_list = mistralclient.commands.v2.services:List resource_member_list = mistralclient.commands.v2.members:List resource_member_show = mistralclient.commands.v2.members:Get resource_member_create = mistralclient.commands.v2.members:Create resource_member_delete = mistralclient.commands.v2.members:Delete resource_member_update = mistralclient.commands.v2.members:Update code_source_list = mistralclient.commands.v2.code_sources:List code_source_show = mistralclient.commands.v2.code_sources:Get code_source_create = mistralclient.commands.v2.code_sources:Create code_source_delete = mistralclient.commands.v2.code_sources:Delete code_source_update = mistralclient.commands.v2.code_sources:Update code_source_content_show = mistralclient.commands.v2.code_sources:GetContent dynamic_action_list = mistralclient.commands.v2.dynamic_actions:List dynamic_action_show = mistralclient.commands.v2.dynamic_actions:Get dynamic_action_create = mistralclient.commands.v2.dynamic_actions:Create dynamic_action_delete = mistralclient.commands.v2.dynamic_actions:Delete dynamic_action_update = mistralclient.commands.v2.dynamic_actions:Update mistralclient.auth = keystone = mistralclient.auth.keystone:KeystoneAuthHandler keycloak-oidc = mistralclient.auth.keycloak:KeycloakAuthHandler [nosetests] cover-package = mistralclient [extract_messages] keywords = _ gettext ngettext l_ lazy_gettext [egg_info] tag_build = tag_date = 0 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/setup.py0000664000175000017500000000127100000000000017446 0ustar00zuulzuul00000000000000# Copyright (c) 2013 Hewlett-Packard Development Company, L.P. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or # implied. # See the License for the specific language governing permissions and # limitations under the License. import setuptools setuptools.setup( setup_requires=['pbr>=2.0.0'], pbr=True) ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/test-requirements.txt0000664000175000017500000000101500000000000022171 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. coverage>=4.0 # Apache-2.0 hacking>=3.0.1,<3.1.0 # Apache-2.0 python-openstackclient>=3.12.0 # Apache-2.0 oslotest>=3.2.0 # Apache-2.0 requests-mock>=1.2.0 # Apache-2.0 tempest>=17.1.0 # Apache-2.0 osprofiler>=1.4.0 # Apache-2.0 stestr>=2.0.0 # Apache-2.0 docutils>=0.11 # BSD os-client-config>=1.28.0 # Apache-2.0 ././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8645468 python-mistralclient-5.3.0/tools/0000775000175000017500000000000000000000000017073 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000003400000000000011452 xustar000000000000000028 mtime=1725030372.8645468 python-mistralclient-5.3.0/tools/config/0000775000175000017500000000000000000000000020340 5ustar00zuulzuul00000000000000././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/tools/config/generate_sample.sh0000775000175000017500000000536100000000000024037 0ustar00zuulzuul00000000000000#!/usr/bin/env bash print_hint() { echo "Try \`${0##*/} --help' for more information." >&2 } PARSED_OPTIONS=$(getopt -n "${0##*/}" -o hb:p:o: \ --long help,base-dir:,package-name:,output-dir: -- "$@") if [ $? != 0 ] ; then print_hint ; exit 1 ; fi eval set -- "$PARSED_OPTIONS" while true; do case "$1" in -h|--help) echo "${0##*/} [options]" echo "" echo "options:" echo "-h, --help show brief help" echo "-b, --base-dir=DIR project base directory" echo "-p, --package-name=NAME project package name" echo "-o, --output-dir=DIR file output directory" exit 0 ;; -b|--base-dir) shift BASEDIR=`echo $1 | sed -e 's/\/*$//g'` shift ;; -p|--package-name) shift PACKAGENAME=`echo $1` shift ;; -o|--output-dir) shift OUTPUTDIR=`echo $1 | sed -e 's/\/*$//g'` shift ;; --) break ;; esac done BASEDIR=${BASEDIR:-`pwd`} if ! [ -d $BASEDIR ] then echo "${0##*/}: missing project base directory" >&2 ; print_hint ; exit 1 elif [[ $BASEDIR != /* ]] then BASEDIR=$(cd "$BASEDIR" && pwd) fi PACKAGENAME=${PACKAGENAME:-${BASEDIR##*/}} TARGETDIR=$BASEDIR/$PACKAGENAME if ! [ -d $TARGETDIR ] then echo "${0##*/}: invalid project package name" >&2 ; print_hint ; exit 1 fi OUTPUTDIR=${OUTPUTDIR:-$BASEDIR/etc} # NOTE(bnemec): Some projects put their sample config in etc/, # some in etc/$PACKAGENAME/ if [ -d $OUTPUTDIR/$PACKAGENAME ] then OUTPUTDIR=$OUTPUTDIR/$PACKAGENAME elif ! [ -d $OUTPUTDIR ] then echo "${0##*/}: cannot access \`$OUTPUTDIR': No such file or directory" >&2 exit 1 fi BASEDIRESC=`echo $BASEDIR | sed -e 's/\//\\\\\//g'` find $TARGETDIR -type f -name "*.pyc" -delete FILES=$(find $TARGETDIR -type f -name "*.py" ! -path "*/tests/*" \ -exec grep -l "Opt(" {} + | sed -e "s/^$BASEDIRESC\///g" | sort -u) EXTRA_MODULES_FILE="`dirname $0`/oslo.config.generator.rc" if test -r "$EXTRA_MODULES_FILE" then source "$EXTRA_MODULES_FILE" fi export EVENTLET_NO_GREENDNS=yes OS_VARS=$(set | sed -n '/^OS_/s/=[^=]*$//gp' | xargs) [ "$OS_VARS" ] && eval "unset \$OS_VARS" DEFAULT_MODULEPATH=mistral.openstack.common.config.generator MODULEPATH=${MODULEPATH:-$DEFAULT_MODULEPATH} OUTPUTFILE=$OUTPUTDIR/$PACKAGENAME.conf.sample python -m $MODULEPATH $FILES > $OUTPUTFILE # Hook to allow projects to append custom config file snippets CONCAT_FILES=$(ls $BASEDIR/tools/config/*.conf.sample 2>/dev/null) for CONCAT_FILE in $CONCAT_FILES; do cat $CONCAT_FILE >> $OUTPUTFILE done ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/tools/mistral.bash_completion0000664000175000017500000000142000000000000023633 0ustar00zuulzuul00000000000000_mistral_opts="" # lazy init _mistral_flags="" # lazy init _mistral_opts_exp="" # lazy init _mistral() { local cur prev mbc cflags COMPREPLY=() cur="${COMP_WORDS[COMP_CWORD]}" prev="${COMP_WORDS[COMP_CWORD-1]}" if [ "x$_mistral_opts" == "x" ] ; then mbc="`mistral bash-completion`" _mistral_opts="`echo "$mbc" | sed -e "s/\s-[a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" _mistral_flags="`echo " $mbc" | sed -e "s/ [^-][^-][a-z0-9_-]*//g" -e "s/\s\s*/ /g"`" _mistral_opts_exp="`echo "$_mistral_opts" | sed -e "s/\s/|/g"`" fi if [[ " ${COMP_WORDS[@]} " =~ " "($_mistral_opts_exp)" " && "$prev" != "help" ]] ; then COMPREPLY=($(compgen -W "${_mistral_flags}" -- ${cur})) else COMPREPLY=($(compgen -W "${_mistral_opts}" -- ${cur})) fi return 0 } complete -F _mistral mistral ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/tools/run_pep80000775000175000017500000000002600000000000020557 0ustar00zuulzuul00000000000000#!/bin/sh tox -epep8 ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/tools/update_env_deps0000775000175000017500000000102500000000000022164 0ustar00zuulzuul00000000000000TOX_ENVLIST=`grep envlist tox.ini | cut -d '=' -f 2 | tr ',' ' '` TESTENVS=`grep testenv tox.ini | awk -F ':' '{print $2}' | tr '[]' ' '` UNFILTERED_ENVLIST=`echo "$TOX_ENVLIST $TESTENVS"` ENVLIST=$( awk 'BEGIN{RS=ORS=" "}!a[$0]++' <<<$UNFILTERED_ENVLIST ); for env in $ENVLIST do ENV_PATH=.tox/$env PIP_PATH=$ENV_PATH/bin/pip echo -e "\nUpdate environment ${env}...\n" if [ ! -d $ENV_PATH -o ! -f $PIP_PATH ] then tox --notest -e$env else $PIP_PATH install -r requirements.txt -r test-requirements.txt fi done ././@PaxHeader0000000000000000000000000000002600000000000011453 xustar000000000000000022 mtime=1725030339.0 python-mistralclient-5.3.0/tox.ini0000664000175000017500000000427000000000000017251 0ustar00zuulzuul00000000000000[tox] envlist = py3,pep8 minversion = 3.1.1 ignore_basepython_conflict = True [testenv] basepython = python3 usedevelop = True install_command = pip install {opts} {packages} setenv = VIRTUAL_ENV={envdir} PYTHONDONTWRITEBYTECODE = 1 PYTHONWARNINGS=default::DeprecationWarning passenv = http_proxy HTTP_PROXY https_proxy HTTPS_PROXY no_proxy NO_PROXY deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/requirements.txt -r{toxinidir}/test-requirements.txt commands = rm -f .testrepository/times.dbm find . -type f -name "*.pyc" -delete stestr run --concurrency 1 --slowest {posargs} Allowlist_externals = find rm [testenv:cover] setenv = {[testenv]setenv} PYTHON=coverage run --source mistralclient --parallel-mode commands = stestr run {posargs} coverage combine coverage html -d cover coverage xml -o cover/coverage.xml coverage report [testenv:functional] setenv = TEST_PATH = ./mistralclient/tests/functional commands = find . -type f -name "*.pyc" -delete stestr run --concurrency 1 --slowest {posargs} [testenv:pep8] commands = flake8 {posargs} [testenv:venv] commands = {posargs} [testenv:docs] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = rm -rf doc/html doc/build rm -rf doc/source/apidoc doc/source/api sphinx-build -W --keep-going -b html doc/source doc/build/html [flake8] # H106: Don't put vim configuration in source files # H203: Use assertIs(Not)None to check for None enable-extensions=H106,H203 show-source = true builtins = _ exclude=.venv,.git,.tox,dist,doc,*lib/python*,*egg,tools [testenv:releasenotes] deps = -c{env:TOX_CONSTRAINTS_FILE:https://releases.openstack.org/constraints/upper/master} -r{toxinidir}/doc/requirements.txt commands = rm -rf releasenotes/build sphinx-build -a -E -W -d releasenotes/build/doctrees --keep-going -b html releasenotes/source releasenotes/build/html [testenv:lower-constraints] deps = -c{toxinidir}/lower-constraints.txt -r{toxinidir}/test-requirements.txt -r{toxinidir}/requirements.txt