From c648cbee0872b35e0999fb43aff2057dd5580801 Mon Sep 17 00:00:00 2001 From: Ghanshyam Mann Date: Fri, 10 May 2024 15:52:22 -0700 Subject: [PATCH] Retire Murano: remove repo content Murano project is retiring - https://review.opendev.org/c/openstack/governance/+/919358 this commit remove the content of this project repo Depends-On: https://review.opendev.org/c/openstack/project-config/+/919359/ Change-Id: I4142dd143526d47df3b2389b8680543024ab6127 --- .coveragerc | 8 - .gitignore | 41 - .stestr.conf | 4 - .zuul.yaml | 40 - CONTRIBUTING.rst | 19 - HACKING.rst | 4 - LICENSE | 176 -- README.rst | 76 +- doc/requirements.txt | 6 - doc/source/cli/index.rst | 42 - doc/source/cli/murano.rst | 1318 ------------- doc/source/conf.py | 83 - doc/source/contributor/contributing.rst | 47 - doc/source/index.rst | 23 - muranoclient/__init__.py | 31 - muranoclient/apiclient/__init__.py | 0 muranoclient/apiclient/auth.py | 216 --- muranoclient/apiclient/base.py | 523 ------ muranoclient/apiclient/client.py | 359 ---- muranoclient/apiclient/exceptions.py | 461 ----- muranoclient/apiclient/fake_client.py | 176 -- muranoclient/client.py | 21 - muranoclient/common/__init__.py | 0 muranoclient/common/base.py | 229 --- muranoclient/common/exceptions.py | 207 --- muranoclient/common/http.py | 373 ---- muranoclient/common/utils.py | 831 --------- muranoclient/common/yaqlexpression.py | 60 - muranoclient/common/yaqlexpression_legacy.py | 48 - muranoclient/data/heat_logo.png | Bin 67840 -> 0 bytes muranoclient/data/mpl_logo.png | Bin 6186 -> 0 bytes muranoclient/glance/__init__.py | 60 - muranoclient/glance/artifacts.py | 360 ---- muranoclient/glance/client.py | 42 - muranoclient/i18n.py | 24 - .../locale/en_GB/LC_MESSAGES/muranoclient.po | 153 -- muranoclient/osc/__init__.py | 0 muranoclient/osc/plugin.py | 114 -- muranoclient/osc/v1/__init__.py | 0 muranoclient/osc/v1/action.py | 90 - muranoclient/osc/v1/category.py | 151 -- muranoclient/osc/v1/deployment.py | 74 - muranoclient/osc/v1/environment.py | 494 ----- muranoclient/osc/v1/package.py | 797 -------- muranoclient/osc/v1/schema.py | 52 - muranoclient/shell.py | 530 ------ muranoclient/tests/__init__.py | 0 muranoclient/tests/functional/__init__.py | 0 .../cli/MockApp/Classes/mock_muranopl.yaml | 34 - muranoclient/tests/functional/cli/__init__.py | 0 .../tests/functional/cli/murano_test_utils.py | 256 --- .../tests/functional/cli/test_murano.py | 893 --------- muranoclient/tests/functional/cli/utils.py | 61 - muranoclient/tests/functional/muranoclient.py | 70 - muranoclient/tests/unit/__init__.py | 0 muranoclient/tests/unit/base.py | 44 - muranoclient/tests/unit/fakes.py | 47 - .../unit/fixture_data/empty-app/manifest.yaml | 18 - .../unit/fixture_data/heat-template.yaml | 1 - muranoclient/tests/unit/fixture_data/logo.png | Bin 11634 -> 0 bytes .../test-app/Classes/testapp.yaml | 35 - .../test-app/Resources/Deploy.template | 21 - .../test-app/Resources/scripts/common.sh | 1 - .../test-app/Resources/scripts/deploy.sh | 1 - .../test-app/Resources/scripts/installer.sh | 1 - .../tests/unit/fixture_data/test-app/ui.yaml | 1 - muranoclient/tests/unit/osc/__init__.py | 0 muranoclient/tests/unit/osc/test_plugin.py | 34 - muranoclient/tests/unit/osc/v1/__init__.py | 0 muranoclient/tests/unit/osc/v1/fakes.py | 22 - .../osc/v1/fixture_data/heat-template.yaml | 1 - .../tests/unit/osc/v1/fixture_data/logo.png | Bin 11634 -> 0 bytes .../test-app/Classes/testapp.yaml | 35 - .../test-app/Resources/Deploy.template | 21 - .../test-app/Resources/scripts/common.sh | 1 - .../test-app/Resources/scripts/deploy.sh | 1 - .../test-app/Resources/scripts/installer.sh | 1 - .../unit/osc/v1/fixture_data/test-app/ui.yaml | 1 - muranoclient/tests/unit/osc/v1/test_action.py | 80 - .../tests/unit/osc/v1/test_category.py | 149 -- .../tests/unit/osc/v1/test_deployment.py | 82 - .../tests/unit/osc/v1/test_environment.py | 584 ------ .../tests/unit/osc/v1/test_package.py | 781 -------- muranoclient/tests/unit/osc/v1/test_schema.py | 54 - muranoclient/tests/unit/test_base.py | 46 - muranoclient/tests/unit/test_common_http.py | 463 ----- muranoclient/tests/unit/test_exc.py | 63 - muranoclient/tests/unit/test_methods.py | 354 ---- .../tests/unit/test_package_creator.py | 210 --- muranoclient/tests/unit/test_shell.py | 1628 ----------------- muranoclient/tests/unit/test_utils.py | 490 ----- muranoclient/v1/__init__.py | 0 muranoclient/v1/actions.py | 35 - muranoclient/v1/artifact_packages.py | 363 ---- muranoclient/v1/categories.py | 57 - muranoclient/v1/client.py | 67 - muranoclient/v1/deployments.py | 54 - muranoclient/v1/environments.py | 92 - muranoclient/v1/instance_statistics.py | 42 - muranoclient/v1/package_creator/__init__.py | 0 .../v1/package_creator/hot_package.py | 102 -- .../v1/package_creator/mpl_package.py | 220 --- muranoclient/v1/packages.py | 161 -- muranoclient/v1/request_statistics.py | 30 - muranoclient/v1/schemas.py | 51 - muranoclient/v1/services.py | 101 - muranoclient/v1/sessions.py | 44 - muranoclient/v1/shell.py | 1283 ------------- muranoclient/v1/static_actions.py | 45 - muranoclient/v1/templates.py | 100 - muranoclient/version.py | 18 - releasenotes/notes/.placeholder | 0 .../action-arguments-06a554f76783f3ed.yaml | 5 - ...nment-support-to-OSC-173b86ec631283b7.yaml | 6 - ...ded-category-command-01cb9dab21ab4a7b.yaml | 6 - ...-environment-command-85b31973e101fd1f.yaml | 6 - ...dded-schemas-support-e4a6e44c7bac2751.yaml | 7 - ...added-static-actions-3d45f5cdc5491770.yaml | 9 - .../notes/bug-1527045-97993f04757b4901.yaml | 6 - .../notes/bug-1536121-51c42f77a9e97db1.yaml | 5 - .../notes/bug-1629221-e7f1766eb9878f23.yaml | 4 - .../dep-exists-action-9af18bebcc0ef053.yaml | 7 - ...kages-service-glance-7d0052a5256adace.yaml | 5 - .../notes/drop-py-2-7-7a44884ca5972df5.yaml | 6 - ...stack-client-support-a273e33d6c31e36e.yaml | 4 - .../environment-edit-7faf5c8e8a3d44ac.yaml | 11 - .../fix-owned-flag-e8b718c074c1c314.yaml | 4 - .../glare-endpoint-cdba1b2351c19592.yaml | 6 - .../global-inherits-fix-6da007ec44a774f2.yaml | 4 - ...ckage-from-directory-8f2b5e393004ef97.yaml | 5 - ...improved-cli-outputs-a3e75cf0224a1993.yaml | 4 - ...s-of-a-given-project-e407dd5271649ad2.yaml | 4 - ...-class-yamls-support-914b3d155324214f.yaml | 4 - ...e-packages-glare-fix-b82a473ad976028f.yaml | 5 - .../osc-murano-url-507932234b49cf9f.yaml | 3 - ...package-list-command-23e39dc92ead8834.yaml | 3 - .../notes/reauth-fix-e03ad966c3178167.yaml | 4 - ...ckage-update-command-72e4c85da1b2c897.yaml | 4 - .../requirements-order-19ecc40ca6d34739.yaml | 5 - ...esources-dir-for-hot-c557c1472bbc79fa.yaml | 5 - ...loader-cve-2016-4972-e3f7ad9b234655ca.yaml | 9 - ...upport-endpoint-type-d7c6b32098b67eeb.yaml | 5 - .../yamlloader-glare-d7d0506f6711b650.yaml | 5 - releasenotes/source/2023.1.rst | 6 - releasenotes/source/_static/.placeholder | 0 releasenotes/source/_templates/.placeholder | 0 releasenotes/source/conf.py | 268 --- releasenotes/source/index.rst | 24 - releasenotes/source/liberty.rst | 6 - .../locale/en_GB/LC_MESSAGES/releasenotes.po | 462 ----- .../locale/fr/LC_MESSAGES/releasenotes.po | 72 - releasenotes/source/mitaka.rst | 6 - releasenotes/source/newton.rst | 6 - releasenotes/source/ocata.rst | 6 - releasenotes/source/pike.rst | 6 - releasenotes/source/queens.rst | 6 - releasenotes/source/rocky.rst | 6 - releasenotes/source/stein.rst | 6 - releasenotes/source/train.rst | 6 - releasenotes/source/unreleased.rst | 5 - releasenotes/source/ussuri.rst | 6 - releasenotes/source/victoria.rst | 6 - releasenotes/source/wallaby.rst | 6 - releasenotes/source/xena.rst | 6 - releasenotes/source/yoga.rst | 6 - releasenotes/source/zed.rst | 6 - requirements.txt | 19 - run_tests.sh | 107 -- setup-centos.sh | 178 -- setup.cfg | 69 - setup.py | 20 - test-requirements.txt | 14 - tools/murano.bash_completion | 26 - tox.ini | 81 - 174 files changed, 8 insertions(+), 19412 deletions(-) delete mode 100644 .coveragerc delete mode 100644 .gitignore delete mode 100644 .stestr.conf delete mode 100644 .zuul.yaml delete mode 100644 CONTRIBUTING.rst delete mode 100644 HACKING.rst delete mode 100644 LICENSE delete mode 100644 doc/requirements.txt delete mode 100644 doc/source/cli/index.rst delete mode 100644 doc/source/cli/murano.rst delete mode 100644 doc/source/conf.py delete mode 100644 doc/source/contributor/contributing.rst delete mode 100644 doc/source/index.rst delete mode 100644 muranoclient/__init__.py delete mode 100644 muranoclient/apiclient/__init__.py delete mode 100644 muranoclient/apiclient/auth.py delete mode 100644 muranoclient/apiclient/base.py delete mode 100644 muranoclient/apiclient/client.py delete mode 100644 muranoclient/apiclient/exceptions.py delete mode 100644 muranoclient/apiclient/fake_client.py delete mode 100644 muranoclient/client.py delete mode 100644 muranoclient/common/__init__.py delete mode 100644 muranoclient/common/base.py delete mode 100644 muranoclient/common/exceptions.py delete mode 100644 muranoclient/common/http.py delete mode 100644 muranoclient/common/utils.py delete mode 100644 muranoclient/common/yaqlexpression.py delete mode 100644 muranoclient/common/yaqlexpression_legacy.py delete mode 100644 muranoclient/data/heat_logo.png delete mode 100644 muranoclient/data/mpl_logo.png delete mode 100644 muranoclient/glance/__init__.py delete mode 100644 muranoclient/glance/artifacts.py delete mode 100644 muranoclient/glance/client.py delete mode 100644 muranoclient/i18n.py delete mode 100644 muranoclient/locale/en_GB/LC_MESSAGES/muranoclient.po delete mode 100644 muranoclient/osc/__init__.py delete mode 100644 muranoclient/osc/plugin.py delete mode 100644 muranoclient/osc/v1/__init__.py delete mode 100644 muranoclient/osc/v1/action.py delete mode 100644 muranoclient/osc/v1/category.py delete mode 100644 muranoclient/osc/v1/deployment.py delete mode 100644 muranoclient/osc/v1/environment.py delete mode 100644 muranoclient/osc/v1/package.py delete mode 100644 muranoclient/osc/v1/schema.py delete mode 100644 muranoclient/shell.py delete mode 100644 muranoclient/tests/__init__.py delete mode 100644 muranoclient/tests/functional/__init__.py delete mode 100644 muranoclient/tests/functional/cli/MockApp/Classes/mock_muranopl.yaml delete mode 100644 muranoclient/tests/functional/cli/__init__.py delete mode 100644 muranoclient/tests/functional/cli/murano_test_utils.py delete mode 100644 muranoclient/tests/functional/cli/test_murano.py delete mode 100644 muranoclient/tests/functional/cli/utils.py delete mode 100644 muranoclient/tests/functional/muranoclient.py delete mode 100644 muranoclient/tests/unit/__init__.py delete mode 100644 muranoclient/tests/unit/base.py delete mode 100644 muranoclient/tests/unit/fakes.py delete mode 100644 muranoclient/tests/unit/fixture_data/empty-app/manifest.yaml delete mode 100644 muranoclient/tests/unit/fixture_data/heat-template.yaml delete mode 100644 muranoclient/tests/unit/fixture_data/logo.png delete mode 100644 muranoclient/tests/unit/fixture_data/test-app/Classes/testapp.yaml delete mode 100644 muranoclient/tests/unit/fixture_data/test-app/Resources/Deploy.template delete mode 100644 muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/common.sh delete mode 100644 muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/deploy.sh delete mode 100644 muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/installer.sh delete mode 100644 muranoclient/tests/unit/fixture_data/test-app/ui.yaml delete mode 100644 muranoclient/tests/unit/osc/__init__.py delete mode 100644 muranoclient/tests/unit/osc/test_plugin.py delete mode 100644 muranoclient/tests/unit/osc/v1/__init__.py delete mode 100644 muranoclient/tests/unit/osc/v1/fakes.py delete mode 100644 muranoclient/tests/unit/osc/v1/fixture_data/heat-template.yaml delete mode 100644 muranoclient/tests/unit/osc/v1/fixture_data/logo.png delete mode 100644 muranoclient/tests/unit/osc/v1/fixture_data/test-app/Classes/testapp.yaml delete mode 100644 muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/Deploy.template delete mode 100644 muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/common.sh delete mode 100644 muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/deploy.sh delete mode 100644 muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/installer.sh delete mode 100644 muranoclient/tests/unit/osc/v1/fixture_data/test-app/ui.yaml delete mode 100644 muranoclient/tests/unit/osc/v1/test_action.py delete mode 100644 muranoclient/tests/unit/osc/v1/test_category.py delete mode 100644 muranoclient/tests/unit/osc/v1/test_deployment.py delete mode 100644 muranoclient/tests/unit/osc/v1/test_environment.py delete mode 100644 muranoclient/tests/unit/osc/v1/test_package.py delete mode 100644 muranoclient/tests/unit/osc/v1/test_schema.py delete mode 100644 muranoclient/tests/unit/test_base.py delete mode 100644 muranoclient/tests/unit/test_common_http.py delete mode 100644 muranoclient/tests/unit/test_exc.py delete mode 100644 muranoclient/tests/unit/test_methods.py delete mode 100644 muranoclient/tests/unit/test_package_creator.py delete mode 100644 muranoclient/tests/unit/test_shell.py delete mode 100644 muranoclient/tests/unit/test_utils.py delete mode 100644 muranoclient/v1/__init__.py delete mode 100644 muranoclient/v1/actions.py delete mode 100644 muranoclient/v1/artifact_packages.py delete mode 100644 muranoclient/v1/categories.py delete mode 100644 muranoclient/v1/client.py delete mode 100644 muranoclient/v1/deployments.py delete mode 100644 muranoclient/v1/environments.py delete mode 100644 muranoclient/v1/instance_statistics.py delete mode 100644 muranoclient/v1/package_creator/__init__.py delete mode 100644 muranoclient/v1/package_creator/hot_package.py delete mode 100644 muranoclient/v1/package_creator/mpl_package.py delete mode 100644 muranoclient/v1/packages.py delete mode 100644 muranoclient/v1/request_statistics.py delete mode 100644 muranoclient/v1/schemas.py delete mode 100644 muranoclient/v1/services.py delete mode 100644 muranoclient/v1/sessions.py delete mode 100644 muranoclient/v1/shell.py delete mode 100644 muranoclient/v1/static_actions.py delete mode 100644 muranoclient/v1/templates.py delete mode 100644 muranoclient/version.py delete mode 100644 releasenotes/notes/.placeholder delete mode 100644 releasenotes/notes/action-arguments-06a554f76783f3ed.yaml delete mode 100644 releasenotes/notes/add-environment-support-to-OSC-173b86ec631283b7.yaml delete mode 100644 releasenotes/notes/added-category-command-01cb9dab21ab4a7b.yaml delete mode 100644 releasenotes/notes/added-environment-command-85b31973e101fd1f.yaml delete mode 100644 releasenotes/notes/added-schemas-support-e4a6e44c7bac2751.yaml delete mode 100644 releasenotes/notes/added-static-actions-3d45f5cdc5491770.yaml delete mode 100644 releasenotes/notes/bug-1527045-97993f04757b4901.yaml delete mode 100644 releasenotes/notes/bug-1536121-51c42f77a9e97db1.yaml delete mode 100644 releasenotes/notes/bug-1629221-e7f1766eb9878f23.yaml delete mode 100644 releasenotes/notes/dep-exists-action-9af18bebcc0ef053.yaml delete mode 100644 releasenotes/notes/deprecate-murano-packages-service-glance-7d0052a5256adace.yaml delete mode 100644 releasenotes/notes/drop-py-2-7-7a44884ca5972df5.yaml delete mode 100644 releasenotes/notes/enable-openstack-client-support-a273e33d6c31e36e.yaml delete mode 100644 releasenotes/notes/environment-edit-7faf5c8e8a3d44ac.yaml delete mode 100644 releasenotes/notes/fix-owned-flag-e8b718c074c1c314.yaml delete mode 100644 releasenotes/notes/glare-endpoint-cdba1b2351c19592.yaml delete mode 100644 releasenotes/notes/global-inherits-fix-6da007ec44a774f2.yaml delete mode 100644 releasenotes/notes/import-package-from-directory-8f2b5e393004ef97.yaml delete mode 100644 releasenotes/notes/improved-cli-outputs-a3e75cf0224a1993.yaml delete mode 100644 releasenotes/notes/list-environments-of-a-given-project-e407dd5271649ad2.yaml delete mode 100644 releasenotes/notes/multi-class-yamls-support-914b3d155324214f.yaml delete mode 100644 releasenotes/notes/multiple-packages-glare-fix-b82a473ad976028f.yaml delete mode 100644 releasenotes/notes/osc-murano-url-507932234b49cf9f.yaml delete mode 100644 releasenotes/notes/osc-package-list-command-23e39dc92ead8834.yaml delete mode 100644 releasenotes/notes/reauth-fix-e03ad966c3178167.yaml delete mode 100644 releasenotes/notes/repair-package-update-command-72e4c85da1b2c897.yaml delete mode 100644 releasenotes/notes/requirements-order-19ecc40ca6d34739.yaml delete mode 100644 releasenotes/notes/resources-dir-for-hot-c557c1472bbc79fa.yaml delete mode 100644 releasenotes/notes/safeloader-cve-2016-4972-e3f7ad9b234655ca.yaml delete mode 100644 releasenotes/notes/support-endpoint-type-d7c6b32098b67eeb.yaml delete mode 100644 releasenotes/notes/yamlloader-glare-d7d0506f6711b650.yaml delete mode 100644 releasenotes/source/2023.1.rst delete mode 100644 releasenotes/source/_static/.placeholder delete mode 100644 releasenotes/source/_templates/.placeholder delete mode 100644 releasenotes/source/conf.py delete mode 100644 releasenotes/source/index.rst delete mode 100644 releasenotes/source/liberty.rst delete mode 100644 releasenotes/source/locale/en_GB/LC_MESSAGES/releasenotes.po delete mode 100644 releasenotes/source/locale/fr/LC_MESSAGES/releasenotes.po delete mode 100644 releasenotes/source/mitaka.rst delete mode 100644 releasenotes/source/newton.rst delete mode 100644 releasenotes/source/ocata.rst delete mode 100644 releasenotes/source/pike.rst delete mode 100644 releasenotes/source/queens.rst delete mode 100644 releasenotes/source/rocky.rst delete mode 100644 releasenotes/source/stein.rst delete mode 100644 releasenotes/source/train.rst delete mode 100644 releasenotes/source/unreleased.rst delete mode 100644 releasenotes/source/ussuri.rst delete mode 100644 releasenotes/source/victoria.rst delete mode 100644 releasenotes/source/wallaby.rst delete mode 100644 releasenotes/source/xena.rst delete mode 100644 releasenotes/source/yoga.rst delete mode 100644 releasenotes/source/zed.rst delete mode 100644 requirements.txt delete mode 100755 run_tests.sh delete mode 100644 setup-centos.sh delete mode 100644 setup.cfg delete mode 100644 setup.py delete mode 100644 test-requirements.txt delete mode 100644 tools/murano.bash_completion delete mode 100644 tox.ini diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 4b0776ca..00000000 --- a/.coveragerc +++ /dev/null @@ -1,8 +0,0 @@ -[run] -source = muranoclient -omit = - .tox/* - muranoclient/tests/* - -[report] -ignore_errors = True diff --git a/.gitignore b/.gitignore deleted file mode 100644 index e15eef69..00000000 --- a/.gitignore +++ /dev/null @@ -1,41 +0,0 @@ -#IntelJ Idea -.idea/ - -#virtualenv -.venv/ - -#Build results -build/ -dist/ -*.egg-info/ -.tox -AUTHORS -ChangeLog -eggs/ -.eggs/ -develop-eggs/ -*.egg - -#Python -*.pyc - -#Translation build -*.mo - -#SQLite Database files -*.sqlite - -#Autogenerated Documentation -doc/source/api - -#Testing framework -.stestr/ -.coverage -*,cover -cover - -#swap file -*.swp - -# Files created by releasenotes build -releasenotes/build diff --git a/.stestr.conf b/.stestr.conf deleted file mode 100644 index eff6feff..00000000 --- a/.stestr.conf +++ /dev/null @@ -1,4 +0,0 @@ -[DEFAULT] -test_path=${OS_TEST_PATH:-./muranoclient/tests/unit} -top_dir=./ - diff --git a/.zuul.yaml b/.zuul.yaml deleted file mode 100644 index 71ad5137..00000000 --- a/.zuul.yaml +++ /dev/null @@ -1,40 +0,0 @@ -- project: - templates: - - openstack-cover-jobs - - openstack-python3-jobs - - check-requirements - - release-notes-jobs-python3 - - publish-openstack-docs-pti - - openstackclient-plugin-jobs - check: - jobs: - - muranoclient-functional-test-mysql-backend - gate: - jobs: - - muranoclient-functional-test-mysql-backend - -- job: - name: muranoclient-functional-test-mysql-backend - parent: devstack-tox-functional - timeout: 4200 - voting: false - vars: - openrc_enable_export: true - devstack_plugins: - heat: https://opendev.org/openstack/heat - murano: https://opendev.org/openstack/murano - devstack_localrc: - KEYSTONE_ADMIN_ENDPOINT: true - irrelevant-files: - - ^(test-|)requirements.txt$ - - ^setup.cfg$ - - ^doc/.*$ - - ^.*\.rst$ - - ^releasenotes/.*$ - - ^muranoclient/tests/.*$ - required-projects: - - openstack/heat - - openstack/murano - - openstack/murano-dashboard - - openstack/python-heatclient - - openstack/python-muranoclient diff --git a/CONTRIBUTING.rst b/CONTRIBUTING.rst deleted file mode 100644 index c85609d7..00000000 --- a/CONTRIBUTING.rst +++ /dev/null @@ -1,19 +0,0 @@ -The source repository for this project can be found at: - - https://opendev.org/openstack/python-muranoclient - -Pull requests submitted through GitHub are not monitored. - -To start contributing to OpenStack, follow the steps in the contribution guide -to set up and use Gerrit: - - https://docs.openstack.org/contributors/code-and-documentation/quick-start.html - -Bugs should be filed on Launchpad: - - https://bugs.launchpad.net/python-muranoclient - -For more specific information about contributing to this repository, see the -python-muranoclient contributor guide: - - https://docs.openstack.org/python-muranoclient/latest/contributor/contributing.html diff --git a/HACKING.rst b/HACKING.rst deleted file mode 100644 index 39378cb3..00000000 --- a/HACKING.rst +++ /dev/null @@ -1,4 +0,0 @@ -Style Commandments -================== - -Read the OpenStack Style Commandments https://docs.openstack.org/hacking/latest/ diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 68c771a0..00000000 --- a/LICENSE +++ /dev/null @@ -1,176 +0,0 @@ - - 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. - diff --git a/README.rst b/README.rst index 08f44e32..4ee2c5f1 100644 --- a/README.rst +++ b/README.rst @@ -1,70 +1,10 @@ -======================== -Team and repository tags -======================== +This project is no longer maintained. -.. image:: https://governance.openstack.org/tc/badges/python-muranoclient.svg - :target: https://governance.openstack.org/tc/reference/tags/index.html +The contents of this repository are still available in the Git +source code management system. To see the contents of this +repository before it reached its end of life, please check out the +previous commit with "git checkout HEAD^1". -.. Change things from this point on - -Murano -====== - -.. image:: https://img.shields.io/pypi/v/python-muranoclient.svg - :target: https://pypi.org/project/python-muranoclient/ - :alt: Latest Version - -Murano Project introduces an application catalog, which allows application -developers and cloud administrators to publish various cloud-ready -applications in a browsable categorised catalog, which may be used by the -cloud users (including the inexperienced ones) to pick-up the needed -applications and services and composes the reliable environments out of them -in a "push-the-button" manner. - -* `PyPi`_ - package installation -* `Launchpad project`_ - release management -* `Blueprints`_ - feature specifications -* `Bugs`_ - issue tracking -* `Source`_ -* `Specs`_ -* `How to Contribute`_ - -.. _PyPi: https://pypi.org/project/python-muranoclient -.. _Launchpad project: https://launchpad.net/python-muranoclient -.. _Blueprints: https://blueprints.launchpad.net/python-muranoclient -.. _Bugs: https://bugs.launchpad.net/python-muranoclient -.. _Source: https://opendev.org/openstack/python-muranoclient -.. _How to Contribute: https://docs.openstack.org/infra/manual/developers.html -.. _Specs: https://specs.openstack.org/openstack/murano-specs/ -.. _Release Notes: https://docs.openstack.org/releasenotes/python-muranoclient - -Python Muranoclient -------------------- -python-muranoclient is a client library for Murano built on the Murano API. -It provides a Python API (the ``muranoclient`` module) and a command-line tool -(``murano``). - - -Project Resources ------------------ - -Project status, bugs, and blueprints are tracked on Launchpad: - -* Client bug tracker - * https://launchpad.net/python-muranoclient - -* Murano bug tracker - * https://launchpad.net/murano - -Developer documentation can be found here: - - https://docs.openstack.org/murano/latest/ - -Additional resources are linked from the project wiki page: - - https://wiki.openstack.org/wiki/Murano - -License -------- - -Apache License Version 2.0 http://www.apache.org/licenses/LICENSE-2.0 +For any further questions, please email +openstack-discuss@lists.openstack.org or join #openstack-dev on +OFTC. diff --git a/doc/requirements.txt b/doc/requirements.txt deleted file mode 100644 index 56f8df5a..00000000 --- a/doc/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -# The order of packages is significant, because pip processes them in the order -# of appearance. Changing the order has an impact on the overall integration -# process, which may cause wedges in the gate later. -sphinx>=2.0.0,!=2.1.0 # BSD -openstackdocstheme>=2.2.1 # Apache-2.0 -reno>=3.1.0 # Apache-2.0 diff --git a/doc/source/cli/index.rst b/doc/source/cli/index.rst deleted file mode 100644 index 85bc6539..00000000 --- a/doc/source/cli/index.rst +++ /dev/null @@ -1,42 +0,0 @@ -================= -Murano API Client -================= - -In order to use the python api directly, you must first obtain an auth token -and identify which endpoint you wish to speak to. Once you have done so, -you can use the API like so:: - - >>> from muranoclient import Client - >>> murano = Client('1', endpoint=MURANO_URL, token=OS_AUTH_TOKEN) - ... - - -Command-line Tool -================= - -In order to use the CLI, you must provide your OpenStack username, -password, tenant, and auth endpoint. Use the corresponding configuration -options (:option:``--os-username``, :option:``--os-password``, -:option:``--os-tenant-id``, and :option:``--os-auth-url``) or -set them in environment variables:: - - export OS_USERNAME=user - export OS_PASSWORD=pass - export OS_TENANT_ID=b363706f891f48019483f8bd6503c54b - export OS_AUTH_URL=http://auth.example.com:5000/v2.0 - -The command line tool will attempt to reauthenticate using your provided -credentials for every request. You can override this behavior by manually -supplying an auth token using :option:``--os-image-url`` and -:option:``--os-auth-token``. You can alternatively set these environment -variables:: - - export MURANO_URL=http://murano.example.org:8082/ - export OS_AUTH_TOKEN=3bcc3d3a03f44e3d8377f9247b0ad155 - -Once you've configured your authentication parameters, you can run -:command:`murano help` to see a complete listing of available commands. - -.. toctree:: - - murano diff --git a/doc/source/cli/murano.rst b/doc/source/cli/murano.rst deleted file mode 100644 index 41988abe..00000000 --- a/doc/source/cli/murano.rst +++ /dev/null @@ -1,1318 +0,0 @@ -======================================================== -Application Catalog service (murano) command-line client -======================================================== - -The murano client is the command-line interface (CLI) for -the Application Catalog service (murano) API and its extensions. - -This chapter documents :command:`murano` version ``0.13.0``. - -For help on a specific :command:`murano` command, enter: - -.. code-block:: console - - $ murano help COMMAND - -.. _murano_command_usage: - -murano usage -~~~~~~~~~~~~ - -.. code-block:: console - - usage: murano [--version] [-d] [-v] [--cert-file OS_CERT] [--key-file OS_KEY] - [--ca-file OS_CACERT] [--api-timeout API_TIMEOUT] - [--os-tenant-id OS_TENANT_ID] [--os-tenant-name OS_TENANT_NAME] - [--os-region-name OS_REGION_NAME] - [--os-auth-token OS_AUTH_TOKEN] [--os-no-client-auth] - [--murano-url MURANO_URL] [--glance-url GLANCE_URL] - [--glare-url GLARE_URL] - [--murano-api-version MURANO_API_VERSION] - [--os-service-type OS_SERVICE_TYPE] - [--os-endpoint-type OS_ENDPOINT_TYPE] [--include-password] - [--murano-repo-url MURANO_REPO_URL] - [--murano-packages-service {murano,glance,glare}] [--insecure] - [--os-cacert ] [--os-cert ] - [--os-key ] [--timeout ] - [--os-auth-url OS_AUTH_URL] [--os-domain-id OS_DOMAIN_ID] - [--os-domain-name OS_DOMAIN_NAME] - [--os-project-id OS_PROJECT_ID] - [--os-project-name OS_PROJECT_NAME] - [--os-project-domain-id OS_PROJECT_DOMAIN_ID] - [--os-project-domain-name OS_PROJECT_DOMAIN_NAME] - [--os-trust-id OS_TRUST_ID] [--os-user-id OS_USER_ID] - [--os-username OS_USERNAME] - [--os-user-domain-id OS_USER_DOMAIN_ID] - [--os-user-domain-name OS_USER_DOMAIN_NAME] - [--os-password OS_PASSWORD] - ... - -**Subcommands:** - -``app-show`` - List applications, added to specified environment. - -``bundle-import`` - Import a bundle. - -``bundle-save`` - Save a bundle. - -``category-create`` - Create a category. - -``category-delete`` - Delete a category. - -``category-list`` - List all available categories. - -``category-show`` - Display category details. - -``class-schema`` - Display class schema - -``deployment-list`` - List deployments for an environment or multiple - environments. - -``env-template-add-app`` - Add application to the environment template. - -``env-template-clone`` - Create a new template, cloned from template. - -``env-template-create`` - Create an environment template. - -``env-template-create-env`` - Create a new environment from template. - -``env-template-del-app`` - Delete application from the environment template. - -``env-template-delete`` - Delete an environment template. - -``env-template-list`` - List the environments templates. - -``env-template-show`` - Display environment template details. - -``env-template-update`` - Update an environment template. - -``environment-action-call`` - Call action \`ACTION\` in environment \`ID\`. - -``environment-action-get-result`` - Get result of \`TASK\` in environment \`ID\`. - -``environment-apps-edit`` - Edit environment's object model. - -``environment-create`` - Create an environment. - -``environment-delete`` - Delete an environment. - -``environment-deploy`` - Start deployment of a murano environment session. - -``environment-list`` - List the environments. - -``environment-model-edit`` - Edit an environment's object model. - -``environment-model-show`` - Display an environment's object model. - -``environment-rename`` - Rename an environment. - -``environment-session-create`` - Creates a new configuration session for environment - ID. - -``environment-show`` - Display environment details. - -``package-create`` - Create an application package. - -``package-delete`` - Delete a package. - -``package-download`` - Download a package to a filename or stdout. - -``package-import`` - Import a package. - -``package-list`` - List available packages. - -``package-save`` - Save a package. - -``package-show`` - Display details for a package. - -``package-update`` - Update an existing package. - -``static-action-call`` - Call static method \`METHOD\` of the class \`CLASS\` with - \`ARGUMENTS\`. - -``bash-completion`` - Prints all of the commands and options to stdout. - -``help`` - Display help about this program or one of its - subcommands. - -.. _murano_command_options: - -murano optional arguments -~~~~~~~~~~~~~~~~~~~~~~~~~ - -``--version`` - Show program's version number and exit. - -``-d, --debug`` - Defaults to ``env[MURANOCLIENT_DEBUG]``. - -``-v, --verbose`` - Print more verbose output. - -``--cert-file OS_CERT`` - **DEPRECATED!** Use --os-cert. - -``--key-file OS_KEY`` - **DEPRECATED!** Use --os-key. - -``--ca-file OS_CACERT`` - **DEPRECATED!** Use --os-cacert. - -``--api-timeout API_TIMEOUT`` - Number of seconds to wait for an API response, - defaults to system socket timeout. - -``--os-tenant-id OS_TENANT_ID`` - Defaults to ``env[OS_TENANT_ID]``. - -``--os-tenant-name OS_TENANT_NAME`` - Defaults to ``env[OS_TENANT_NAME]``. - -``--os-region-name OS_REGION_NAME`` - Defaults to ``env[OS_REGION_NAME]``. - -``--os-auth-token OS_AUTH_TOKEN`` - Defaults to ``env[OS_AUTH_TOKEN]``. - -``--os-no-client-auth`` - Do not contact keystone for a token. Defaults to - ``env[OS_NO_CLIENT_AUTH]``. - -``--murano-url MURANO_URL`` - Defaults to ``env[MURANO_URL]``. - -``--glance-url GLANCE_URL`` - Defaults to ``env[GLANCE_URL]``. - -``--glare-url GLARE_URL`` - Defaults to ``env[GLARE_URL]``. - -``--murano-api-version MURANO_API_VERSION`` - Defaults to ``env[MURANO_API_VERSION]`` or 1. - -``--os-service-type OS_SERVICE_TYPE`` - Defaults to ``env[OS_SERVICE_TYPE]``. - -``--os-endpoint-type OS_ENDPOINT_TYPE`` - Defaults to ``env[OS_ENDPOINT_TYPE]``. - -``--include-password`` - Send os-username and os-password to murano. - -``--murano-repo-url MURANO_REPO_URL`` - Defaults to ``env[MURANO_REPO_URL]`` or - http://apps.openstack.org/api/v1/murano_repo/liberty/ - -``--murano-packages-service {murano,glance,glare}`` - Specifies if murano-api ("murano") or Glance Artifact - Repository ("glare") should be used to store murano - packages. Defaults to ``env[MURANO_PACKAGES_SERVICE]`` or - to "murano" - -``--insecure`` - Explicitly allow client to perform "insecure" TLS - (https) requests. The server's certificate will not be - verified against any certificate authorities. This - option should be used with caution. - -``--os-cacert `` - Specify a CA bundle file to use in verifying a TLS - (https) server certificate. Defaults to - ``env[OS_CACERT]``. - -``--os-cert `` - Defaults to ``env[OS_CERT]``. - -``--os-key `` - Defaults to ``env[OS_KEY]``. - -``--timeout `` - Set request timeout (in seconds). - -``--os-auth-url OS_AUTH_URL`` - Authentication URL - -``--os-domain-id OS_DOMAIN_ID`` - Domain ID to scope to - -``--os-domain-name OS_DOMAIN_NAME`` - Domain name to scope to - -``--os-project-id OS_PROJECT_ID`` - Project ID to scope to - -``--os-project-name OS_PROJECT_NAME`` - Project name to scope to - -``--os-project-domain-id OS_PROJECT_DOMAIN_ID`` - Domain ID containing project - -``--os-project-domain-name OS_PROJECT_DOMAIN_NAME`` - Domain name containing project - -``--os-trust-id OS_TRUST_ID`` - Trust ID - -``--os-user-id OS_USER_ID`` - User ID - -``--os-username OS_USERNAME, --os-user-name OS_USERNAME, --os-user_name OS_USERNAME`` - Username - -``--os-user-domain-id OS_USER_DOMAIN_ID`` - User's domain id - -``--os-user-domain-name OS_USER_DOMAIN_NAME`` - User's domain name - -``--os-password OS_PASSWORD`` - User's password - -.. _murano_app-show: - -murano app-show ---------------- - -.. code-block:: console - - usage: murano app-show [-p ] - -List applications, added to specified environment. - -**Positional arguments:** - -```` - Environment ID to show applications from. - -**Optional arguments:** - -``-p , --path `` - Level of detalization to show. Leave empty to browse - all applications in the environment. - -.. _murano_bundle-import: - -murano bundle-import --------------------- - -.. code-block:: console - - usage: murano bundle-import [--is-public] [--exists-action {a,s,u}] - [ ...] - -Import -a -bundle. -\`FILE\` -can -be -either -a -path -to -a -zip -file, -URL, -or -name -from -repo. If \`FILE\` is a local file, treat names of packages in a bundle as file -names, relative to location of the bundle file. Requirements are first -searched in the same directory. - -**Positional arguments:** - -```` - Bundle URL, bundle name, or path to the bundle file. - -**Optional arguments:** - -``--is-public`` - Make packages available to users from other tenants. - -``--exists-action {a,s,u}`` - Default action when a package already exists. - -.. _murano_bundle-save: - -murano bundle-save ------------------- - -.. code-block:: console - - usage: murano bundle-save [-p ] [--no-images] - -Save a bundle. This will download a bundle of packages with all dependencies -to specified path. If path doesn't exist it will be created. - -**Positional arguments:** - -```` - Bundle URL, bundle name, or path to the bundle file. - -**Optional arguments:** - -``-p , --path `` - Path to the directory to store packages. If not set - will use current directory. - -``--no-images`` - If set will skip images downloading. - -.. _murano_category-create: - -murano category-create ----------------------- - -.. code-block:: console - - usage: murano category-create - -Create a category. - -**Positional arguments:** - -```` - Category name. - -.. _murano_category-delete: - -murano category-delete ----------------------- - -.. code-block:: console - - usage: murano category-delete [ ...] - -Delete a category. - -**Positional arguments:** - -```` - ID of a category(ies) to delete. - -.. _murano_category-list: - -murano category-list --------------------- - -.. code-block:: console - - usage: murano category-list - -List all available categories. - -.. _murano_category-show: - -murano category-show --------------------- - -.. code-block:: console - - usage: murano category-show - -Display category details. - -**Positional arguments:** - -```` - ID of a category(s) to show. - -.. _murano_class-schema: - -murano class-schema -------------------- - -.. code-block:: console - - usage: murano class-schema [--package-name PACKAGE_NAME] - [--class-version CLASS_VERSION] - [ [ ...]] - -Display class schema - -**Positional arguments:** - -```` - Class FQN - -```` - Method name - -**Optional arguments:** - -``--package-name PACKAGE_NAME`` - FQN of the package where the class is located - -``--class-version CLASS_VERSION`` - Class version or version range (version spec) - -.. _murano_deployment-list: - -murano deployment-list ----------------------- - -.. code-block:: console - - usage: murano deployment-list [--all-environments] [] - -List deployments for an environment or multiple environments. - -**Positional arguments:** - -```` - Environment ID for which to list deployments. - -**Optional arguments:** - -``--all-environments`` - Lists all deployments for all environments in user's - tenant. - -.. _murano_env-template-add-app: - -murano env-template-add-app ---------------------------- - -.. code-block:: console - - usage: murano env-template-add-app - -Add application to the environment template. - -**Positional arguments:** - -```` - Environment template ID. - -```` - Path to the template. - -.. _murano_env-template-clone: - -murano env-template-clone -------------------------- - -.. code-block:: console - - usage: murano env-template-clone - -Create a new template, cloned from template. - -**Positional arguments:** - -```` - Environment template ID. - -```` - New environment template name. - -.. _murano_env-template-create: - -murano env-template-create --------------------------- - -.. code-block:: console - - usage: murano env-template-create [--is-public] - -Create an environment template. - -**Positional arguments:** - -```` - Environment template name. - -**Optional arguments:** - -``--is-public`` - Make the template available for users from other - tenants. - -.. _murano_env-template-create-env: - -murano env-template-create-env ------------------------------- - -.. code-block:: console - - usage: murano env-template-create-env [--region ] - -Create a new environment from template. - -**Positional arguments:** - -```` - Environment template ID. - -```` - New environment name. - -**Optional arguments:** - -``--region `` - Name of the target OpenStack region. - -.. _murano_env-template-del-app: - -murano env-template-del-app ---------------------------- - -.. code-block:: console - - usage: murano env-template-del-app - -Delete application from the environment template. - -**Positional arguments:** - -```` - Environment template ID. - -```` - Application ID. - -.. _murano_env-template-delete: - -murano env-template-delete --------------------------- - -.. code-block:: console - - usage: murano env-template-delete [ ...] - -Delete an environment template. - -**Positional arguments:** - -```` - ID of environment(s) template to delete. - -.. _murano_env-template-list: - -murano env-template-list ------------------------- - -.. code-block:: console - - usage: murano env-template-list - -List the environments templates. - -.. _murano_env-template-show: - -murano env-template-show ------------------------- - -.. code-block:: console - - usage: murano env-template-show - -Display environment template details. - -**Positional arguments:** - -```` - Environment template ID. - -.. _murano_env-template-update: - -murano env-template-update --------------------------- - -.. code-block:: console - - usage: murano env-template-update - -Update an environment template. - -**Positional arguments:** - -```` - Environment template ID. - -```` - Environment template name. - -.. _murano_environment-action-call: - -murano environment-action-call ------------------------------- - -.. code-block:: console - - usage: murano environment-action-call --action-id - [--arguments [ [ ...]]] - id - -Call -action -\`ACTION\` -in -environment -\`ID\`. -Returns -id -of -an -asynchronous -task, -that executes the action. Actions can only be called on a \`deployed\` -environment. To view actions available in a given environment use -\`environment-show\` command. - -**Positional arguments:** - -``id`` - ID of Environment to call action against. - -**Optional arguments:** - -``--action-id `` - ID of action to run. - -``--arguments [ [ ...]]`` - Action arguments. - -.. _murano_environment-action-get-result: - -murano environment-action-get-result ------------------------------------- - -.. code-block:: console - - usage: murano environment-action-get-result --task-id - -Get result of \`TASK\` in environment \`ID\`. - -**Positional arguments:** - -```` - ID of Environment where task is being executed. - -**Optional arguments:** - -``--task-id `` - ID of action to run. - -.. _murano_environment-apps-edit: - -murano environment-apps-edit ----------------------------- - -.. code-block:: console - - usage: murano environment-apps-edit --session-id [FILE] - -Edit environment's object model. \`FILE\` is path to a file, that contains -jsonpatch, that describes changes to be made to environment's object-model. [ -{ "op": "add", "path": "/-", "value": { ... your-app object model here ... } -}, { "op": "replace", "path": "/0/?/name", "value": "new_name" }, ] NOTE: -Values '===id1===', '===id2===', etc. in the resulting object-model will be -substituted with uuids. For more info on jsonpatch see RFC 6902 - -**Positional arguments:** - -```` - ID of Environment to edit. - -``FILE`` - File to read jsonpatch from (defaults to stdin). - -**Optional arguments:** - -``--session-id `` - Id of a config session. - -.. _murano_environment-create: - -murano environment-create -------------------------- - -.. code-block:: console - - usage: murano environment-create [--join-net-id ] - [--join-subnet-id ] - [--region ] - - -Create an environment. - -**Positional arguments:** - -```` - Environment name. - -**Optional arguments:** - -``--join-net-id `` - Network id to join. - -``--join-subnet-id `` - Subnetwork id to join. - -``--region `` - Name of the target OpenStack region. - -.. _murano_environment-delete: - -murano environment-delete -------------------------- - -.. code-block:: console - - usage: murano environment-delete [--abandon] [ ...] - -Delete an environment. - -**Positional arguments:** - -```` - Id or name of environment(s) to delete. - -**Optional arguments:** - -``--abandon`` - If set will abandon environment without deleting any of its - resources. - -.. _murano_environment-deploy: - -murano environment-deploy -------------------------- - -.. code-block:: console - - usage: murano environment-deploy --session-id - -Start deployment of a murano environment session. - -**Positional arguments:** - -```` - ID of Environment to deploy. - -**Optional arguments:** - -``--session-id `` - ID of configuration session to deploy. - -.. _murano_environment-list: - -murano environment-list ------------------------ - -.. code-block:: console - - usage: murano environment-list [--all-tenants] [--tenant ] - -List the environments. - -**Optional arguments:** - -``--all-tenants`` - Allows to list environments from all tenants (admin - only). - -``--tenant `` - Allows to list environments for a given tenant (admin - only). - -.. _murano_environment-model-edit: - -murano environment-model-edit ------------------------------ - -.. code-block:: console - - usage: murano environment-model-edit --session-id [] - -Edit an environment's object model. - -**Positional arguments:** - -```` - ID of Environment to edit. - -```` - File to read JSON-patch from (defaults to stdin). - -**Optional arguments:** - -``--session-id `` - Id of a config session. - -.. _murano_environment-model-show: - -murano environment-model-show ------------------------------ - -.. code-block:: console - - usage: murano environment-model-show [--path ] - [--session-id ] - - -Display an environment's object model. - -**Positional arguments:** - -```` - ID of Environment to show. - -**Optional arguments:** - -``--path `` - Path to Environment model section. Defaults to '/'. - -``--session-id `` - Id of a config session. - -.. _murano_environment-rename: - -murano environment-rename -------------------------- - -.. code-block:: console - - usage: murano environment-rename - -Rename an environment. - -**Positional arguments:** - -```` - Environment ID or name. - -```` - A name to which the environment will be renamed. - -.. _murano_environment-session-create: - -murano environment-session-create ---------------------------------- - -.. code-block:: console - - usage: murano environment-session-create - -Creates a new configuration session for environment ID. - -**Positional arguments:** - -```` - ID of Environment to add session to. - -.. _murano_environment-show: - -murano environment-show ------------------------ - -.. code-block:: console - - usage: murano environment-show [--session-id ] [--only-apps] - - -Display environment details. - -**Positional arguments:** - -```` - Environment ID or name. - -**Optional arguments:** - -``--session-id `` - Id of a config session. - -``--only-apps`` - Only print apps of the environment (useful for - automation). - -.. _murano_package-create: - -murano package-create ---------------------- - -.. code-block:: console - - usage: murano package-create [-t ] [-c ] - [-r ] [-n ] - [-f ] [-a ] - [--tags [ [ ...]]] - [-d ] [-o ] - [-u ] [--type TYPE] [-l ] - -Create an application package. - -**Optional arguments:** - -``-t , --template `` - Path to the Heat template to import as an Application - Definition. - -``-c , --classes-dir `` - Path to the directory containing application classes. - -``-r , --resources-dir `` - Path to the directory containing application - resources. - -``-n , --name `` - Display name of the Application in Catalog. - -``-f , --full-name `` - Fully-qualified name of the Application in Catalog. - -``-a , --author `` - Name of the publisher. - -``--tags [ [ ...]]`` - A list of keywords connected to the application. - -``-d , --description `` - Detailed description for the Application in Catalog. - -``-o , --output `` - The name of the output file archive to save locally. - -``-u , --ui `` - Dynamic UI form definition. - -``--type TYPE`` - Package type. Possible values: Application or Library. - -``-l , --logo `` - Path to the package logo. - -.. _murano_package-delete: - -murano package-delete ---------------------- - -.. code-block:: console - - usage: murano package-delete [ ...] - -Delete a package. - -**Positional arguments:** - -```` - Package ID to delete. - -.. _murano_package-download: - -murano package-download ------------------------ - -.. code-block:: console - - usage: murano package-download [file] - -Download a package to a filename or stdout. - -**Positional arguments:** - -```` - Package ID to download. - -``file`` - Filename to save package to. If it is not specified and there is no - stdout redirection the package won't be saved. - -.. _murano_package-import: - -murano package-import ---------------------- - -.. code-block:: console - - usage: murano package-import [-c [ [ ...]]] [--is-public] - [--package-version PACKAGE_VERSION] - [--exists-action {a,s,u}] - [--dep-exists-action {a,s,u}] - [ ...] - -Import a package. \`FILE\` can be either a path to a zip file, url or a FQPN. -You -can -use -\`--\` -to -separate -\`FILE\`s -from -other -arguments. -Categories -have -to -be separated with a space and have to be already present in murano. - -**Positional arguments:** - -```` - URL of the murano zip package, FQPN, path to zip - package or path to directory with package. - -**Optional arguments:** - -``-c [ [ ...]], --categories [ [ ...]]`` - Category list to attach. - -``--is-public`` - Make the package available for users from other - tenants. - -``--package-version PACKAGE_VERSION`` - Version of the package to use from repository (ignored - when importing with multiple packages). - -``--exists-action {a,s,u}`` - Default action when a package already exists: (s)kip, - (u)pdate, (a)bort. - -``--dep-exists-action {a,s,u}`` - Default action when a dependency package already - exists: (s)kip, (u)pdate, (a)bort. - -.. _murano_package-list: - -murano package-list -------------------- - -.. code-block:: console - - usage: murano package-list [--limit LIMIT] [--marker MARKER] - [--include-disabled] [--owned] - [--search ] [--name ] - [--fqn ] - [--type ] - [--category ] - [--class_name ] - [--tag ] - -List available packages. - -**Optional arguments:** - -``--limit LIMIT`` - Show limited number of packages - -``--marker MARKER`` - Show packages starting from package with id excluding - it - -``--include-disabled`` - -``--owned`` - -``--search `` - Show packages, that match search keys fuzzily - -``--name `` - Show packages, whose name match parameter exactly - -``--fqn `` - Show packages, whose fully qualified name match - parameter exactly - -``--type `` - Show packages, whose type match parameter exactly - -``--category `` - Show packages, whose categories include parameter - -``--class_name `` - Show packages, whose class name match parameter - exactly - -``--tag `` - Show packages, whose tags include parameter - -.. _murano_package-save: - -murano package-save -------------------- - -.. code-block:: console - - usage: murano package-save [-p ] [--package-version PACKAGE_VERSION] - [--no-images] - [ ...] - -Save a package. This will download package(s) with all dependencies to -specified path. If path doesn't exist it will be created. - -**Positional arguments:** - -```` - Package URL or name. - -**Optional arguments:** - -``-p , --path `` - Path to the directory to store package. If not set - will use current directory. - -``--package-version PACKAGE_VERSION`` - Version of the package to use from repository (ignored - when saving with multiple packages). - -``--no-images`` - If set will skip images downloading. - -.. _murano_package-show: - -murano package-show -------------------- - -.. code-block:: console - - usage: murano package-show - -Display details for a package. - -**Positional arguments:** - -```` - Package ID to show. - -.. _murano_package-update: - -murano package-update ---------------------- - -.. code-block:: console - - usage: murano package-update [--is-public {true|false}] - [--enabled {true|false}] [--name NAME] - [--description DESCRIPTION] - [--tags [ [ ...]]] - - -Update an existing package. - -**Positional arguments:** - -```` - Package ID to update. - -**Optional arguments:** - -``--is-public {true|false}`` - Make package available to users from other tenants. - -``--enabled {true|false}`` - Make package active and available for deployments. - -``--name NAME`` - New name for the package. - -``--description DESCRIPTION`` - New package description. - -``--tags [ [ ...]]`` - A list of keywords connected to the application. - -.. _murano_static-action-call: - -murano static-action-call -------------------------- - -.. code-block:: console - - usage: murano static-action-call [--arguments [ [ ...]]] - [--package-name ] - [--class-version CLASS_VERSION] - - -Call -static -method -\`METHOD\` -of -the -class -\`CLASS\` -with -\`ARGUMENTS\`. -Returns -the -result -of -the -method -execution. -\`PACKAGE\` -and -\`CLASS_VERSION\` -can -be -specified -optionally to find class in a particular package and to look for the specific -version of a class respectively. - -**Positional arguments:** - -```` - FQN of the class with static method - -```` - Static method to run - -**Optional arguments:** - -``--arguments [ [ ...]]`` - Method arguments. No arguments by default - -``--package-name `` - Optional FQN of the package to look for the class in - -``--class-version CLASS_VERSION`` - Optional version of the class, otherwise version =0 is - used - diff --git a/doc/source/conf.py b/doc/source/conf.py deleted file mode 100644 index 5b735cda..00000000 --- a/doc/source/conf.py +++ /dev/null @@ -1,83 +0,0 @@ -# -*- coding: utf-8 -*- -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os - -# -- General configuration ---------------------------------------------------- - -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', - 'openstackdocstheme',] - -# autodoc generation is a bit aggressive and a nuisance when doing heavy -# text edit cycles. -# execute "export SPHINX_DEBUG=1" in your terminal to disable - -# Add any paths that contain templates here, relative to this directory. -templates_path = ['_templates'] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The master toctree document. -master_doc = 'index' - -# General information about the project. -copyright = u'OpenStack Foundation' -exclude_trees = ['api'] - -# If true, '()' will be appended to :func: etc. cross-reference text. -add_function_parentheses = True - -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -add_module_names = True - -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'native' - -# -- Options for HTML output -------------------------------------------------- - -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = 'openstackdocs' - -# Add any paths that contain custom themes here, relative to this directory. -#html_theme_path = ['_theme'] -#html_theme_path = [openstackdocstheme.get_html_theme_path()] - -# openstackdocstheme options -openstackdocs_repo_name = 'openstack/python-muranoclient' -openstackdocs_bug_project = 'python-muranoclient' -openstackdocs_bug_tag = '' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'python-muranoclientdoc' - - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass -# [howto/manual]). -latex_documents = [ - ( - 'index', - 'python-muranoclient.tex', - u'python-muranoclient Documentation', - u'OpenStack Foundation', - 'manual' - ), -] - -# Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} diff --git a/doc/source/contributor/contributing.rst b/doc/source/contributor/contributing.rst deleted file mode 100644 index c32f2b9b..00000000 --- a/doc/source/contributor/contributing.rst +++ /dev/null @@ -1,47 +0,0 @@ -============================ -So You Want to Contribute... -============================ -For general information on contributing to OpenStack, please check out the -`contributor guide `_ to get started. -It covers all the basics that are common to all OpenStack projects: the accounts -you need, the basics of interacting with our Gerrit review system, how we -communicate as a community, etc. -Below will cover the more project specific information you need to get started -with python-muranoclient. - -Communication -~~~~~~~~~~~~~ -* IRC channel #murano at OFTC -* Mailing list (prefix subjects with ``[murano]`` for faster responses) - http://lists.openstack.org/cgi-bin/mailman/listinfo/openstack-discuss - -Contacting the Core Team -~~~~~~~~~~~~~~~~~~~~~~~~ -Please refer the `python-muranoclient Core Team -`_ contacts. - -New Feature Planning -~~~~~~~~~~~~~~~~~~~~ -python-muranoclient features are tracked on `Launchpad `_. - -Task Tracking -~~~~~~~~~~~~~ -We track our tasks in `Launchpad `_. -If you're looking for some smaller, easier work item to pick up and get started -on, search for the 'low-hanging-fruit' tag. - -Reporting a Bug -~~~~~~~~~~~~~~~ -You found an issue and want to make sure we are aware of it? You can do so on -`Launchpad `_. - -Getting Your Patch Merged -~~~~~~~~~~~~~~~~~~~~~~~~~ -All changes proposed to the python-muranoclient project require one or two +2 votes -from python-muranoclient core reviewers before one of the core reviewers can approve -patch by giving ``Workflow +1`` vote. - -Project Team Lead Duties -~~~~~~~~~~~~~~~~~~~~~~~~ -All common PTL duties are enumerated in the `PTL guide -`_. diff --git a/doc/source/index.rst b/doc/source/index.rst deleted file mode 100644 index 3aec7b67..00000000 --- a/doc/source/index.rst +++ /dev/null @@ -1,23 +0,0 @@ -================================= -python-muranoclient documentation -================================= - -This is a client for the OpenStack Application Catalog API. -There's a Python API (the :mod:`muranoclient` -module) and a :doc:`command-line script ` -(installed as :program:`murano`). - -.. toctree:: - :maxdepth: 2 - - cli/index - -For Contributors -================ - -* If you are a new contributor to python-muranoclient please refer: :doc:`contributor/contributing` - - .. toctree:: - :hidden: - - contributor/contributing diff --git a/muranoclient/__init__.py b/muranoclient/__init__.py deleted file mode 100644 index 083d7b8f..00000000 --- a/muranoclient/__init__.py +++ /dev/null @@ -1,31 +0,0 @@ -# Copyright (c) 2014 Mirantis, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os - -import pbr.version - -_ROOT = os.path.abspath(os.path.dirname(__file__)) - - -def get_resource(path): - return os.path.join(_ROOT, 'data', path) - - -version_info = pbr.version.VersionInfo('python-muranoclient') - -try: - __version__ = version_info.version_string() -except AttributeError: - __version__ = None diff --git a/muranoclient/apiclient/__init__.py b/muranoclient/apiclient/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/muranoclient/apiclient/auth.py b/muranoclient/apiclient/auth.py deleted file mode 100644 index 38be9922..00000000 --- a/muranoclient/apiclient/auth.py +++ /dev/null @@ -1,216 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# Copyright 2013 Spanish National Research Council. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -# E0202: An attribute inherited from %s hide this method -# pylint: disable=E0202 - -import abc -import argparse -import os - -from stevedore import extension - -from muranoclient.apiclient import exceptions - - -_discovered_plugins = {} - - -def discover_auth_systems(): - """Discover the available auth-systems. - - This won't take into account the old style auth-systems. - """ - global _discovered_plugins - _discovered_plugins = {} - - def add_plugin(ext): - _discovered_plugins[ext.name] = ext.plugin - - ep_namespace = "muranoclient.apiclient.auth" - mgr = extension.ExtensionManager(ep_namespace) - mgr.map(add_plugin) - - -def load_auth_system_opts(parser): - """Load options needed by the available auth-systems into a parser. - - This function will try to populate the parser with options from the - available plugins. - """ - group = parser.add_argument_group("Common auth options") - BaseAuthPlugin.add_common_opts(group) - for name, auth_plugin in _discovered_plugins.items(): - group = parser.add_argument_group( - "Auth-system '%s' options" % name, - conflict_handler="resolve") - auth_plugin.add_opts(group) - - -def load_plugin(auth_system): - try: - plugin_class = _discovered_plugins[auth_system] - except KeyError: - raise exceptions.AuthSystemNotFound(auth_system) - return plugin_class(auth_system=auth_system) - - -def load_plugin_from_args(args): - """Load required plugin and populate it with options. - - Try to guess auth system if it is not specified. Systems are tried in - alphabetical order. - - :type args: argparse.Namespace - :raises: AuthPluginOptionsMissing - """ - auth_system = args.os_auth_system - if auth_system: - plugin = load_plugin(auth_system) - plugin.parse_opts(args) - plugin.sufficient_options() - return plugin - - for plugin_auth_system in sorted(iter(_discovered_plugins.keys())): - plugin_class = _discovered_plugins[plugin_auth_system] - plugin = plugin_class() - plugin.parse_opts(args) - try: - plugin.sufficient_options() - except exceptions.AuthPluginOptionsMissing: - continue - return plugin - raise exceptions.AuthPluginOptionsMissing(["auth_system"]) - - -class BaseAuthPlugin(object, metaclass=abc.ABCMeta): - """Base class for authentication plugins. - - An authentication plugin needs to override at least the authenticate - method to be a valid plugin. - """ - - auth_system = None - opt_names = [] - common_opt_names = [ - "auth_system", - "username", - "password", - "tenant_name", - "token", - "auth_url", - ] - - def __init__(self, auth_system=None, **kwargs): - self.auth_system = auth_system or self.auth_system - self.opts = dict((name, kwargs.get(name)) - for name in self.opt_names) - - @staticmethod - def _parser_add_opt(parser, opt): - """Add an option to parser in two variants. - - :param opt: option name (with underscores) - """ - dashed_opt = opt.replace("_", "-") - env_var = "OS_%s" % opt.upper() - arg_default = os.environ.get(env_var, "") - arg_help = "Defaults to env[%s]." % env_var - parser.add_argument( - "--os-%s" % dashed_opt, - metavar="<%s>" % dashed_opt, - default=arg_default, - help=arg_help) - parser.add_argument( - "--os_%s" % opt, - metavar="<%s>" % dashed_opt, - help=argparse.SUPPRESS) - - @classmethod - def add_opts(cls, parser): - """Populate the parser with the options for this plugin.""" - for opt in cls.opt_names: - # use `BaseAuthPlugin.common_opt_names` since it is never - # changed in child classes - if opt not in BaseAuthPlugin.common_opt_names: - cls._parser_add_opt(parser, opt) - - @classmethod - def add_common_opts(cls, parser): - """Add options that are common for several plugins.""" - for opt in cls.common_opt_names: - cls._parser_add_opt(parser, opt) - - @staticmethod - def get_opt(opt_name, args): - """Return option name and value. - - :param opt_name: name of the option, e.g., "username" - :param args: parsed arguments - """ - return (opt_name, getattr(args, "os_%s" % opt_name, None)) - - def parse_opts(self, args): - """Parse the actual auth-system options if any. - - This method is expected to populate the attribute `self.opts` with a - dict containing the options and values needed to make authentication. - """ - self.opts.update(dict(self.get_opt(opt_name, args) - for opt_name in self.opt_names)) - - def authenticate(self, http_client): - """Authenticate using plugin defined method. - - The method usually analyses `self.opts` and performs - a request to authentication server. - - :param http_client: client object that needs authentication - :type http_client: HTTPClient - :raises: AuthorizationFailure - """ - self.sufficient_options() - self._do_authenticate(http_client) - - @abc.abstractmethod - def _do_authenticate(self, http_client): - """Protected method for authentication.""" - - def sufficient_options(self): - """Check if all required options are present. - - :raises: AuthPluginOptionsMissing - """ - missing = [opt - for opt in self.opt_names - if not self.opts.get(opt)] - if missing: - raise exceptions.AuthPluginOptionsMissing(missing) - - @abc.abstractmethod - def token_and_endpoint(self, endpoint_type, service_type): - """Return token and endpoint. - - :param service_type: Service type of the endpoint - :type service_type: string - :param endpoint_type: Type of endpoint. - Possible values: public or publicURL, - internal or internalURL, - admin or adminURL - :type endpoint_type: string - :returns: tuple of token and endpoint strings - :raises: EndpointException - """ diff --git a/muranoclient/apiclient/base.py b/muranoclient/apiclient/base.py deleted file mode 100644 index 665fc55b..00000000 --- a/muranoclient/apiclient/base.py +++ /dev/null @@ -1,523 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack Foundation -# Copyright 2012 Grid Dynamics -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Base utilities to build API operation managers and objects on top of. -""" - -# E1102: %s is not callable -# pylint: disable=E1102 - -import abc -import copy - -from oslo_utils import strutils -from oslo_utils import uuidutils -import parse - -from muranoclient.apiclient import exceptions -from muranoclient.i18n import _ - - -def getid(obj): - """Return id if argument is a Resource. - - Abstracts the common pattern of allowing both an object or an object's ID - (UUID) as a parameter when dealing with relationships. - """ - try: - if obj.uuid: - return obj.uuid - except AttributeError: - pass - try: - return obj.id - except AttributeError: - return obj - - -# TODO(aababilov): call run_hooks() in HookableMixin's child classes -class HookableMixin(object): - """Mixin so classes can register and run hooks.""" - _hooks_map = {} - - @classmethod - def add_hook(cls, hook_type, hook_func): - """Add a new hook of specified type. - - :param cls: class that registers hooks - :param hook_type: hook type, e.g., '__pre_parse_args__' - :param hook_func: hook function - """ - if hook_type not in cls._hooks_map: - cls._hooks_map[hook_type] = [] - - cls._hooks_map[hook_type].append(hook_func) - - @classmethod - def run_hooks(cls, hook_type, *args, **kwargs): - """Run all hooks of specified type. - - :param cls: class that registers hooks - :param hook_type: hook type, e.g., '__pre_parse_args__' - :param args: args to be passed to every hook function - :param kwargs: kwargs to be passed to every hook function - """ - hook_funcs = cls._hooks_map.get(hook_type) or [] - for hook_func in hook_funcs: - hook_func(*args, **kwargs) - - -class BaseManager(HookableMixin): - """Basic manager type providing common operations. - - Managers interact with a particular type of API (servers, flavors, images, - etc.) and provide CRUD operations for them. - """ - resource_class = None - - def __init__(self, client): - """Initializes BaseManager with `client`. - - :param client: instance of BaseClient descendant for HTTP requests - """ - super(BaseManager, self).__init__() - self.client = client - - def _list(self, url, response_key, obj_class=None, json=None): - """List the collection. - - :param url: a partial URL, e.g., '/servers' - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' - :param obj_class: class for constructing the returned objects - (self.resource_class will be used by default) - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - """ - if json: - body = self.client.post(url, json=json).json() - else: - body = self.client.get(url).json() - - if obj_class is None: - obj_class = self.resource_class - - data = body[response_key] - # NOTE(ja): keystone returns values as list as {'values': [ ... ]} - # unlike other services which just return the list... - try: - data = data['values'] - except (KeyError, TypeError): - pass - - return [obj_class(self, res, loaded=True) for res in data if res] - - def _get(self, url, response_key): - """Get an object from collection. - - :param url: a partial URL, e.g., '/servers' - :param response_key: the key to be looked up in response dictionary, - e.g., 'server' - """ - body = self.client.get(url).json() - return self.resource_class(self, body[response_key], loaded=True) - - def _head(self, url): - """Retrieve request headers for an object. - - :param url: a partial URL, e.g., '/servers' - """ - resp = self.client.head(url) - return resp.status_code == 204 - - def _post(self, url, json, response_key, return_raw=False): - """Create an object. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' - :param return_raw: flag to force returning raw JSON instead of - Python object of self.resource_class - """ - body = self.client.post(url, json=json).json() - if return_raw: - return body[response_key] - return self.resource_class(self, body[response_key]) - - def _put(self, url, json=None, response_key=None): - """Update an object with PUT method. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' - """ - resp = self.client.put(url, json=json) - # PUT requests may not return a body - if resp.content: - body = resp.json() - if response_key is not None: - return self.resource_class(self, body[response_key]) - else: - return self.resource_class(self, body) - - def _patch(self, url, json=None, response_key=None): - """Update an object with PATCH method. - - :param url: a partial URL, e.g., '/servers' - :param json: data that will be encoded as JSON and passed in POST - request (GET will be sent by default) - :param response_key: the key to be looked up in response dictionary, - e.g., 'servers' - """ - body = self.client.patch(url, json=json).json() - if response_key is not None: - return self.resource_class(self, body[response_key]) - else: - return self.resource_class(self, body) - - def _delete(self, url): - """Delete an object. - - :param url: a partial URL, e.g., '/servers/my-server' - """ - return self.client.delete(url) - - -class ManagerWithFind(BaseManager, metaclass=abc.ABCMeta): - """Manager with additional `find()`/`findall()` methods.""" - - @abc.abstractmethod - def list(self): - pass - - def find(self, **kwargs): - """Find a single item with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - matches = self.findall(**kwargs) - num_matches = len(matches) - if num_matches == 0: - msg = _("No %(name)s matching %(args)s.") % { - 'name': self.resource_class.__name__, - 'args': kwargs - } - raise exceptions.NotFound(msg) - elif num_matches > 1: - raise exceptions.NoUniqueMatch() - else: - return matches[0] - - def findall(self, **kwargs): - """Find all items with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - found = [] - searches = kwargs.items() - - for obj in self.list(): - try: - if all(getattr(obj, attr) == value - for (attr, value) in searches): - found.append(obj) - except AttributeError: - continue - - return found - - -class CrudManager(BaseManager): - """Base manager class for manipulating entities. - - Children of this class are expected to define a `collection_key` and `key`. - - - `collection_key`: Usually a plural noun by convention (e.g. `entities`); - used to refer collections in both URL's (e.g. `/v3/entities`) and JSON - objects containing a list of member resources (e.g. `{'entities': [{}, - {}, {}]}`). - - `key`: Usually a singular noun by convention (e.g. `entity`); used to - refer to an individual member of the collection. - - """ - collection_key = None - key = None - - def build_url(self, base_url=None, **kwargs): - """Builds a resource URL for the given kwargs. - - Given an example collection where `collection_key = 'entities'` and - `key = 'entity'`, the following URL's could be generated. - - By default, the URL will represent a collection of entities, e.g.:: - - /entities - - If kwargs contains an `entity_id`, then the URL will represent a - specific member, e.g.:: - - /entities/{entity_id} - - :param base_url: if provided, the generated URL will be appended to it - """ - url = base_url if base_url is not None else '' - - url += '/%s' % self.collection_key - - # do we have a specific entity? - entity_id = kwargs.get('%s_id' % self.key) - if entity_id is not None: - url += '/%s' % entity_id - - return url - - def _filter_kwargs(self, kwargs): - """Drop null values and handle ids.""" - for key, ref in kwargs.copy().items(): - if ref is None: - kwargs.pop(key) - else: - if isinstance(ref, Resource): - kwargs.pop(key) - kwargs['%s_id' % key] = getid(ref) - return kwargs - - def create(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._post( - self.build_url(**kwargs), - {self.key: kwargs}, - self.key) - - def get(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._get( - self.build_url(**kwargs), - self.key) - - def head(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - return self._head(self.build_url(**kwargs)) - - def list(self, base_url=None, **kwargs): - """List the collection. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - return self._list( - '%(base_url)s%(query)s' % { - 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) - - def put(self, base_url=None, **kwargs): - """Update an element. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - return self._put(self.build_url(base_url=base_url, **kwargs)) - - def update(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - params = kwargs.copy() - params.pop('%s_id' % self.key) - - return self._patch( - self.build_url(**kwargs), - {self.key: params}, - self.key) - - def delete(self, **kwargs): - kwargs = self._filter_kwargs(kwargs) - - return self._delete( - self.build_url(**kwargs)) - - def find(self, base_url=None, **kwargs): - """Find a single item with attributes matching ``**kwargs``. - - :param base_url: if provided, the generated URL will be appended to it - """ - kwargs = self._filter_kwargs(kwargs) - - rl = self._list( - '%(base_url)s%(query)s' % { - 'base_url': self.build_url(base_url=base_url, **kwargs), - 'query': '?%s' % parse.urlencode(kwargs) if kwargs else '', - }, - self.collection_key) - num = len(rl) - - if num == 0: - msg = _("No %(name)s matching %(args)s.") % { - 'name': self.resource_class.__name__, - 'args': kwargs - } - raise exceptions.NotFound(404, msg) - elif num > 1: - raise exceptions.NoUniqueMatch - else: - return rl[0] - - -class Extension(HookableMixin): - """Extension descriptor.""" - - SUPPORTED_HOOKS = ('__pre_parse_args__', '__post_parse_args__') - manager_class = None - - def __init__(self, name, module): - super(Extension, self).__init__() - self.name = name - self.module = module - self._parse_extension_module() - - def _parse_extension_module(self): - self.manager_class = None - for attr_name, attr_value in self.module.__dict__.items(): - if attr_name in self.SUPPORTED_HOOKS: - self.add_hook(attr_name, attr_value) - else: - try: - if issubclass(attr_value, BaseManager): - self.manager_class = attr_value - except TypeError: - pass - - def __repr__(self): - return "" % self.name - - -class Resource(object): - """Base class for OpenStack resources (tenant, user, etc.). - - This is pretty much just a bag for attributes. - """ - - HUMAN_ID = False - NAME_ATTR = 'name' - - def __init__(self, manager, info, loaded=False): - """Populate and bind to a manager. - - :param manager: BaseManager object - :param info: dictionary representing resource attributes - :param loaded: prevent lazy-loading if set to True - """ - self.manager = manager - self._info = info - self._add_details(info) - self._loaded = loaded - self._init_completion_cache() - - def _init_completion_cache(self): - cache_write = getattr(self.manager, 'write_to_completion_cache', None) - if not cache_write: - return - - # NOTE(sirp): ensure `id` is already present because if it isn't we'll - # enter an infinite loop of __getattr__ -> get -> __init__ -> - # __getattr__ -> ... - if 'id' in self.__dict__ and uuidutils.is_uuid_like(self.id): - cache_write('uuid', self.id) - - if self.human_id: - cache_write('human_id', self.human_id) - - def __repr__(self): - reprkeys = sorted(k - for k in self.__dict__.keys() - if k[0] != '_' and k != 'manager') - info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) - return "<%s %s>" % (self.__class__.__name__, info) - - @property - def human_id(self): - """Human-readable ID which can be used for bash completion.""" - if self.HUMAN_ID: - name = getattr(self, self.NAME_ATTR, None) - if name is not None: - return strutils.to_slug(name) - return None - - def _add_details(self, info): - for (k, v) in info.items(): - try: - setattr(self, k, v) - self._info[k] = v - except AttributeError: - # In this case we already defined the attribute on the class - pass - - def __getattr__(self, k): - if k not in self.__dict__: - # NOTE(bcwaldon): disallow lazy-loading if already loaded once - if not self.is_loaded(): - self.get() - return self.__getattr__(k) - - raise AttributeError(k) - else: - return self.__dict__[k] - - def get(self): - """Support for lazy loading details. - - Some clients, such as novaclient have the option to lazy load the - details, details which can be loaded with this function. - """ - # set_loaded() first ... so if we have to bail, we know we tried. - self.set_loaded(True) - if not hasattr(self.manager, 'get'): - return - - new = self.manager.get(self.id) - if new: - self._add_details(new._info) - - def __eq__(self, other): - if not isinstance(other, Resource): - return NotImplemented - # two resources of different types are not equal - if not isinstance(other, self.__class__): - return False - return self._info == other._info - - def __ne__(self, other): - return not self.__eq__(other) - - def is_loaded(self): - return self._loaded - - def set_loaded(self, val): - self._loaded = val - - def to_dict(self): - return copy.deepcopy(self._info) diff --git a/muranoclient/apiclient/client.py b/muranoclient/apiclient/client.py deleted file mode 100644 index 00f19299..00000000 --- a/muranoclient/apiclient/client.py +++ /dev/null @@ -1,359 +0,0 @@ -# Copyright 2010 Jacob Kaplan-Moss -# Copyright 2011 OpenStack Foundation -# Copyright 2011 Piston Cloud Computing, Inc. -# Copyright 2013 Alessio Ababilov -# Copyright 2013 Grid Dynamics -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -OpenStack Client interface. Handles the REST calls and responses. -""" - -# E0202: An attribute inherited from %s hide this method -# pylint: disable=E0202 - -try: - import simplejson as json -except ImportError: - import json - -import time - -from oslo_log import log as logging -from oslo_utils import importutils -import requests - -from muranoclient.apiclient import exceptions -from muranoclient.i18n import _ - - -_logger = logging.getLogger(__name__) - - -class HTTPClient(object): - """This client handles sending HTTP requests to OpenStack servers. - - Features: - - - share authentication information between several clients to different - services (e.g., for compute and image clients); - - reissue authentication request for expired tokens; - - encode/decode JSON bodies; - - raise exceptions on HTTP errors; - - pluggable authentication; - - store authentication information in a keyring; - - store time spent for requests; - - register clients for particular services, so one can use - `http_client.identity` or `http_client.compute`; - - log requests and responses in a format that is easy to copy-and-paste - into terminal and send the same request with curl. - """ - - user_agent = "muranoclient.apiclient" - - def __init__(self, - auth_plugin, - region_name=None, - endpoint_type="publicURL", - original_ip=None, - verify=True, - cert=None, - timeout=None, - timings=False, - keyring_saver=None, - debug=False, - user_agent=None, - http=None): - self.auth_plugin = auth_plugin - - self.endpoint_type = endpoint_type - self.region_name = region_name - - self.original_ip = original_ip - self.timeout = timeout - self.verify = verify - self.cert = cert - - self.keyring_saver = keyring_saver - self.debug = debug - self.user_agent = user_agent or self.user_agent - - self.times = [] # [("item", starttime, endtime), ...] - self.timings = timings - - # requests within the same session can reuse TCP connections from pool - self.http = http or requests.Session() - - self.cached_token = None - - def _http_log_req(self, url, method, kwargs): - if not self.debug: - return - - string_parts = [ - "curl -i", - "-X '%s'" % method, - "'%s'" % url, - ] - - for element in kwargs['headers']: - header = "-H '%s: %s'" % (element, kwargs['headers'][element]) - string_parts.append(header) - - _logger.debug("REQ: %s" % " ".join(string_parts)) - if 'data' in kwargs: - _logger.debug("REQ BODY: %s\n" % (kwargs['data'])) - - def _http_log_resp(self, resp): - if not self.debug: - return - _logger.debug( - "RESP: [%s] %s\n", - resp.status_code, - resp.headers) - if resp._content_consumed: - _logger.debug( - "RESP BODY: %s\n", - resp.text) - - def serialize(self, kwargs): - if kwargs.get('json') is not None: - kwargs['headers']['Content-Type'] = 'application/json' - kwargs['data'] = json.dumps(kwargs.pop('json')) - - def get_timings(self): - return self.times - - def reset_timings(self): - self.times = [] - - def request(self, url, method, **kwargs): - """Send an http request with the specified characteristics. - - Wrapper around `requests.Session.request` to handle tasks such as - setting headers, JSON encoding/decoding, and error handling. - - :param method: method of HTTP request - :param url: URL of HTTP request - :param kwargs: any other parameter that can be passed to - requests.Session.request (such as `headers`) or `json` - that will be encoded as JSON and used as `data` argument - """ - kwargs.setdefault("headers", kwargs.get("headers", {})) - kwargs["headers"]["User-Agent"] = self.user_agent - if self.original_ip: - kwargs["headers"]["Forwarded"] = "for=%s;by=%s" % ( - self.original_ip, self.user_agent) - if self.timeout is not None: - kwargs.setdefault("timeout", self.timeout) - kwargs.setdefault("verify", self.verify) - if self.cert is not None: - kwargs.setdefault("cert", self.cert) - self.serialize(kwargs) - - self._http_log_req(url, method, kwargs) - if self.timings: - start_time = time.time() - resp = self.http.request(url, method, **kwargs) - if self.timings: - self.times.append(("%s %s" % (url, method), - start_time, time.time())) - self._http_log_resp(resp) - - if resp.status_code >= 400: - _logger.debug( - "Request returned failure status: %s", - resp.status_code) - raise exceptions.from_response(resp, url, method) - - return resp - - @staticmethod - def concat_url(endpoint, url): - """Concatenate endpoint and final URL. - - E.g., "http://keystone/v2.0/" and "/tokens" are concatenated to - "http://keystone/v2.0/tokens". - - :param endpoint: the base URL - :param url: the final URL - """ - return "%s/%s" % (endpoint.rstrip("/"), url.strip("/")) - - def client_request(self, client, url, method, **kwargs): - """Send an http request using `client`'s endpoint and specified `url`. - - If request was rejected as unauthorized (possibly because the token is - expired), issue one authorization attempt and send the request once - again. - - :param client: instance of BaseClient descendant - :param method: method of HTTP request - :param url: URL of HTTP request - :param kwargs: any other parameter that can be passed to - `HTTPClient.request` - """ - - filter_args = { - "endpoint_type": client.endpoint_type or self.endpoint_type, - "service_type": client.service_type, - } - token, endpoint = (self.cached_token, client.cached_endpoint) - just_authenticated = False - if not (token and endpoint): - try: - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - except exceptions.EndpointException: - pass - if not (token and endpoint): - self.authenticate() - just_authenticated = True - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - if not (token and endpoint): - raise exceptions.AuthorizationFailure( - _("Cannot find endpoint or token for request")) - - old_token_endpoint = (token, endpoint) - kwargs.setdefault("headers", {})["X-Auth-Token"] = token - self.cached_token = token - client.cached_endpoint = endpoint - # Perform the request once. If we get Unauthorized, then it - # might be because the auth token expired, so try to - # re-authenticate and try again. If it still fails, bail. - try: - return self.request( - method, self.concat_url(endpoint, url), **kwargs) - except exceptions.Unauthorized as unauth_ex: - if just_authenticated: - raise - self.cached_token = None - client.cached_endpoint = None - self.authenticate() - try: - token, endpoint = self.auth_plugin.token_and_endpoint( - **filter_args) - except exceptions.EndpointException: - raise unauth_ex - if (not (token and endpoint) or - old_token_endpoint == (token, endpoint)): - raise unauth_ex - self.cached_token = token - client.cached_endpoint = endpoint - kwargs["headers"]["X-Auth-Token"] = token - return self.request( - method, self.concat_url(endpoint, url), **kwargs) - - def add_client(self, base_client_instance): - """Add a new instance of :class:`BaseClient` descendant. - - `self` will store a reference to `base_client_instance`. - - Example: - - >>> def test_clients(): - ... from keystoneclient.auth import keystone - ... from openstack.common.apiclient import client - ... auth = keystone.KeystoneAuthPlugin( - ... username="user", password="pass", tenant_name="tenant", - ... auth_url="http://auth:5000/v2.0") - ... openstack_client = client.HTTPClient(auth) - ... # create nova client - ... from novaclient.v1_1 import client - ... client.Client(openstack_client) - ... # create keystone client - ... from keystoneclient.v2_0 import client - ... client.Client(openstack_client) - ... # use them - ... openstack_client.identity.tenants.list() - ... openstack_client.compute.servers.list() - """ - service_type = base_client_instance.service_type - if service_type and not hasattr(self, service_type): - setattr(self, service_type, base_client_instance) - - def authenticate(self): - self.auth_plugin.authenticate(self) - # Store the authentication results in the keyring for later requests - if self.keyring_saver: - self.keyring_saver.save(self) - - -class BaseClient(object): - """Top-level object to access the OpenStack API. - - This client uses :class:`HTTPClient` to send requests. :class:`HTTPClient` - will handle a bunch of issues such as authentication. - """ - - service_type = None - endpoint_type = None # "publicURL" will be used - cached_endpoint = None - - def __init__(self, http_client, extensions=None): - self.http_client = http_client - http_client.add_client(self) - - # Add in any extensions... - if extensions: - for extension in extensions: - if extension.manager_class: - setattr(self, extension.name, - extension.manager_class(self)) - - def client_request(self, url, method, **kwargs): - return self.http_client.client_request( - self, url, method, **kwargs) - - def head(self, url, **kwargs): - return self.client_request(url, "HEAD", **kwargs) - - def get(self, url, **kwargs): - return self.client_request(url, "GET", **kwargs) - - def post(self, url, **kwargs): - return self.client_request(url, "POST", **kwargs) - - def put(self, url, **kwargs): - return self.client_request(url, "PUT", **kwargs) - - def delete(self, url, **kwargs): - return self.client_request(url, "DELETE", **kwargs) - - def patch(self, url, **kwargs): - return self.client_request(url, "PATCH", **kwargs) - - @staticmethod - def get_class(api_name, version, version_map): - """Returns the client class for the requested API version - - :param api_name: the name of the API, e.g. 'compute', 'image', etc - :param version: the requested API version - :param version_map: a dict of client classes keyed by version - :rtype: a client class for the requested API version - """ - try: - client_path = version_map[str(version)] - except (KeyError, ValueError): - msg = _("Invalid %(api_name)s client version '%(version)s'. " - "Must be one of: %(version_map)s") % { - 'api_name': api_name, - 'version': version, - 'version_map': ', '.join(version_map.keys())} - raise exceptions.UnsupportedVersion(msg) - - return importutils.import_class(client_path) diff --git a/muranoclient/apiclient/exceptions.py b/muranoclient/apiclient/exceptions.py deleted file mode 100644 index 3787e138..00000000 --- a/muranoclient/apiclient/exceptions.py +++ /dev/null @@ -1,461 +0,0 @@ -# Copyright 2011 Nebula, Inc. -# Copyright 2013 Alessio Ababilov -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Exception definitions. -""" - -import inspect -import sys - -from muranoclient.i18n import _ - - -class ClientException(Exception): - """The base exception class for all exceptions this library raises.""" - pass - - -class MissingArgs(ClientException): - """Supplied arguments are not sufficient for calling a function.""" - def __init__(self, missing): - self.missing = missing - msg = _("Missing arguments: %s") % ", ".join(missing) - super(MissingArgs, self).__init__(msg) - - -class ValidationError(ClientException): - """Error in validation on API client side.""" - pass - - -class UnsupportedVersion(ClientException): - """User is trying to use an unsupported version of the API.""" - pass - - -class CommandError(ClientException): - """Error in CLI tool.""" - pass - - -class AuthorizationFailure(ClientException): - """Cannot authorize API client.""" - pass - - -class ConnectionRefused(ClientException): - """Cannot connect to API service.""" - pass - - -class AuthPluginOptionsMissing(AuthorizationFailure): - """Auth plugin misses some options.""" - def __init__(self, opt_names): - super(AuthPluginOptionsMissing, self).__init__( - _("Authentication failed. Missing options: %s") % - ", ".join(opt_names)) - self.opt_names = opt_names - - -class AuthSystemNotFound(AuthorizationFailure): - """User has specified an AuthSystem that is not installed.""" - def __init__(self, auth_system): - super(AuthSystemNotFound, self).__init__( - _("AuthSystemNotFound: %s") % repr(auth_system)) - self.auth_system = auth_system - - -class NoUniqueMatch(ClientException): - """Multiple entities found instead of one.""" - pass - - -class EndpointException(ClientException): - """Something is rotten in Service Catalog.""" - pass - - -class EndpointNotFound(EndpointException): - """Could not find requested endpoint in Service Catalog.""" - pass - - -class AmbiguousEndpoints(EndpointException): - """Found more than one matching endpoint in Service Catalog.""" - def __init__(self, endpoints=None): - super(AmbiguousEndpoints, self).__init__( - _("AmbiguousEndpoints: %s") % repr(endpoints)) - self.endpoints = endpoints - - -class HttpError(ClientException): - """The base exception class for all HTTP exceptions.""" - http_status = 0 - message = _("HTTP Error") - - def __init__(self, message=None, details=None, - response=None, request_id=None, - url=None, method=None, http_status=None): - self.http_status = http_status or self.http_status - self.message = message or self.message - self.details = details - self.request_id = request_id - self.response = response - self.url = url - self.method = method - formatted_string = "%s (HTTP %s)" % (self.message, self.http_status) - if request_id: - formatted_string += " (Request-ID: %s)" % request_id - super(HttpError, self).__init__(formatted_string) - - -class HTTPRedirection(HttpError): - """HTTP Redirection.""" - message = _("HTTP Redirection") - - -class HTTPClientError(HttpError): - """Client-side HTTP error. - - Exception for cases in which the client seems to have erred. - """ - message = _("HTTP Client Error") - - -class HttpServerError(HttpError): - """Server-side HTTP error. - - Exception for cases in which the server is aware that it has - erred or is incapable of performing the request. - """ - message = _("HTTP Server Error") - - -class MultipleChoices(HTTPRedirection): - """HTTP 300 - Multiple Choices. - - Indicates multiple options for the resource that the client may follow. - """ - - http_status = 300 - message = _("Multiple Choices") - - -class BadRequest(HTTPClientError): - """HTTP 400 - Bad Request. - - The request cannot be fulfilled due to bad syntax. - """ - http_status = 400 - message = _("Bad Request") - - -class Unauthorized(HTTPClientError): - """HTTP 401 - Unauthorized. - - Similar to 403 Forbidden, but specifically for use when authentication - is required and has failed or has not yet been provided. - """ - http_status = 401 - message = _("Unauthorized") - - -class PaymentRequired(HTTPClientError): - """HTTP 402 - Payment Required. - - Reserved for future use. - """ - http_status = 402 - message = _("Payment Required") - - -class Forbidden(HTTPClientError): - """HTTP 403 - Forbidden. - - The request was a valid request, but the server is refusing to respond - to it. - """ - http_status = 403 - message = _("Forbidden") - - -class NotFound(HTTPClientError): - """HTTP 404 - Not Found. - - The requested resource could not be found but may be available again - in the future. - """ - http_status = 404 - message = _("Not Found") - - -class MethodNotAllowed(HTTPClientError): - """HTTP 405 - Method Not Allowed. - - A request was made of a resource using a request method not supported - by that resource. - """ - http_status = 405 - message = _("Method Not Allowed") - - -class NotAcceptable(HTTPClientError): - """HTTP 406 - Not Acceptable. - - The requested resource is only capable of generating content not - acceptable according to the Accept headers sent in the request. - """ - http_status = 406 - message = _("Not Acceptable") - - -class ProxyAuthenticationRequired(HTTPClientError): - """HTTP 407 - Proxy Authentication Required. - - The client must first authenticate itself with the proxy. - """ - http_status = 407 - message = _("Proxy Authentication Required") - - -class RequestTimeout(HTTPClientError): - """HTTP 408 - Request Timeout. - - The server timed out waiting for the request. - """ - http_status = 408 - message = _("Request Timeout") - - -class Conflict(HTTPClientError): - """HTTP 409 - Conflict. - - Indicates that the request could not be processed because of conflict - in the request, such as an edit conflict. - """ - http_status = 409 - message = _("Conflict") - - -class Gone(HTTPClientError): - """HTTP 410 - Gone. - - Indicates that the resource requested is no longer available and will - not be available again. - """ - http_status = 410 - message = _("Gone") - - -class LengthRequired(HTTPClientError): - """HTTP 411 - Length Required. - - The request did not specify the length of its content, which is - required by the requested resource. - """ - http_status = 411 - message = _("Length Required") - - -class PreconditionFailed(HTTPClientError): - """HTTP 412 - Precondition Failed. - - The server does not meet one of the preconditions that the requester - put on the request. - """ - http_status = 412 - message = _("Precondition Failed") - - -class RequestEntityTooLarge(HTTPClientError): - """HTTP 413 - Request Entity Too Large. - - The request is larger than the server is willing or able to process. - """ - http_status = 413 - message = _("Request Entity Too Large") - - def __init__(self, *args, **kwargs): - try: - self.retry_after = int(kwargs.pop('retry_after')) - except (KeyError, ValueError): - self.retry_after = 0 - - super(RequestEntityTooLarge, self).__init__(*args, **kwargs) - - -class RequestUriTooLong(HTTPClientError): - """HTTP 414 - Request-URI Too Long. - - The URI provided was too long for the server to process. - """ - http_status = 414 - message = _("Request-URI Too Long") - - -class UnsupportedMediaType(HTTPClientError): - """HTTP 415 - Unsupported Media Type. - - The request entity has a media type which the server or resource does - not support. - """ - http_status = 415 - message = _("Unsupported Media Type") - - -class RequestedRangeNotSatisfiable(HTTPClientError): - """HTTP 416 - Requested Range Not Satisfiable. - - The client has asked for a portion of the file, but the server cannot - supply that portion. - """ - http_status = 416 - message = _("Requested Range Not Satisfiable") - - -class ExpectationFailed(HTTPClientError): - """HTTP 417 - Expectation Failed. - - The server cannot meet the requirements of the Expect request-header field. - """ - http_status = 417 - message = _("Expectation Failed") - - -class UnprocessableEntity(HTTPClientError): - """HTTP 422 - Unprocessable Entity. - - The request was well-formed but was unable to be followed due to semantic - errors. - """ - http_status = 422 - message = _("Unprocessable Entity") - - -class InternalServerError(HttpServerError): - """HTTP 500 - Internal Server Error. - - A generic error message, given when no more specific message is suitable. - """ - http_status = 500 - message = _("Internal Server Error") - - -# NotImplemented is a python keyword. -class HttpNotImplemented(HttpServerError): - """HTTP 501 - Not Implemented. - - The server either does not recognize the request method, or it lacks - the ability to fulfill the request. - """ - http_status = 501 - message = _("Not Implemented") - - -class BadGateway(HttpServerError): - """HTTP 502 - Bad Gateway. - - The server was acting as a gateway or proxy and received an invalid - response from the upstream server. - """ - http_status = 502 - message = _("Bad Gateway") - - -class ServiceUnavailable(HttpServerError): - """HTTP 503 - Service Unavailable. - - The server is currently unavailable. - """ - http_status = 503 - message = _("Service Unavailable") - - -class GatewayTimeout(HttpServerError): - """HTTP 504 - Gateway Timeout. - - The server was acting as a gateway or proxy and did not receive a timely - response from the upstream server. - """ - http_status = 504 - message = _("Gateway Timeout") - - -class HttpVersionNotSupported(HttpServerError): - """HTTP 505 - HttpVersion Not Supported. - - The server does not support the HTTP protocol version used in the request. - """ - http_status = 505 - message = _("HTTP Version Not Supported") - - -# _code_map contains all the classes that have http_status attribute. -_code_map = dict( - (getattr(obj, 'http_status', None), obj) - for name, obj in vars(sys.modules[__name__]).items() - if inspect.isclass(obj) and getattr(obj, 'http_status', False) -) - - -def from_response(response, url, method): - """Returns an instance of :class:`HttpError` or subclass based on response. - - :param response: instance of `requests.Response` class - :param method: HTTP method used for request - :param url: URL used for request - """ - - req_id = response.headers.get("x-openstack-request-id") - # NOTE(hdd): true for older versions of nova and cinder - if not req_id: - req_id = response.headers.get("x-compute-request-id") - kwargs = { - "http_status": response.status_code, - "response": response, - "method": method, - "url": url, - "request_id": req_id, - } - if "retry-after" in response.headers: - kwargs["retry_after"] = response.headers["retry-after"] - - content_type = response.headers.get("Content-Type", "") - if content_type.startswith("application/json"): - try: - body = response.json() - except ValueError: - pass - else: - if isinstance(body, dict): - error = list(body.values())[0] - kwargs["message"] = error.get("message") - kwargs["details"] = error.get("details") - elif content_type.startswith("text/"): - kwargs["details"] = response.text - - try: - cls = _code_map[response.status_code] - except KeyError: - if 500 <= response.status_code < 600: - cls = HttpServerError - elif 400 <= response.status_code < 500: - cls = HTTPClientError - else: - cls = HttpError - return cls(**kwargs) diff --git a/muranoclient/apiclient/fake_client.py b/muranoclient/apiclient/fake_client.py deleted file mode 100644 index e4f3de5c..00000000 --- a/muranoclient/apiclient/fake_client.py +++ /dev/null @@ -1,176 +0,0 @@ -# Copyright 2013 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -A fake server that "responds" to API methods with pre-canned responses. - -All of these responses come from the spec, so if for some reason the spec's -wrong the tests might raise AssertionError. I've indicated in comments the -places where actual behavior differs from the spec. -""" - -# W0102: Dangerous default value %s as argument -# pylint: disable=W0102 - -import json - -import parse -import requests - -from muranoclient.apiclient import client - - -def assert_has_keys(dct, required=None, optional=None): - if required is None: - required = [] - if optional is None: - optional = [] - for k in required: - try: - assert k in dct - except AssertionError: - extra_keys = set(dct.keys()).difference(set(required + optional)) - raise AssertionError("found unexpected keys: %s" % - list(extra_keys)) - - -class TestResponse(requests.Response): - """Wrap requests.Response and provide a convenient initialization.""" - - def __init__(self, data): - super(TestResponse, self).__init__() - self._content_consumed = True - if isinstance(data, dict): - self.status_code = data.get('status_code', 200) - # Fake the text attribute to streamline Response creation - text = data.get('text', "") - if isinstance(text, (dict, list)): - self._content = json.dumps(text) - default_headers = { - "Content-Type": "application/json", - } - else: - self._content = text - default_headers = {} - if isinstance(self._content, str): - self._content = self._content.encode('utf-8', 'strict') - self.headers = data.get('headers') or default_headers - else: - self.status_code = data - - def __eq__(self, other): - return (self.status_code == other.status_code and - self.headers == other.headers and - self._content == other._content) - - def __ne__(self, other): - return not self.__eq__(other) - - -class FakeHTTPClient(client.HTTPClient): - - def __init__(self, *args, **kwargs): - self.callstack = [] - self.fixtures = kwargs.pop("fixtures", None) or {} - if not args and "auth_plugin" not in kwargs: - args = (None, ) - super(FakeHTTPClient, self).__init__(*args, **kwargs) - - def assert_called(self, url, method, body=None, pos=-1): - """Assert than an API method was just called.""" - expected = (url, method) - called = self.callstack[pos][0:2] - assert self.callstack, \ - "Expected %s %s but no calls were made." % expected - - assert expected == called, 'Expected %s %s; got %s %s' % \ - (expected + called) - - if body is not None: - if self.callstack[pos][3] != body: - raise AssertionError('%r != %r' % - (self.callstack[pos][3], body)) - - def assert_called_anytime(self, url, method, body=None): - """Assert than an API method was called anytime in the test.""" - expected = (url, method) - - assert self.callstack, \ - "Expected %s %s but no calls were made." % expected - - found = False - entry = None - for entry in self.callstack: - if expected == entry[0:2]: - found = True - break - - assert found, 'Expected %s %s; got %s' % \ - (url, method, self.callstack) - if body is not None: - assert entry[3] == body, "%s != %s" % (entry[3], body) - - self.callstack = [] - - def clear_callstack(self): - self.callstack = [] - - def authenticate(self): - pass - - def client_request(self, client, url, method, **kwargs): - # Check that certain things are called correctly - if method in ["GET", "DELETE"]: - assert "json" not in kwargs - - # Note the call - self.callstack.append( - (url, - method, - kwargs.get("headers") or {}, - kwargs.get("json") or kwargs.get("data"))) - try: - fixture = self.fixtures[url][method] - except KeyError: - pass - else: - return TestResponse({"headers": fixture[0], - "text": fixture[1]}) - - # Call the method - args = parse.parse_qsl(parse.urlparse(url)[4]) - kwargs.update(args) - munged_url = url.rsplit('?', 1)[0] - munged_url = munged_url.strip('/').replace('/', '_').replace('.', '_') - munged_url = munged_url.replace('-', '_') - - callback = "%s_%s" % (method.lower(), munged_url) - - if not hasattr(self, callback): - raise AssertionError('Called unknown API method: %s %s, ' - 'expected fakes method name: %s' % - (url, method, callback)) - - resp = getattr(self, callback)(**kwargs) - if len(resp) == 3: - status, headers, body = resp - else: - status, body = resp - headers = {} - return TestResponse({ - "status_code": status, - "text": body, - "headers": headers, - }) diff --git a/muranoclient/client.py b/muranoclient/client.py deleted file mode 100644 index 485ed898..00000000 --- a/muranoclient/client.py +++ /dev/null @@ -1,21 +0,0 @@ -# Copyright (c) 2013 Mirantis, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -from oslo_utils import importutils - - -def Client(version, *args, **kwargs): - module = importutils.import_versioned_module('muranoclient', - version, 'client') - client_class = getattr(module, 'Client') - return client_class(*args, **kwargs) diff --git a/muranoclient/common/__init__.py b/muranoclient/common/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/muranoclient/common/base.py b/muranoclient/common/base.py deleted file mode 100644 index f4eb521d..00000000 --- a/muranoclient/common/base.py +++ /dev/null @@ -1,229 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Base utilities to build API operation managers and objects on top of. -""" - -import abc -import copy - -from muranoclient.apiclient import exceptions - - -def getid(obj): - """Get obj's id or object itself if no id - - Abstracts the common pattern of allowing both an object or - an object's ID (UUID) as a parameter when dealing with relationships. - """ - try: - return obj.id - except AttributeError: - return obj - - -class Manager(object): - """Interacts with type of API - - Managers interact with a particular type of API (servers, flavors, - images, etc.) and provide CRUD operations for them. - """ - resource_class = None - - def __init__(self, api): - self.api = api - - def _list(self, url, response_key=None, obj_class=None, - data=None, headers=None): - - if headers is None: - headers = {} - resp, body = self.api.json_request(url, 'GET', headers=headers) - - if obj_class is None: - obj_class = self.resource_class - - if response_key: - if response_key not in body: - body[response_key] = [] - data = body[response_key] - else: - data = body - return [obj_class(self, res, loaded=True) for res in data if res] - - def _delete(self, url, headers=None): - if headers is None: - headers = {} - self.api.request(url, 'DELETE', headers=headers) - - def _update(self, url, data, response_key=None, return_raw=False, - headers=None, method='PUT', content_type='application/json'): - if headers is None: - headers = {} - resp, body = self.api.json_request(url, method, - content_type=content_type, - data=data, headers=headers) - # PUT or PATCH requests may not return a body - if body: - if return_raw: - if response_key: - return body[response_key] - return body - if response_key: - return self.resource_class(self, body[response_key]) - return self.resource_class(self, body) - - def _create(self, url, data=None, response_key=None, - return_raw=False, headers=None): - if headers is None: - headers = {} - if data: - resp, body = self.api.json_request(url, 'POST', - data=data, headers=headers) - else: - resp, body = self.api.json_request(url, 'POST', headers=headers) - if return_raw: - if response_key: - return body[response_key] - return body - if response_key: - return self.resource_class(self, body[response_key]) - return self.resource_class(self, body) - - def _get(self, url, response_key=None, return_raw=False, headers=None): - if headers is None: - headers = {} - resp, body = self.api.json_request(url, 'GET', headers=headers) - if return_raw: - if response_key: - return body[response_key] - return body - if response_key: - return self.resource_class(self, body[response_key]) - return self.resource_class(self, body) - - -class ManagerWithFind(Manager, metaclass=abc.ABCMeta): - """Manager with additional `find()`/`findall()` methods.""" - - @abc.abstractmethod - def list(self): - pass - - def find(self, **kwargs): - """Find a single item with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - rl = self.findall(**kwargs) - num = len(rl) - - if num == 0: - msg = "No %s matching %s." % (self.resource_class.__name__, kwargs) - raise exceptions.NotFound(msg) - elif num > 1: - raise exceptions.NoUniqueMatch - else: - return self.get(rl[0].id) - - def findall(self, **kwargs): - """Find all items with attributes matching ``**kwargs``. - - This isn't very efficient: it loads the entire list then filters on - the Python side. - """ - found = [] - searches = kwargs.items() - - for obj in self.list(): - try: - if all(getattr(obj, attr) == value - for (attr, value) in searches): - found.append(obj) - except AttributeError: - continue - - return found - - -class Resource(object): - """Represents an instance of an object - - A resource represents a particular instance of an object (tenant, user, - etc). This is pretty much just a bag for attributes. - - :param manager: Manager object - :param info: dictionary representing resource attributes - :param loaded: prevent lazy-loading if set to True - """ - - def __init__(self, manager, info, loaded=False): - self.manager = manager - self._info = info - self._add_details(info) - self._loaded = loaded - - def _add_details(self, info): - for k, v in info.items(): - setattr(self, k, v) - - def __setstate__(self, d): - for k, v in d.items(): - setattr(self, k, v) - - def __getattr__(self, k): - if k not in self.__dict__: - # NOTE(bcwaldon): disallow lazy-loading if already loaded once - if not self.is_loaded(): - self.get() - return self.__getattr__(k) - raise AttributeError(k) - else: - return self.__dict__[k] - - def __repr__(self): - reprkeys = sorted(k for k in self.__dict__.keys() if k[0] != '_' and - k != 'manager') - info = ", ".join("%s=%s" % (k, getattr(self, k)) for k in reprkeys) - return "<%s %s>" % (self.__class__.__name__, info) - - def get(self): - # set_loaded() first ... so if we have to bail, we know we tried. - self.set_loaded(True) - if not hasattr(self.manager, 'get'): - return - - new = self.manager.get(self.id) - if new: - self._add_details(new._info) - - def __eq__(self, other): - if not isinstance(other, self.__class__): - return False - return self._info == other._info - - def __ne__(self, other): - return not self.__eq__(other) - - def is_loaded(self): - return self._loaded - - def set_loaded(self, val): - self._loaded = val - - def to_dict(self): - return copy.deepcopy(self._info) diff --git a/muranoclient/common/exceptions.py b/muranoclient/common/exceptions.py deleted file mode 100644 index 0f94dd25..00000000 --- a/muranoclient/common/exceptions.py +++ /dev/null @@ -1,207 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import re -import sys - - -# TODO(sjmc7): This module is likely redundant because it's replaced -# by openstack.common.apiclient; should be removed -class BaseException(Exception): - """An error occurred.""" - def __init__(self, message=None): - self.message = message - - def __str__(self): - return self.message or self.__class__.__doc__ - - -class InvalidEndpoint(BaseException): - """The provided endpoint is invalid.""" - - -class CommunicationError(BaseException): - """Unable to communicate with server.""" - - -class ClientException(Exception): - """DEPRECATED!""" - - -class HTTPException(ClientException): - """Base exception for all HTTP-derived exceptions.""" - code = 'N/A' - - def __init__(self, details=None): - self.details = details or self.__class__.__name__ - - def __str__(self): - return "%s (HTTP %s)" % (self.details, self.code) - - -class HTTPMultipleChoices(HTTPException): - code = 300 - - def __str__(self): - self.details = ("Requested version of Application Catalog API is not " - "available.") - return "%s (HTTP %s) %s" % (self.__class__.__name__, self.code, - self.details) - - -class BadRequest(HTTPException): - """DEPRECATED!""" - code = 400 - - -class HTTPBadRequest(BadRequest): - pass - - -class Unauthorized(HTTPException): - """DEPRECATED!""" - code = 401 - - -class HTTPUnauthorized(Unauthorized): - pass - - -class Forbidden(HTTPException): - """DEPRECATED!""" - code = 403 - - -class HTTPForbidden(Forbidden): - pass - - -class NotFound(HTTPException): - """DEPRECATED!""" - code = 404 - - -class HTTPNotFound(NotFound): - pass - - -class HTTPMethodNotAllowed(HTTPException): - code = 405 - - -class Conflict(HTTPException): - """DEPRECATED!""" - code = 409 - - -class HTTPConflict(Conflict): - pass - - -class OverLimit(HTTPException): - """DEPRECATED!""" - code = 413 - - -class HTTPOverLimit(OverLimit): - pass - - -class HTTPInternalServerError(HTTPException): - code = 500 - - -class HTTPNotImplemented(HTTPException): - code = 501 - - -class HTTPBadGateway(HTTPException): - code = 502 - - -class ServiceUnavailable(HTTPException): - """DEPRECATED!""" - code = 503 - - -class HTTPServiceUnavailable(ServiceUnavailable): - pass - - -# NOTE(bcwaldon): Build a mapping of HTTP codes to corresponding exception -# classes -_code_map = {} -for obj_name in dir(sys.modules[__name__]): - if obj_name.startswith('HTTP'): - obj = getattr(sys.modules[__name__], obj_name) - _code_map[obj.code] = obj - - -def from_response(response): - """Return an instance of an HTTPException based on httplib response.""" - cls = _code_map.get(response.status_code, HTTPException) - body = response.content - if body and response.headers['content-type'].\ - lower().startswith("application/json"): - # Iterate over the nested objects and retrieve the "message" attribute. - messages = [obj.get('message') for obj in response.json().values()] - # Join all of the messages together nicely and filter out any objects - # that don't have a "message" attr. - details = '\n'.join(i for i in messages if i is not None) - return cls(details=details) - elif body and \ - response.headers['content-type'].lower().startswith("text/html"): - # Split the lines, strip whitespace and inline HTML from the response. - details = [re.sub(r'<.+?>', '', i.strip()) - for i in response.text.splitlines()] - details = [i for i in details if i] - # Remove duplicates from the list. - details_seen = set() - details_temp = [] - for i in details: - if i not in details_seen: - details_temp.append(i) - details_seen.add(i) - # Return joined string separated by colons. - details = ': '.join(details_temp) - return cls(details=details) - elif body: - details = body.replace('\n\n', '\n') - return cls(details=details) - - return cls() - - -def from_code(code): - cls = _code_map.get(code, HTTPException) - return cls() - - -class NoTokenLookupException(Exception): - """DEPRECATED!""" - pass - - -class EndpointNotFound(Exception): - """DEPRECATED!""" - pass - - -class SSLConfigurationError(BaseException): - pass - - -class SSLCertificateError(BaseException): - pass diff --git a/muranoclient/common/http.py b/muranoclient/common/http.py deleted file mode 100644 index 4cf14955..00000000 --- a/muranoclient/common/http.py +++ /dev/null @@ -1,373 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import copy -import hashlib -import os -import socket - -import keystoneclient.adapter as keystone_adapter -from oslo_log import log as logging -from oslo_serialization import jsonutils -from oslo_utils import encodeutils -import requests -import urllib - -from muranoclient.common import exceptions as exc - -LOG = logging.getLogger(__name__) -USER_AGENT = 'python-muranoclient' -CHUNKSIZE = 1024 * 64 # 64kB - - -def get_system_ca_file(): - """Return path to system default CA file.""" - # Standard CA file locations for Debian/Ubuntu, RedHat/Fedora, - # Suse, FreeBSD/OpenBSD, MacOSX, and the bundled ca - ca_path = ['/etc/ssl/certs/ca-certificates.crt', - '/etc/pki/tls/certs/ca-bundle.crt', - '/etc/ssl/ca-bundle.pem', - '/etc/ssl/cert.pem', - '/System/Library/OpenSSL/certs/cacert.pem', - requests.certs.where()] - for ca in ca_path: - LOG.debug("Looking for ca file %s", ca) - if os.path.exists(ca): - LOG.debug("Using ca file %s", ca) - return ca - LOG.warning("System ca file could not be found.") - - -class HTTPClient(object): - - def __init__(self, endpoint, **kwargs): - self.endpoint = endpoint - self.auth_url = kwargs.get('auth_url') - self.auth_token = kwargs.get('token') - self.username = kwargs.get('username') - self.password = kwargs.get('password') - self.region_name = kwargs.get('region_name') - self.include_pass = kwargs.get('include_pass') - self.endpoint_url = endpoint - - self.cert_file = kwargs.get('cert_file') - self.key_file = kwargs.get('key_file') - self.timeout = kwargs.get('timeout') - - self.ssl_connection_params = { - 'cacert': kwargs.get('cacert'), - 'cert_file': kwargs.get('cert_file'), - 'key_file': kwargs.get('key_file'), - 'insecure': kwargs.get('insecure'), - } - - self.verify_cert = None - if urllib.parse.urlparse(endpoint).scheme == "https": - if kwargs.get('insecure'): - self.verify_cert = False - else: - self.verify_cert = kwargs.get('cacert', get_system_ca_file()) - - def _safe_header(self, name, value): - if name in ['X-Auth-Token', 'X-Subject-Token']: - # because in python3 byte string handling is ... ug - v = value.encode('utf-8') - h = hashlib.sha1(v) - d = h.hexdigest() - return encodeutils.safe_decode(name), "{SHA1}%s" % d - else: - return (encodeutils.safe_decode(name), - encodeutils.safe_decode(value)) - - def log_curl_request(self, url, method, kwargs): - curl = ['curl -i -X %s' % method] - - for (key, value) in kwargs['headers'].items(): - header = '-H \'%s: %s\'' % self._safe_header(key, value) - curl.append(header) - - conn_params_fmt = [ - ('key_file', '--key %s'), - ('cert_file', '--cert %s'), - ('cacert', '--cacert %s'), - ] - for (key, fmt) in conn_params_fmt: - value = self.ssl_connection_params.get(key) - if value: - curl.append(fmt % value) - - if self.ssl_connection_params.get('insecure'): - curl.append('-k') - - if 'data' in kwargs: - curl.append('-d \'%s\'' % kwargs['data']) - - curl.append('%s%s' % (self.endpoint, url)) - LOG.debug(' '.join(curl)) - - @staticmethod - def log_http_response(resp): - status = (resp.raw.version / 10.0, resp.status_code, resp.reason) - dump = ['\nHTTP/%.1f %s %s' % status] - dump.extend(['%s: %s' % (k, v) for k, v in resp.headers.items()]) - dump.append('') - if resp.content: - content = resp.content - if isinstance(content, bytes): - try: - content = encodeutils.safe_decode(resp.content) - except UnicodeDecodeError: - pass - else: - dump.extend([content, '']) - LOG.debug('\n'.join(dump)) - - def request(self, url, method, log=True, **kwargs): - """Send an http request with the specified characteristics. - - Wrapper around requests.request to handle tasks such - as setting headers and error handling. - """ - _set_data(kwargs) - - # Copy the kwargs so we can reuse the original in case of redirects - kwargs['headers'] = copy.deepcopy(kwargs.get('headers', {})) - kwargs['headers'].setdefault('User-Agent', USER_AGENT) - if self.auth_token: - kwargs['headers'].setdefault('X-Auth-Token', self.auth_token) - else: - kwargs['headers'].update(self.credentials_headers()) - if self.auth_url: - kwargs['headers'].setdefault('X-Auth-Url', self.auth_url) - if self.region_name: - kwargs['headers'].setdefault('X-Region-Name', self.region_name) - - self.log_curl_request(url, method, kwargs) - - if self.cert_file and self.key_file: - kwargs['cert'] = (self.cert_file, self.key_file) - - if self.verify_cert is not None: - kwargs['verify'] = self.verify_cert - - if self.timeout is not None: - kwargs['timeout'] = float(self.timeout) - - # Allow the option not to follow redirects - follow_redirects = kwargs.pop('follow_redirects', True) - - # Since requests does not follow the RFC when doing redirection to sent - # back the same method on a redirect we are simply bypassing it. For - # example if we do a DELETE/POST/PUT on a URL and we get a 302 RFC says - # that we should follow that URL with the same method as before, - # requests doesn't follow that and send a GET instead for the method. - # Hopefully this could be fixed as they say in a comment in a future - # point version i.e.: 3.x - # See issue: https://github.com/kennethreitz/requests/issues/1704 - allow_redirects = False - - try: - resp = requests.request( - method, - self.endpoint_url + url, - allow_redirects=allow_redirects, - **kwargs) - except socket.gaierror as e: - message = ("Error finding address for %(url)s: %(e)s" % - {'url': self.endpoint_url + url, 'e': e}) - raise exc.InvalidEndpoint(message=message) - except (socket.error, - socket.timeout, - requests.exceptions.ConnectionError) as e: - endpoint = self.endpoint - message = ("Error communicating with %(endpoint)s %(e)s" % - {'endpoint': endpoint, 'e': e}) - raise exc.CommunicationError(message=message) - - if log: - self.log_http_response(resp) - - if 'X-Auth-Key' not in kwargs['headers'] and \ - (resp.status_code == 401 or - (resp.status_code == 500 and - "(HTTP 401)" in resp.content)): - raise exc.HTTPUnauthorized("Authentication failed. Please try" - " again.\n%s" - % resp.content) - elif 400 <= resp.status_code < 600: - raise exc.from_response(resp) - elif resp.status_code in (301, 302, 305): - # Redirected. Reissue the request to the new location, - # unless caller specified follow_redirects=False - if follow_redirects: - location = resp.headers.get('location') - path = self.strip_endpoint(location) - resp = self.request(path, method, **kwargs) - elif resp.status_code == 300: - raise exc.from_response(resp) - - return resp - - def strip_endpoint(self, location): - if location is None: - message = "Location not returned with 302" - raise exc.InvalidEndpoint(message=message) - elif location.startswith(self.endpoint): - return location[len(self.endpoint):] - else: - message = "Prohibited endpoint redirect %s" % location - raise exc.InvalidEndpoint(message=message) - - def credentials_headers(self): - creds = {} - if self.username: - creds['X-Auth-User'] = self.username - if self.password: - creds['X-Auth-Key'] = self.password - return creds - - def json_request(self, url, method, content_type='application/json', - **kwargs): - - kwargs.setdefault('headers', {}) - kwargs['headers'].setdefault('Content-Type', content_type) - # Don't set Accept because we aren't always dealing in JSON - - _set_data(kwargs) - if 'data' in kwargs: - kwargs['data'] = jsonutils.dumps(kwargs['data']) - - resp = self.request(url, method, **kwargs) - body = resp.content - - if body and 'application/json' in resp.headers['content-type']: - try: - body = resp.json() - except ValueError: - LOG.error('Could not decode response body as JSON') - else: - body = None - - return resp, body - - def json_patch_request(self, url, method='PATCH', **kwargs): - content_type = 'application/murano-packages-json-patch' - return self.json_request( - url, method, content_type=content_type, **kwargs) - - def head(self, url, **kwargs): - return self.json_request(url, "HEAD", **kwargs) - - def get(self, url, **kwargs): - return self.json_request(url, "GET", **kwargs) - - def post(self, url, **kwargs): - return self.json_request(url, "POST", **kwargs) - - def put(self, url, **kwargs): - return self.json_request(url, "PUT", **kwargs) - - def delete(self, url, **kwargs): - return self.request(url, "DELETE", **kwargs) - - def patch(self, url, **kwargs): - return self.json_request(url, "PATCH", **kwargs) - - -class SessionClient(keystone_adapter.Adapter): - """Murano specific keystoneclient Adapter. - - Murano can't use keystoneclient LegacyJsonAdapter, because murano has the - check for right content-type for "update" operation which is - 'application/murano-packages-json-patch'. So, we need to create our own - adapter. - """ - - def request(self, url, method, **kwargs): - raise_exc = kwargs.pop('raise_exc', True) - _set_data(kwargs) - resp = super(SessionClient, self).request(url, - method, - raise_exc=False, - **kwargs) - - if raise_exc and resp.status_code >= 400: - LOG.trace("Error communicating with {url}: {exc}" - .format(url=url, exc=exc.from_response(resp))) - raise exc.from_response(resp) - - return resp - - def json_request(self, url, method, **kwargs): - headers = kwargs.setdefault('headers', {}) - headers['Content-Type'] = kwargs.pop('content_type', - 'application/json') - - _set_data(kwargs) - if 'data' in kwargs: - kwargs['data'] = jsonutils.dumps(kwargs['data']) - # NOTE(starodubcevna): We need to prove that json field is empty, - # or it will be modified by keystone adapter. - kwargs['json'] = None - - resp = self.request(url, method, **kwargs) - body = resp.text - if body: - try: - body = jsonutils.loads(body) - except ValueError: - pass - - return resp, body - - def json_patch_request(self, url, method='PATCH', **kwargs): - content_type = 'application/murano-packages-json-patch' - return self.json_request( - url, method, content_type=content_type, **kwargs) - - -def _construct_http_client(*args, **kwargs): - session = kwargs.pop('session', None) - auth = kwargs.pop('auth', None) - endpoint = next(iter(args), None) - - if session: - service_type = kwargs.pop('service_type', None) - endpoint_type = kwargs.pop('endpoint_type', None) - region_name = kwargs.pop('region_name', None) - service_name = kwargs.pop('service_name', None) - parameters = { - 'endpoint_override': endpoint, - 'session': session, - 'auth': auth, - 'interface': endpoint_type, - 'service_type': service_type, - 'region_name': region_name, - 'service_name': service_name, - 'user_agent': 'python-muranoclient', - } - parameters.update(kwargs) - return SessionClient(**parameters) - else: - return HTTPClient(*args, **kwargs) - - -def _set_data(kwargs): - if 'body' in kwargs: - if 'data' in kwargs: - raise ValueError("Can't provide both 'data' and " - "'body' to a request") - LOG.warning("Use of 'body' is deprecated; use 'data' instead") - kwargs['data'] = kwargs.pop('body') diff --git a/muranoclient/common/utils.py b/muranoclient/common/utils.py deleted file mode 100644 index ad0a3509..00000000 --- a/muranoclient/common/utils.py +++ /dev/null @@ -1,831 +0,0 @@ -# Copyright 2012 OpenStack LLC. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import collections -from io import BytesIO -import json -from muranopkgcheck import manager as check_manager -from muranopkgcheck import pkg_loader as check_pkg_loader -from muranopkgcheck import validators as check_validators -import os -import re -import shutil -import sys -import tempfile -import textwrap -import uuid -import warnings -import zipfile - -from oslo_log import log as logging -from oslo_serialization import jsonutils -from oslo_utils import encodeutils -from oslo_utils import uuidutils - -import prettytable -import requests -import urllib -import yaml -import yaql - -from muranoclient.common import exceptions -from muranoclient.i18n import _ - -try: - import yaql.language # noqa - - from muranoclient.common.yaqlexpression import YaqlExpression -except ImportError: - # no yaql.language means legacy yaql - from muranoclient.common.yaqlexpression_legacy import YaqlExpression - - -LOG = logging.getLogger(__name__) - - -# Decorator for cli-args -def arg(*args, **kwargs): - def _decorator(func): - # Because of the semantics of decorator composition if we just append - # to the options list positional options will appear to be backwards. - func.__dict__.setdefault('arguments', []).insert(0, (args, kwargs)) - return func - return _decorator - - -def json_formatter(js): - return jsonutils.dumps(js, indent=2) - - -def text_wrap_formatter(d): - return '\n'.join(textwrap.wrap(d or '', 55)) - - -def pretty_choice_list(l): - return ', '.join("'%s'" % i for i in l) - - -def print_list(objs, fields, field_labels, formatters=None, sortby=0): - if formatters is None: - formatters = {} - pt = prettytable.PrettyTable([f for f in field_labels], caching=False) - pt.align = 'l' - - for o in objs: - row = [] - for field in fields: - if field in formatters: - row.append(formatters[field](o)) - else: - data = getattr(o, field, None) or '' - row.append(data) - pt.add_row(row) - - result = encodeutils.safe_encode(pt.get_string()) - - result = result.decode() - - print(result) - - -def print_dict(d, formatters=None): - if formatters is None: - formatters = {} - pt = prettytable.PrettyTable(['Property', 'Value'], caching=False) - pt.align = 'l' - - for field in d.keys(): - if field in formatters: - pt.add_row([field, formatters[field](d[field])]) - else: - pt.add_row([field, d[field]]) - - result = encodeutils.safe_encode(pt.get_string(sortby='Property')) - - result = result.decode() - - print(result) - - -def find_resource(manager, name_or_id, *args, **kwargs): - """Helper for the _find_* methods.""" - # first try to get entity as integer id - try: - if isinstance(name_or_id, int) or name_or_id.isdigit(): - return manager.get(int(name_or_id), *args, **kwargs) - except exceptions.NotFound: - pass - - # now try to get entity as uuid - try: - uuid.UUID(str(name_or_id)) - return manager.get(name_or_id, *args, **kwargs) - except (ValueError, exceptions.NotFound): - pass - - # finally try to find entity by name - try: - return manager.find(name=name_or_id) - except exceptions.NotFound: - msg = "No %s with a name or ID of '%s' exists." % \ - (manager.resource_class.__name__.lower(), name_or_id) - raise exceptions.CommandError(msg) - - -def string_to_bool(arg): - return arg.strip().lower() in ('t', 'true', 'yes', '1') - - -def env(*vars, **kwargs): - """Search for the first defined of possibly many env vars - - Returns the first environment variable defined in vars, or - returns the default defined in kwargs. - """ - for v in vars: - value = os.environ.get(v, None) - if value: - return value - return kwargs.get('default', '') - - -def exit(msg=''): - if msg: - print(encodeutils.safe_encode(msg), file=sys.stderr) - sys.exit(1) - - -def getsockopt(self, *args, **kwargs): - """Allows us to monkey patch eventlet's GreenSocket - - A function which allows us to monkey patch eventlet's - GreenSocket, adding a required 'getsockopt' method. - TODO: (mclaren) we can remove this once the eventlet fix - (https://bitbucket.org/eventlet/eventlet/commits/609f230) - lands in mainstream packages. - NOTE: Already in 0.13, but we can't be sure that all clients - that use python-muranoclient also use newest eventlet - """ - return self.fd.getsockopt(*args, **kwargs) - - -def exception_to_str(exc): - try: - error = str(exc) - except UnicodeError: - error = ("Caught '%(exception)s' exception." % - {"exception": exc.__class__.__name__}) - return encodeutils.safe_encode(error, errors='ignore') - - -class NoCloseProxy(object): - """A proxy object, that does nothing on close.""" - def __init__(self, obj): - self.obj = obj - - def close(self): - return - - def __getattr__(self, name): - return getattr(self.obj, name) - - -class File(object): - def __init__(self, name, binary=True): - self.name = name - self.binary = binary - - def open(self): - mode = 'rb' if self.binary else 'r' - if hasattr(self.name, 'read'): - # NOTE(kzaitsev) We do not want to close a file object - # passed to File wrapper. The caller should be responsible - # for closing it - return NoCloseProxy(self.name) - else: - if os.path.isfile(self.name): - return open(self.name, mode) - if os.path.isdir(self.name): - tmp = tempfile.NamedTemporaryFile() - archive = zipfile.ZipFile(tmp, 'w', zipfile.ZIP_DEFLATED) - for root, dirs, files in os.walk(self.name): - for _file in files: - destination = os.path.relpath( - os.path.join(root, _file), os.path.join(self.name)) - archive.write(os.path.join(root, _file), destination) - tmp.flush() - return open(tmp.name, mode) - url = urllib.parse.urlparse(self.name) - if url.scheme in ('http', 'https'): - resp = requests.get(self.name, stream=True) - if not resp.ok: - raise ValueError("Got non-ok status({0}) " - "while connecting to {1}".format( - resp.status_code, self.name)) - temp_file = tempfile.NamedTemporaryFile(mode='w+b') - for chunk in resp.iter_content(1024 * 1024): - temp_file.write(chunk) - temp_file.flush() - return open(temp_file.name, mode) - raise ValueError("Can't open {0}".format(self.name)) - - -def to_url(filename, base_url, version='', path='/', extension=''): - if urllib.parse.urlparse(filename).scheme in ('http', 'https'): - return filename - if not base_url: - raise ValueError("No base_url for repository supplied") - if '/' in filename or filename in ('.', '..'): - raise ValueError("Invalid filename path supplied: {0}".format( - filename)) - version = '.' + version if version else '' - return urllib.parse.urljoin(base_url, - path + filename + version + extension) - - -class FileWrapperMixin(object): - def __init__(self, file_wrapper): - self.file_wrapper = file_wrapper - try: - self._file = self.file_wrapper.open() - except Exception: - # NOTE(kzaitsev): We need to have _file available at __del__ time. - self._file = None - raise - - def file(self): - self._file.seek(0) - return self._file - - def close(self): - if self._file and not self._file.closed: - self._file.close() - - def save(self, dst, binary=True): - file_name = self.file_wrapper.name - - if urllib.parse.urlparse(file_name).scheme: - file_name = file_name.split('/')[-1] - - dst = os.path.join(dst, file_name) - - mode = 'wb' if binary else 'w' - with open(dst, mode) as dst_file: - self._file.seek(0) - shutil.copyfileobj(self._file, dst_file) - - def __del__(self): - self.close() - - -class Package(FileWrapperMixin): - """Represents murano package contents.""" - - @staticmethod - def from_file(file_obj): - if not isinstance(file_obj, File): - file_obj = File(file_obj) - pkg = Package(file_obj) - errs = pkg.validate() - if errs: - raise exceptions.HTTPBadRequest(details=errs) - return pkg - - @staticmethod - def fromFile(file_obj): - warnings.warn("Use from_file function", DeprecationWarning) - return Package.from_file(file_obj) - - @staticmethod - def from_location(name, base_url='', version='', url='', path=None): - """Open file using one of three possible options - - If path is supplied search for name file in the path, otherwise - if url is supplied - open that url and finally search murano - repository for the package. - """ - if path: - pkg_name = os.path.join(path, name) - file_name = None - for f in [pkg_name, pkg_name + '.zip']: - if os.path.exists(f): - file_name = f - if file_name: - return Package.from_file(file_name) - LOG.error("Couldn't find file for package {0}, tried {1}".format( - name, [pkg_name, pkg_name + '.zip'])) - if url: - return Package.from_file(url) - return Package.from_file(to_url( - name, - base_url=base_url, - version=version, - path='apps/', - extension='.zip') - ) - - def validate(self): - m = check_manager.Manager(self._file, - loader=check_pkg_loader.ZipLoader) - errors = m.validate( - validators=[check_validators.manifest.ManifestValidator], - only_errors=True) - if errors: - fmt = check_manager.PlainTextFormatter().format - return 'Invalid Murano package\n{}\n'.format(fmt(errors)) - - @property - def contents(self): - """Contents of a package.""" - if not hasattr(self, '_contents'): - try: - self._file.seek(0) - self._zip_obj = zipfile.ZipFile( - BytesIO(self._file.read())) - except Exception as e: - LOG.error("Error {0} occurred," - " while parsing the package".format(e)) - raise - return self._zip_obj - - @property - def manifest(self): - """Parsed manifest file of a package.""" - if not hasattr(self, '_manifest'): - try: - self._manifest = yaml.safe_load( - self.contents.open('manifest.yaml')) - except Exception as e: - LOG.error("Error {0} occurred, while extracting " - "manifest from package".format(e)) - raise - return self._manifest - - def images(self): - """Returns a list of required image specifications.""" - if 'images.lst' not in self.contents.namelist(): - return [] - try: - return yaml.safe_load( - self.contents.open('images.lst')).get('Images', []) - except Exception: - return [] - - @property - def resolvers(self): - if not hasattr(self, '_resolvers'): - self.classes - return self._resolvers - - @property - def classes(self): - if not hasattr(self, '_classes'): - self._classes = {} - self._resolvers = {} - for class_name, class_file in ( - self.manifest.get('Classes', {}).items()): - filename = "Classes/%s" % class_file - if filename not in self.contents.namelist(): - continue - klass_list = yaml.load_all(self.contents.open(filename), - DummyYaqlYamlLoader) - if not klass_list: - raise ValueError('No classes defined in file') - resolver = None - for klass in klass_list: - ns = klass.get('Namespaces') - if ns: - resolver = NamespaceResolver(ns) - name = klass.get('Name') - if name and resolver: - name = resolver.resolve_name(name) - if name == class_name: - self._classes[class_name] = klass - self._resolvers[class_name] = resolver - break - return self._classes - - @property - def ui(self): - if not hasattr(self, '_ui'): - if 'UI/ui.yaml' in self.contents.namelist(): - self._ui = self.contents.open('UI/ui.yaml') - else: - self._ui = None - return self._ui - - @property - def logo(self): - if not hasattr(self, '_logo'): - if 'logo.png' in self.contents.namelist(): - self._logo = self.contents.open('logo.png') - else: - self._logo = None - return self._logo - - def _get_package_order(self, packages_graph): - """Sorts packages according to dependencies between them - - Murano allows cyclic dependencies. It is impossible - to do topological sort for graph with cycles, so at first - graph condensation should be built. - For condensation building Kosaraju's algorithm is used. - Packages in strongly connected components can be situated - in random order to each other. - """ - def topological_sort(graph, start_node): - order = [] - not_seen = set(graph) - - def dfs(node): - not_seen.discard(node) - for dep_node in graph[node]: - if dep_node in not_seen: - dfs(dep_node) - order.append(node) - - dfs(start_node) - return order - - def transpose_graph(graph): - transposed = collections.defaultdict(list) - for node, deps in graph.items(): - for dep in deps: - transposed[dep].append(node) - return transposed - - order = topological_sort(packages_graph, self.manifest['FullName']) - order.reverse() - transposed = transpose_graph(packages_graph) - - def top_sort_by_components(graph, component_order): - result = [] - seen = set() - - def dfs(node): - seen.add(node) - result.append(node) - for dep_node in graph[node]: - if dep_node not in seen: - dfs(dep_node) - for item in component_order: - if item not in seen: - dfs(item) - return reversed(result) - return top_sort_by_components(transposed, order) - - def requirements(self, base_url, path=None, dep_dict=None): - """Scans Require section of manifests of all the dependencies. - - Returns a dict with FQPNs as keys and respective Package objects - as values, ordered by topological sort. - - :param base_url: url of packages location - :param path: local path of packages location - :param dep_dict: unused. Left for backward compatibility - """ - - unordered_requirements = {} - requirements_graph = collections.defaultdict(list) - dep_queue = collections.deque([(self.manifest['FullName'], self)]) - while dep_queue: - dep_name, dep_file = dep_queue.popleft() - unordered_requirements[dep_name] = dep_file - direct_deps = Package._get_direct_deps(dep_file, base_url, path) - for name, file in direct_deps: - if name not in unordered_requirements: - dep_queue.append((name, file)) - requirements_graph[dep_name] = [dep[0] for dep in direct_deps] - - ordered_reqs_names = self._get_package_order(requirements_graph) - ordered_reqs_dict = collections.OrderedDict() - for name in ordered_reqs_names: - ordered_reqs_dict[name] = unordered_requirements[name] - - return ordered_reqs_dict - - @staticmethod - def _get_direct_deps(package, base_url, path): - result = [] - if 'Require' in package.manifest: - for dep_name, ver in package.manifest['Require'].items(): - try: - req_file = Package.from_location( - dep_name, - version=ver, - path=path, - base_url=base_url, - ) - except Exception as e: - LOG.error("Error {0} occurred while parsing package {1}, " - "required by {2} package".format( - e, dep_name, - package.manifest['FullName'])) - continue - result.append((req_file.manifest['FullName'], req_file)) - return result - - -def save_image_local(image_spec, base_url, dst): - dst = os.path.join(dst, image_spec['Name']) - - download_url = to_url( - image_spec.get("Url", image_spec['Name']), - base_url=base_url, - path='images/' - ) - - with open(dst, "w") as image_file: - response = requests.get(download_url, stream=True) - total_length = response.headers.get('content-length') - - if total_length is None: - image_file.write(response.content) - else: - dl = 0 - total_length = int(total_length) - for chunk in response.iter_content(1024 * 1024): - dl += len(chunk) - image_file.write(chunk) - done = int(50 * dl / total_length) - sys.stdout.write("\r[{0}{1}]". - format('=' * done, ' ' * (50 - done))) - sys.stdout.flush() - sys.stdout.write("\n") - image_file.flush() - - -def ensure_images(glance_client, image_specs, base_url, - local_path=None, - is_package_public=False): - """Ensure that images are available - - Ensure that images from image_specs are available in glance. If not - attempts: instructs glance to download the images and sets murano-specific - metadata for it. - """ - def _image_valid(image, keys): - for key in keys: - if key not in image: - LOG.warning("Image specification invalid: " - "No {0} key in image ").format(key) - return False - return True - - keys = ['Name', 'DiskFormat', 'ContainerFormat', ] - installed_images = [] - for image_spec in image_specs: - if not _image_valid(image_spec, keys): - continue - filters = { - 'name': image_spec["Name"], - 'disk_format': image_spec["DiskFormat"], - 'container_format': image_spec["ContainerFormat"], - } - - images = glance_client.images.list(filters=filters) - try: - img = next(images).to_dict() - except StopIteration: - img = None - - update_metadata = False - if img: - LOG.info("Found desired image {0}, id {1}".format( - img['name'], img['id'])) - # check for murano meta-data - if 'murano_image_info' in img.get('properties', {}): - LOG.info("Image {0} already has murano meta-data".format( - image_spec['Name'])) - else: - update_metadata = True - else: - LOG.info("Desired image {0} not found attempting " - "to download".format(image_spec['Name'])) - update_metadata = True - - img_file = None - if local_path: - img_file = os.path.join(local_path, image_spec['Name']) - - if img_file and not os.path.exists(img_file): - LOG.error("Image file {0} does not exist." - .format(img_file)) - - if img_file and os.path.exists(img_file): - img = glance_client.images.create( - name=image_spec['Name'], - container_format=image_spec['ContainerFormat'], - disk_format=image_spec['DiskFormat'], - data=open(img_file, 'rb'), - ) - img = img.to_dict() - else: - download_url = to_url( - image_spec.get("Url", image_spec['Name']), - base_url=base_url, - path='images/', - ) - LOG.info("Instructing glance to download image {0}".format( - image_spec['Name'])) - img = glance_client.images.create( - name=image_spec["Name"], - container_format=image_spec['ContainerFormat'], - disk_format=image_spec['DiskFormat'], - copy_from=download_url) - img = img.to_dict() - - if is_package_public: - try: - glance_client.images.update(img['id'], is_public=True) - LOG.debug('Success update for image {0}'.format(img['id'])) - except Exception as e: - LOG.exception(_("Error {0} occurred while setting " - "image {1} public").format(e, img['id'])) - - installed_images.append(img) - - if update_metadata and 'Meta' in image_spec: - LOG.info("Updating image {0} metadata".format( - image_spec['Name'])) - murano_image_info = jsonutils.dumps(image_spec['Meta']) - glance_client.images.update( - img['id'], properties={'murano_image_info': - murano_image_info}) - return installed_images - - -class Bundle(FileWrapperMixin): - """Represents murano bundle contents.""" - - @staticmethod - def from_file(file_obj): - if not isinstance(file_obj, File): - file_obj = File(file_obj, binary=False) - return Bundle(file_obj) - - @staticmethod - def fromFile(file_obj): - warnings.warn("Use from_file function", DeprecationWarning) - return Bundle.from_file(file_obj) - - def package_specs(self): - """Get a generator yielding package specifications - - Returns a generator yielding package specifications i.e. - dicts with 'Name' and 'Version' fields - """ - self._file.seek(0) - bundle = None - try: - # NOTE(kzaitsev) jsonutils throws a type error here - # see bug 1515231 - bundle = json.load(self._file) - except ValueError: - pass - if bundle is None: - try: - bundle = yaml.safe_load(self._file) - except yaml.error.YAMLError: - pass - - if bundle is None or 'Packages' not in bundle: - raise ValueError("Can't parse bundle contents") - - for package in bundle['Packages']: - if 'Name' not in package: - continue - yield package - - def packages(self, base_url='', path=None): - """Get a generator yielding Package objects - - Returns a generator, yielding Package objects for each package - found in the bundle. - """ - for package in self.package_specs(): - try: - pkg_obj = Package.from_location( - package['Name'], - version=package.get('Version'), - url=package.get('Url'), - path=path, - base_url=base_url, - ) - - except Exception as e: - LOG.error("Error {0} occurred while obtaining " - "package {1}".format(e, package['Name'])) - continue - yield pkg_obj - - -class DummyYaqlYamlLoader(yaml.SafeLoader): - """Constructor that treats !yaql as string.""" - pass - - -DummyYaqlYamlLoader.add_constructor( - u'!yaql', DummyYaqlYamlLoader.yaml_constructors[u'tag:yaml.org,2002:str']) - - -class YaqlYamlLoader(yaml.SafeLoader): - pass - - -# workaround for PyYAML bug: http://pyyaml.org/ticket/221 -resolvers = {} -for k, v in yaml.SafeLoader.yaml_implicit_resolvers.items(): - resolvers[k] = v[:] -YaqlYamlLoader.yaml_implicit_resolvers = resolvers - - -def yaql_constructor(loader, node): - value = loader.construct_scalar(node) - return YaqlExpression(value) - - -YaqlYamlLoader.add_constructor(u'!yaql', yaql_constructor) -YaqlYamlLoader.add_implicit_resolver(u'!yaql', YaqlExpression, None) - - -def traverse_and_replace(obj, - pattern=re.compile(r'^===id(\d+)===$'), - replacements=None): - """Helper function that traverses object model and substitutes ids. - - Recursively checks values of objects found in `obj` against `pattern`, - and replaces strings that match pattern with uuidutils.generate_uuid(). - Keeps track of any replacements already made, i.e. ===id1=== would - always be the same, across `obj`. Uses 1st group, found in the `pattern` - regexp as unique identifier of a replacement - """ - if replacements is None: - replacements = collections.defaultdict( - lambda: uuidutils.generate_uuid(dashed=False)) - - def _maybe_replace(obj, key, value): - """Check and replace value against pattern""" - if isinstance(value, str): - m = pattern.search(value) - if m: - if m.group(1) not in replacements: - replacements[m.group(1)] = uuidutils.generate_uuid( - dashed=False) - obj[key] = replacements[m.group(1)] - - if isinstance(obj, list): - for key, value in enumerate(obj): - if isinstance(value, (list, dict)): - traverse_and_replace(value, pattern, replacements) - else: - _maybe_replace(obj, key, value) - elif isinstance(obj, dict): - for key, value in obj.items(): - if isinstance(value, (list, dict)): - traverse_and_replace(value, pattern, replacements) - else: - _maybe_replace(obj, key, value) - else: - _maybe_replace(obj, key, value) - - -class NamespaceResolver(object): - """Copied from main murano repo - - original at murano/dsl/namespace_resolver.py - """ - - def __init__(self, namespaces): - self._namespaces = namespaces - self._namespaces[''] = '' - - def resolve_name(self, name, relative=None): - if name is None: - raise ValueError() - if name and name.startswith(':'): - return name[1:] - if ':' in name: - parts = name.split(':') - if len(parts) != 2 or not parts[1]: - raise NameError('Incorrectly formatted name ' + name) - if parts[0] not in self._namespaces: - raise KeyError('Unknown namespace prefix ' + parts[0]) - return '.'.join((self._namespaces[parts[0]], parts[1])) - if not relative and '=' in self._namespaces and '.' not in name: - return '.'.join((self._namespaces['='], name)) - if relative and '.' not in name: - return '.'.join((relative, name)) - return name diff --git a/muranoclient/common/yaqlexpression.py b/muranoclient/common/yaqlexpression.py deleted file mode 100644 index 1a689be0..00000000 --- a/muranoclient/common/yaqlexpression.py +++ /dev/null @@ -1,60 +0,0 @@ -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import re - -import yaql -from yaql.language import exceptions as yaql_exc - - -def _set_up_yaql(): - legacy_engine_options = { - 'yaql.limitIterators': 10000, - 'yaql.memoryQuota': 1000000 - } - return yaql.YaqlFactory().create(options=legacy_engine_options) - - -YAQL = _set_up_yaql() - - -class YaqlExpression(object): - def __init__(self, expression): - self._expression = str(expression) - self._parsed_expression = YAQL(self._expression) - - def expression(self): - return self._expression - - def __repr__(self): - return 'YAQL(%s)' % self._expression - - def __str__(self): - return self._expression - - @staticmethod - def match(expr): - if not isinstance(expr, str): - return False - if re.match(r'^[\s\w\d.:]*$', expr): - return False - try: - YAQL(expr) - return True - except yaql_exc.YaqlGrammarException: - return False - except yaql_exc.YaqlLexicalException: - return False - - def evaluate(self, data=yaql.utils.NO_VALUE, context=None): - return self._parsed_expression.evaluate(data=data, context=context) diff --git a/muranoclient/common/yaqlexpression_legacy.py b/muranoclient/common/yaqlexpression_legacy.py deleted file mode 100644 index 01bb5ee3..00000000 --- a/muranoclient/common/yaqlexpression_legacy.py +++ /dev/null @@ -1,48 +0,0 @@ -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. -import re - -import yaql - - -class YaqlExpression(object): - def __init__(self, expression): - self._expression = str(expression) - self._parsed_expression = yaql.parse(self._expression) - - def expression(self): - return self._expression - - def __repr__(self): - return 'YAQL(%s)' % self._expression - - def __str__(self): - return self._expression - - @staticmethod - def match(expr): - if not isinstance(expr, str): - return False - if re.match(r'^[\s\w\d.:]*$', expr): - return False - try: - yaql.parse(expr) - return True - except yaql.exceptions.YaqlGrammarException: - return False - except yaql.exceptions.YaqlLexicalException: - return False - - def evaluate(self, data=None, context=None): - return self._parsed_expression.evaluate(data=data, context=context) diff --git a/muranoclient/data/heat_logo.png b/muranoclient/data/heat_logo.png deleted file mode 100644 index 09cd4d37c049a3e4f2a7abab60aedc3666b88e64..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 67840 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

UC9f_W=N6W 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 diff --git a/muranoclient/glance/__init__.py b/muranoclient/glance/__init__.py deleted file mode 100644 index 0c6c07d3..00000000 --- a/muranoclient/glance/__init__.py +++ /dev/null @@ -1,60 +0,0 @@ -# Copyright (c) 2015 Mirantis, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from glanceclient import exc - - -class ArtifactType(object): - generic_properties = ('created_at', 'id', 'name', 'owner', 'state', - 'type_name', 'type_version', 'updated_at', - 'version', 'visibility', 'description', 'tags', - 'published_at', 'deleted_at') - - supported_show_levels = ('none', 'basic', 'direct', 'transitive') - - def __init__(self, **kwargs): - try: - for prop in self.generic_properties: - setattr(self, prop, kwargs.pop(prop)) - except KeyError: - msg = "Invalid parameters were provided" - raise exc.HTTPBadRequest(msg) - self.type_specific_properties = {} - for key, value in kwargs.items(): - try: - if _is_dependency(value): - - self.type_specific_properties[key] = ArtifactType(**value) - elif _is_dependencies_list(value): - - self.type_specific_properties[key] = [ArtifactType(**elem) - for elem in value] - else: - self.type_specific_properties[key] = value - except exc.HTTPBadRequest: - # if it's not possible to generate artifact object then - # assign the value as a regular dict. - self.type_specific_properties[key] = value - - -def _is_dependency(d): - if type(d) is dict and d.get('type_name') and d.get('type_version'): - return True - return False - - -def _is_dependencies_list(l): - if type(l) is list and all(_is_dependency(d) for d in l): - return True - return False diff --git a/muranoclient/glance/artifacts.py b/muranoclient/glance/artifacts.py deleted file mode 100644 index 79a526f4..00000000 --- a/muranoclient/glance/artifacts.py +++ /dev/null @@ -1,360 +0,0 @@ -# Copyright (c) 2015 Mirantis, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from glanceclient.common import utils -from glanceclient import exc -from oslo_utils import encodeutils -from urllib import parse - -from muranoclient.glance import ArtifactType - - -glare_urls = { - 'create': '/v{version}/artifacts/{type_name}/v{type_version}/drafts', - 'update_get_delete': '/v{version}/artifacts/{type_name}/v{type_version}' - '/{artifact_id}', - 'list_drafts': '/v{version}/artifacts/{type_name}/v{type_version}/drafts?', - 'list_no_drafts': '/v{version}/artifacts/{type_name}/v{type_version}?', - 'publish': '/v{version}/artifacts/{type_name}/v{type_version}/' - '{artifact_id}/publish', - 'blob': '/v{version}/artifacts/{type_name}/v{type_version}/{artifact_id}' - '/{blob_property}', -} - - -class Controller(object): - def __init__(self, http_client, type_name=None, type_version=None, - version='0.1'): - self.http_client = http_client - self.type_name = type_name - self.type_version = type_version - self.version = version - self.default_page_size = 20 - self.sort_dir_values = ('asc', 'desc') - - def _check_type_params(self, type_name, type_version): - """Check that type name and type versions were specified""" - type_name = type_name or self.type_name - type_version = type_version or self.type_version - - if type_name is None: - msg = "Type name must be specified" - raise exc.HTTPBadRequest(msg) - - if type_version is None: - msg = "Type version must be specified" - raise exc.HTTPBadRequest(msg) - - return type_name, type_version - - def _validate_sort_param(self, sort): - """Validates sorting argument for invalid keys and directions values. - - :param sort: comma-separated list of sort keys with optional <:dir> - after each key - """ - for sort_param in sort.strip().split(','): - key, _sep, dir = sort_param.partition(':') - if dir and dir not in self.sort_dir_values: - msg = ('Invalid sort direction: %(sort_dir)s.' - ' It must be one of the following: %(available)s.' - ) % {'sort_dir': dir, - 'available': ', '.join(self.sort_dir_values)} - raise exc.HTTPBadRequest(msg) - return sort - - def create(self, name, version, type_name=None, type_version=None, - **kwargs): - """Create an artifact of given type and version. - - :param name: name of creating artifact. - :param version: semver string describing an artifact version - """ - type_name, type_version = self._check_type_params(type_name, - type_version) - kwargs.update({'name': name, 'version': version}) - url = glare_urls['create'].format(version=self.version, - type_name=type_name, - type_version=type_version) - resp, body = self.http_client.post(url, data=kwargs) - return ArtifactType(**body) - - def update(self, artifact_id, type_name=None, type_version=None, - remove_props=None, **kwargs): - """Update attributes of an artifact. - - :param artifact_id: ID of the artifact to modify. - :param remove_props: List of property names to remove - :param **kwargs: Artifact attribute names and their new values. - """ - type_name, type_version = self._check_type_params(type_name, - type_version) - url = glare_urls['update_get_delete'].format(version=self.version, - type_name=type_name, - type_version=type_version, - artifact_id=artifact_id) - hdrs = { - 'Content-Type': 'application/openstack-images-v2.1-json-patch'} - - artifact_obj = self.get(artifact_id, type_name, type_version) - - changes = [] - if remove_props: - for prop in remove_props: - if prop in ArtifactType.generic_properties: - msg = "Generic properties cannot be removed" - raise exc.HTTPBadRequest(msg) - if prop not in kwargs: - changes.append({'op': 'remove', - 'path': '/' + prop}) - - for prop in kwargs: - if prop in artifact_obj.generic_properties: - op = 'add' if getattr(artifact_obj, - prop) is None else 'replace' - elif prop in artifact_obj.type_specific_properties: - if artifact_obj.type_specific_properties[prop] is None: - op = 'add' - else: - op = 'replace' - else: - msg = ("Property '%s' doesn't exist in type '%s' with version" - " '%s'" % (prop, type_name, type_version)) - raise exc.HTTPBadRequest(msg) - changes.append({'op': op, 'path': '/' + prop, - 'value': kwargs[prop]}) - - resp, body = self.http_client.patch(url, headers=hdrs, data=changes) - return ArtifactType(**body) - - def get(self, artifact_id, type_name=None, type_version=None, - show_level=None): - """Get information about an artifact. - - :param artifact_id: ID of the artifact to get. - :param show_level: value of datalization. Possible values: - "none", "basic", "direct", "transitive" - """ - type_name, type_version = self._check_type_params(type_name, - type_version) - - url = glare_urls['update_get_delete'].format(version=self.version, - type_name=type_name, - type_version=type_version, - artifact_id=artifact_id) - if show_level: - if show_level not in ArtifactType.supported_show_levels: - msg = "Invalid show level: %s" % show_level - raise exc.HTTPBadRequest(msg) - url += '?show_level=%s' % show_level - resp, body = self.http_client.get(url) - return ArtifactType(**body) - - def list(self, **kwargs): - return self._list(drafts=False, **kwargs) - - def drafts(self, **kwargs): - return self._list(drafts=True, **kwargs) - - def _list(self, drafts, type_name=None, type_version=None, **kwargs): - """Retrieve a listing of Image objects. - - :param page_size: Number of images to request in each - paginated request. - :returns: generator over list of artifacts. - """ - type_name, type_version = self._check_type_params(type_name, - type_version) - - limit = kwargs.get('limit') - page_size = kwargs.get('page_size') or self.default_page_size - - def paginate(url, page_size, limit=None): - next_url = url - - while True: - if limit and page_size > limit: - next_url = next_url.replace("limit=%s" % page_size, - "limit=%s" % limit) - - resp, body = self.http_client.get(next_url) - for artifact in body['artifacts']: - yield ArtifactType(**artifact) - - if limit: - limit -= 1 - if limit <= 0: - raise StopIteration - - try: - next_url = body['next'] - except KeyError: - return - - filters = kwargs.get('filters', {}) - filters['limit'] = page_size - - url_params = [] - for param, items in filters.items(): - values = [items] if not isinstance(items, list) else items - for value in values: - if isinstance(value, str): - value = encodeutils.safe_encode(value) - url_params.append({param: value}) - - if drafts: - url = glare_urls['list_drafts'].format(version=self.version, - type_name=type_name, - type_version=type_version) - else: - url = glare_urls['list_no_drafts'].format( - version=self.version, - type_name=type_name, - type_version=type_version - ) - - for param in url_params: - url = '%s&%s' % (url, parse.urlencode(param)) - - if 'sort' in kwargs: - url = '%s&sort=%s' % (url, self._validate_sort_param( - kwargs['sort'])) - - for artifact in paginate(url, page_size, limit): - yield artifact - - def active(self, artifact_id, type_name=None, type_version=None): - """Set artifact status to 'active'. - - :param artifact_id: ID of the artifact to get. - """ - type_name, type_version = self._check_type_params(type_name, - type_version) - - url = glare_urls['publish'].format(version=self.version, - type_name=type_name, - type_version=type_version, - artifact_id=artifact_id) - - resp, body = self.http_client.post(url) - return ArtifactType(**body) - - def deactivate(self, artifact_id, type_name=None, type_version=None): - raise NotImplementedError() - - def delete(self, artifact_id, type_name=None, type_version=None): - """Delete an artifact and all its data. - - :param artifact_id: ID of the artifact to delete. - """ - type_name, type_version = self._check_type_params(type_name, - type_version) - url = glare_urls['update_get_delete'].format(version=self.version, - type_name=type_name, - type_version=type_version, - artifact_id=artifact_id) - self.http_client.delete(url) - - def upload_blob(self, artifact_id, blob_property, data, position=None, - type_name=None, type_version=None): - """Upload blob data. - - :param artifact_id: ID of the artifact to download a blob - :param blob_property: blob property name - :param position: if blob_property is a list then the - position must be specified - """ - type_name, type_version = self._check_type_params(type_name, - type_version) - hdrs = {'Content-Type': 'application/octet-stream'} - - url = glare_urls['blob'].format(version=self.version, - type_name=type_name, - type_version=type_version, - artifact_id=artifact_id, - blob_property=blob_property) - if position: - url += "/%s" % position - - self.http_client.put(url, headers=hdrs, data=data) - - def download_blob(self, artifact_id, blob_property, position=None, - type_name=None, type_version=None, do_checksum=True): - """Get blob data. - - :param artifact_id: ID of the artifact to download a blob - :param blob_property: blob property name - :param position: if blob_property is a list then the - position must be specified - :param do_checksum: Enable/disable checksum validation. - """ - type_name, type_version = self._check_type_params(type_name, - type_version) - url = glare_urls['blob'].format(version=self.version, - type_name=type_name, - type_version=type_version, - artifact_id=artifact_id, - blob_property=blob_property) - if position: - url += '/%s' % position - - url += '/download' - - resp, body = self.http_client.get(url) - checksum = resp.headers.get('content-md5', None) - content_length = int(resp.headers.get('content-length', 0)) - - if checksum is not None and do_checksum: - body = utils.integrity_iter(body, checksum) - - return utils.IterableWithLength(body, content_length) - - def delete_blob(self, artifact_id, blob_property, position=None, - type_name=None, type_version=None): - """Delete blob and related data. - - :param artifact_id: ID of the artifact to delete a blob - :param blob_property: blob property name - :param position: if blob_property is a list then the - position must be specified - """ - type_name, type_version = self._check_type_params(type_name, - type_version) - url = glare_urls['blob'].format(version=self.version, - type_name=type_name, - type_version=type_version, - artifact_id=artifact_id, - blob_property=blob_property) - if position: - url += '/%s' % position - self.http_client.delete(url) - - def add_property(self, artifact_id, dependency_id, position=None, - type_name=None, type_version=None): - raise NotImplementedError() - - def replace_property(self, artifact_id, dependency_id, position=None, - type_name=None, type_version=None): - raise NotImplementedError() - - def remove_property(self, artifact_id, dependency_id, position=None, - type_name=None, type_version=None): - raise NotImplementedError() - - def artifact_export(self, artifact_id, - type_name=None, type_version=None): - raise NotImplementedError() - - def artifact_import(self, data, type_name=None, type_version=None): - raise NotImplementedError() diff --git a/muranoclient/glance/client.py b/muranoclient/glance/client.py deleted file mode 100644 index 6d5b6b1f..00000000 --- a/muranoclient/glance/client.py +++ /dev/null @@ -1,42 +0,0 @@ -# Copyright (c) 2015 Mirantis, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from glanceclient.common import http -from glanceclient.common import utils - -from muranoclient.glance import artifacts - - -class Client(object): - """Client for the OpenStack glance-glare API. - - :param string endpoint: A user-supplied endpoint URL for the glance - service. - :param string token: Token for authentication. - :param integer timeout: Allows customization of the timeout for client - http requests. (optional) - """ - - def __init__(self, endpoint, type_name, type_version, **kwargs): - endpoint, version = utils.strip_version(endpoint) - self.version = version or '0.1' - self.http_client = http.HTTPClient(endpoint, **kwargs) - - self.type_name = type_name - self.type_version = type_version - - self.artifacts = artifacts.Controller(self.http_client, - self.type_name, - self.type_version, - self.version) diff --git a/muranoclient/i18n.py b/muranoclient/i18n.py deleted file mode 100644 index 14b7accb..00000000 --- a/muranoclient/i18n.py +++ /dev/null @@ -1,24 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""oslo.i18n integration module. - -See https://docs.openstack.org/oslo.i18n/latest/user/index.html - -""" - -import oslo_i18n - -_translators = oslo_i18n.TranslatorFactory(domain='muranoclient') - -# The primary translation function using the well-known name "_" -_ = _translators.primary diff --git a/muranoclient/locale/en_GB/LC_MESSAGES/muranoclient.po b/muranoclient/locale/en_GB/LC_MESSAGES/muranoclient.po deleted file mode 100644 index dd82597f..00000000 --- a/muranoclient/locale/en_GB/LC_MESSAGES/muranoclient.po +++ /dev/null @@ -1,153 +0,0 @@ -# Andi Chandler , 2017. #zanata -msgid "" -msgstr "" -"Project-Id-Version: python-muranoclient VERSION\n" -"Report-Msgid-Bugs-To: https://bugs.launchpad.net/openstack-i18n/\n" -"POT-Creation-Date: 2018-01-30 01:30+0000\n" -"MIME-Version: 1.0\n" -"Content-Type: text/plain; charset=UTF-8\n" -"Content-Transfer-Encoding: 8bit\n" -"PO-Revision-Date: 2017-10-21 09:07+0000\n" -"Last-Translator: Andi Chandler \n" -"Language-Team: English (United Kingdom)\n" -"Language: en_GB\n" -"X-Generator: Zanata 4.3.3\n" -"Plural-Forms: nplurals=2; plural=(n != 1)\n" - -#, python-format -msgid "AmbiguousEndpoints: %s" -msgstr "AmbiguousEndpoints: %s" - -msgid "" -"Application catalog API version, default={0}(Env:" -"OS_APPLICATION_CATALOG_API_VERSION)" -msgstr "" -"Application catalogue API version, default={0}(Env:" -"OS_APPLICATION_CATALOG_API_VERSION)" - -#, python-format -msgid "AuthSystemNotFound: %s" -msgstr "AuthSystemNotFound: %s" - -#, python-format -msgid "Authentication failed. Missing options: %s" -msgstr "Authentication failed. Missing options: %s" - -msgid "Bad Gateway" -msgstr "Bad Gateway" - -msgid "Bad Request" -msgstr "Bad Request" - -msgid "Cannot find endpoint or token for request" -msgstr "Cannot find endpoint or token for request" - -msgid "Conflict" -msgstr "Conflict" - -msgid "Defaults to env[MURANO_URL]." -msgstr "Defaults to env[MURANO_URL]." - -msgid "Error {0} occurred while setting image {1} public" -msgstr "Error {0} occurred while setting image {1} public" - -msgid "Expectation Failed" -msgstr "Expectation Failed" - -msgid "Forbidden" -msgstr "Forbidden" - -msgid "Gateway Timeout" -msgstr "Gateway Timeout" - -msgid "Gone" -msgstr "Gone" - -msgid "HTTP Client Error" -msgstr "HTTP Client Error" - -msgid "HTTP Error" -msgstr "HTTP Error" - -msgid "HTTP Redirection" -msgstr "HTTP Redirection" - -msgid "HTTP Server Error" -msgstr "HTTP Server Error" - -msgid "HTTP Version Not Supported" -msgstr "HTTP Version Not Supported" - -msgid "Internal Server Error" -msgstr "Internal Server Error" - -#, python-format -msgid "" -"Invalid %(api_name)s client version '%(version)s'. Must be one of: " -"%(version_map)s" -msgstr "" -"Invalid %(api_name)s client version '%(version)s'. Must be one of: " -"%(version_map)s" - -msgid "Length Required" -msgstr "Length Required" - -msgid "Method Not Allowed" -msgstr "Method Not Allowed" - -#, python-format -msgid "Missing arguments: %s" -msgstr "Missing arguments: %s" - -msgid "Multiple Choices" -msgstr "Multiple Choices" - -#, python-format -msgid "No %(name)s matching %(args)s." -msgstr "No %(name)s matching %(args)s." - -msgid "Not Acceptable" -msgstr "Not Acceptable" - -msgid "Not Found" -msgstr "Not Found" - -msgid "Not Implemented" -msgstr "Not Implemented" - -msgid "Payment Required" -msgstr "Payment Required" - -msgid "Precondition Failed" -msgstr "Precondition Failed" - -msgid "Proxy Authentication Required" -msgstr "Proxy Authentication Required" - -msgid "Request Entity Too Large" -msgstr "Request Entity Too Large" - -msgid "Request Timeout" -msgstr "Request Timeout" - -msgid "Request-URI Too Long" -msgstr "Request-URI Too Long" - -msgid "Requested Range Not Satisfiable" -msgstr "Requested Range Not Satisfiable" - -msgid "Service Unavailable" -msgstr "Service Unavailable" - -#, python-format -msgid "Some attributes are missing in %(pkg_name)s: %(attrs)s." -msgstr "Some attributes are missing in %(pkg_name)s: %(attrs)s." - -msgid "Unauthorized" -msgstr "Unauthorised" - -msgid "Unprocessable Entity" -msgstr "Unprocessable Entity" - -msgid "Unsupported Media Type" -msgstr "Unsupported Media Type" diff --git a/muranoclient/osc/__init__.py b/muranoclient/osc/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/muranoclient/osc/plugin.py b/muranoclient/osc/plugin.py deleted file mode 100644 index 31021ffe..00000000 --- a/muranoclient/osc/plugin.py +++ /dev/null @@ -1,114 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""OpenStackClient plugin for Application Catalog service.""" - -from osc_lib import utils -from oslo_log import log as logging - -from muranoclient.apiclient import exceptions as exc -from muranoclient.glance import client as art_client -from muranoclient.i18n import _ - -LOG = logging.getLogger(__name__) - -DEFAULT_APPLICATION_CATALOG_API_VERSION = "1" -API_VERSION_OPTION = "os_application_catalog_api_version" -API_NAME = "application_catalog" -API_VERSIONS = { - '1': 'muranoclient.v1.client.Client', -} - - -def make_client(instance): - """Returns an application-catalog service client""" - application_catalog_client = utils.get_client_class( - API_NAME, - instance._api_version[API_NAME], - API_VERSIONS) - LOG.debug("Instantiating application-catalog client: {0}".format( - application_catalog_client)) - - kwargs = { - 'session': instance.session, - 'service_type': 'application-catalog', - 'region_name': instance._region_name - } - - murano_packages_service = \ - instance.get_configuration().get('murano_packages_service') - - if murano_packages_service == 'glare': - glare_endpoint = instance.get_configuration().get('glare_url') - if not glare_endpoint: - try: - # no glare_endpoint and we requested to store packages in glare - # check keystone catalog - glare_endpoint = instance.get_endpoint_for_service_type( - 'artifact', - region_name=instance._region_name, - interface=instance._interface - ) - except Exception: - raise exc.CommandError( - "You set murano-packages-service to {}" - " but there is not 'artifact' endpoint in keystone" - " Either register one or specify endpoint " - " via either --glare-url or env[GLARE_API]".format( - murano_packages_service)) - - artifacts_client = art_client.Client( - endpoint=glare_endpoint, - type_name='murano', - type_version=1, - token=instance.auth_ref.auth_token) - kwargs['artifacts_client'] = artifacts_client - - murano_endpoint = instance.get_configuration().get('murano_url') - if not murano_endpoint: - murano_endpoint = instance.get_endpoint_for_service_type( - 'application-catalog', - region_name=instance._region_name, - interface=instance._interface - ) - - client = application_catalog_client(murano_endpoint, **kwargs) - return client - - -def build_option_parser(parser): - """Hook to add global options""" - parser.add_argument( - '--os-application-catalog-api-version', - metavar='', - default=utils.env( - 'OS_APPLICATION_CATALOG_API_VERSION', - default=DEFAULT_APPLICATION_CATALOG_API_VERSION), - help=_("Application catalog API version, default={0}" - "(Env:OS_APPLICATION_CATALOG_API_VERSION)").format( - DEFAULT_APPLICATION_CATALOG_API_VERSION)) - parser.add_argument('--murano-url', - default=utils.env('MURANO_URL'), - help=_('Defaults to env[MURANO_URL].')) - parser.add_argument('--glare-url', - default=utils.env('GLARE_URL'), - help='Defaults to env[GLARE_URL].') - parser.add_argument('--murano-packages-service', - choices=['murano', 'glare'], - default=utils.env('MURANO_PACKAGES_SERVICE', - default='murano'), - help='Specifies if murano-api ("murano") or ' - 'Glance Artifact Repository ("glare") ' - 'should be used to store murano packages. ' - 'Defaults to env[MURANO_PACKAGES_SERVICE] or ' - 'to "murano"') - return parser diff --git a/muranoclient/osc/v1/__init__.py b/muranoclient/osc/v1/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/muranoclient/osc/v1/action.py b/muranoclient/osc/v1/action.py deleted file mode 100644 index 84441129..00000000 --- a/muranoclient/osc/v1/action.py +++ /dev/null @@ -1,90 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Application-catalog v1 action implementation""" - -import json - -from osc_lib.command import command -from oslo_log import log as logging - -from muranoclient.apiclient import exceptions - -LOG = logging.getLogger(__name__) - - -class StaticActionCall(command.ShowOne): - """Call static method of the MuranoPL class.""" - - def get_parser(self, prog_name): - parser = super(StaticActionCall, self).get_parser(prog_name) - parser.add_argument( - "class_name", - metavar='', - help="FQN of the class with static method", - ) - parser.add_argument( - "method_name", - metavar='', - help="Static method to run", - ) - parser.add_argument( - "--arguments", - metavar='', - nargs='*', - help="Method arguments. No arguments by default", - ) - parser.add_argument( - "--package-name", - metavar='', - default='', - help='Optional FQN of the package to look for the class in', - ) - parser.add_argument( - "--class-version", - default='', - help='Optional version of the class, otherwise version =0 is ' - 'used ', - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - arguments = {} - for argument in parsed_args.arguments or []: - if '=' not in argument: - raise exceptions.CommandError( - "Argument should be in form of KEY=VALUE. Found: " - "{0}".format(argument)) - key, value = argument.split('=', 1) - try: - value = json.loads(value) - except ValueError: - # treat value as a string if it doesn't load as json - pass - - arguments[key] = value - - request_body = { - "className": parsed_args.class_name, - "methodName": parsed_args.method_name, - "packageName": parsed_args.package_name or None, - "classVersion": parsed_args.class_version or '=0', - "parameters": arguments - } - - print("Waiting for result...") - result = client.static_actions.call(request_body).get_result() - return ["Static action result"], [result] diff --git a/muranoclient/osc/v1/category.py b/muranoclient/osc/v1/category.py deleted file mode 100644 index 5d6d83ba..00000000 --- a/muranoclient/osc/v1/category.py +++ /dev/null @@ -1,151 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Application-catalog v1 category action implementation""" - -import textwrap - -from osc_lib.command import command -from osc_lib import utils -from oslo_log import log as logging - -from muranoclient.apiclient import exceptions - -LOG = logging.getLogger(__name__) - - -class ListCategories(command.Lister): - """List all available categories.""" - - def take_action(self, parsed_args=None): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - if parsed_args is None: - parsed_args = {} - - data = client.categories.list() - - fields = ["id", "name"] - field_labels = ["ID", "Name"] - - return ( - field_labels, - list(utils.get_item_properties( - s, - fields, - ) for s in data) - ) - - -class ShowCategory(command.ShowOne): - """Display category details.""" - - def get_parser(self, prog_name): - parser = super(ShowCategory, self).get_parser(prog_name) - parser.add_argument( - "id", - metavar="", - help=("ID of a category(s) to show."), - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - category = client.categories.get(parsed_args.id) - packages = client.packages.filter(category=category.name) - to_display = dict(id=category.id, - name=category.name, - packages=', '.join(p.name - for p in packages)) - - to_display['packages'] = '\n'.join(textwrap.wrap(to_display['packages'] - or '', 55)) - - return self.dict2columns(to_display) - - -class CreateCategory(command.Lister): - """Create a category.""" - - def get_parser(self, prog_name): - parser = super(CreateCategory, self).get_parser(prog_name) - parser.add_argument( - "name", - metavar="", - help=("Category name."), - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - data = [client.categories.add({"name": parsed_args.name})] - - fields = ["id", "name"] - field_labels = ["ID", "Name"] - - return ( - field_labels, - list(utils.get_item_properties( - s, - fields, - ) for s in data) - ) - - -class DeleteCategory(command.Lister): - """Delete a category.""" - - def get_parser(self, prog_name): - parser = super(DeleteCategory, self).get_parser(prog_name) - parser.add_argument( - "id", - metavar="", - nargs="+", - help=("ID of a category(ies) to delete."), - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - failure_count = 0 - for category_id in parsed_args.id: - try: - client.categories.delete(category_id) - except Exception: - failure_count += 1 - print("Failed to delete '{0}'; category not found". - format(category_id)) - if failure_count == len(parsed_args.id): - raise exceptions.CommandError("Unable to find and delete any of " - "the specified categories.") - data = client.categories.list() - - fields = ["id", "name"] - field_labels = ["ID", "Name"] - - return ( - field_labels, - list(utils.get_item_properties( - s, - fields, - ) for s in data) - ) diff --git a/muranoclient/osc/v1/deployment.py b/muranoclient/osc/v1/deployment.py deleted file mode 100644 index f19f9eb1..00000000 --- a/muranoclient/osc/v1/deployment.py +++ /dev/null @@ -1,74 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Application-catalog v1 deployments action implementation""" - -from osc_lib.command import command -from osc_lib import utils -from oslo_log import log as logging - -from muranoclient.apiclient import exceptions - -LOG = logging.getLogger(__name__) - - -class ListDeployment(command.Lister): - """List deployments for an environment.""" - - def get_parser(self, prog_name): - parser = super(ListDeployment, self).get_parser(prog_name) - parser.add_argument( - "id", - metavar="", - nargs="?", - default=None, - help=("Environment ID for which to list deployments."), - ) - parser.add_argument( - "--all-environments", - action="store_true", - default=False, - help="List all deployments for all environments in user's tenant." - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - all_environments = getattr(parsed_args, 'all_environments', False) - env_id = getattr(parsed_args, 'id', None) - - if env_id and all_environments: - raise exceptions.CommandError( - 'Environment ID and all-environments flag cannot both be set.') - elif not env_id and not all_environments: - raise exceptions.CommandError( - 'Either environment ID or all-environments flag must be set.') - - if all_environments: - data = client.deployments.list(None, all_environments) - else: - environment = utils.find_resource(client.environments, - env_id) - data = client.deployments.list(environment.id) - - columns = ('id', 'state', 'created', 'updated', 'finished') - column_headers = [c.capitalize() for c in columns] - return ( - column_headers, - list(utils.get_item_properties( - s, - columns, - ) for s in data) - ) diff --git a/muranoclient/osc/v1/environment.py b/muranoclient/osc/v1/environment.py deleted file mode 100644 index 74094c70..00000000 --- a/muranoclient/osc/v1/environment.py +++ /dev/null @@ -1,494 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Application-catalog v1 stack action implementation""" - -import json -import sys -import urllib - -import jsonpatch -from osc_lib.command import command -from osc_lib import utils -from oslo_log import log as logging -from oslo_serialization import jsonutils -from oslo_utils import uuidutils - -from muranoclient.apiclient import exceptions -from muranoclient.common import utils as murano_utils - - -LOG = logging.getLogger(__name__) - - -class ListEnvironments(command.Lister): - """Lists environments""" - - def get_parser(self, prog_name): - parser = super(ListEnvironments, self).get_parser(prog_name) - parser.add_argument( - '--all-tenants', - action='store_true', - default=False, - help='List environments from all tenants (admin only).', - ) - parser.add_argument( - '--tenant', - metavar='', - default=None, - help='Allows to list environments for a given tenant (admin only).' - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - data = client.environments.list( - parsed_args.all_tenants, parsed_args.tenant) - - columns = ('id', 'name', 'status', 'created', 'updated') - column_headers = [c.capitalize() for c in columns] - return ( - column_headers, - list(utils.get_item_properties( - s, - columns, - ) for s in data) - ) - - -class ShowEnvironment(command.ShowOne): - """Display environment details""" - - def get_parser(self, prog_name): - parser = super(ShowEnvironment, self).get_parser(prog_name) - parser.add_argument( - "id", - metavar="", - help=("Name or ID of the environment to display"), - ) - parser.add_argument( - "--only-apps", - action='store_true', - default=False, - help="Only print apps of the environment (useful for automation).", - ) - parser.add_argument( - "--session-id", - metavar="", - default='', - help="Id of a config session.", - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - environment = utils.find_resource(client.environments, - parsed_args.id) - data = client.environments.get(environment.id, - parsed_args.session_id).to_dict() - - data['services'] = jsonutils.dumps(data['services'], indent=2) - - if getattr(parsed_args, 'only_apps', False): - return(['services'], [data['services']]) - else: - return self.dict2columns(data) - - -class RenameEnvironment(command.Lister): - """Rename an environment.""" - - def get_parser(self, prog_name): - parser = super(RenameEnvironment, self).get_parser(prog_name) - parser.add_argument( - 'id', - metavar="", - help="Environment ID or name.", - ) - parser.add_argument( - 'name', - metavar="", - help="A name to which the environment will be renamed.", - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - environment = utils.find_resource(client.environments, - parsed_args.id) - data = client.environments.update(environment.id, - parsed_args.name) - - columns = ('id', 'name', 'status', 'created', 'updated') - column_headers = [c.capitalize() for c in columns] - - return ( - column_headers, - [utils.get_item_properties( - data, - columns, - )] - ) - - -class EnvironmentSessionCreate(command.ShowOne): - """Creates a new configuration session for environment ID.""" - - def get_parser(self, prog_name): - parser = super(EnvironmentSessionCreate, self).get_parser(prog_name) - parser.add_argument( - 'id', - metavar="", - help="ID of Environment to add session to.", - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - environment_id = parsed_args.id - session_id = client.sessions.configure(environment_id).id - sessionid = murano_utils.text_wrap_formatter(session_id) - - return (['id'], [sessionid]) - - -class EnvironmentCreate(command.Lister): - """Create an environment.""" - - def get_parser(self, prog_name): - parser = super(EnvironmentCreate, self).get_parser(prog_name) - parser.add_argument( - 'name', - metavar="", - help="Environment name.", - ) - parser.add_argument( - '--region', - metavar="", - help="Name of the target OpenStack region.", - ) - parser.add_argument( - '--join-subnet-id', - metavar="", - help="Subnetwork id to join.", - ) - parser.add_argument( - '--join-net-id', - metavar="", - help="Network id to join.", - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - body = {"name": parsed_args.name, "region": parsed_args.region} - if parsed_args.join_net_id or parsed_args.join_subnet_id: - res = { - 'defaultNetworks': { - 'environment': { - '?': { - 'id': uuidutils.generate_uuid(dashed=False), - 'type': - 'io.murano.resources.ExistingNeutronNetwork' - }, - }, - 'flat': None - } - } - if parsed_args.join_net_id: - res['defaultNetworks']['environment']['internalNetworkName'] \ - = parsed_args.join_net_id - if parsed_args.join_subnet_id: - res['defaultNetworks']['environment']['internalSubnetworkName' - ] = \ - parsed_args.join_subnet_id - - body.update(res) - - data = client.environments.create(body) - - columns = ('id', 'name', 'status', 'created', 'updated') - column_headers = [c.capitalize() for c in columns] - - return ( - column_headers, - [utils.get_item_properties( - data, - columns, - )] - ) - - -class EnvironmentDelete(command.Lister): - """Delete an environment.""" - - def get_parser(self, prog_name): - parser = super(EnvironmentDelete, self).get_parser(prog_name) - parser.add_argument( - 'id', - metavar="", - nargs="+", - help="Id or name of environment(s) to delete.", - ) - parser.add_argument( - '--abandon', - action='store_true', - default=False, - help="If set will abandon environment without deleting any of its" - " resources.", - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - abandon = getattr(parsed_args, 'abandon', False) - failure_count = 0 - for environment_id in parsed_args.id: - try: - environment = murano_utils.find_resource(client.environments, - environment_id) - client.environments.delete(environment.id, abandon) - except exceptions.NotFound: - failure_count += 1 - print("Failed to delete '{0}'; environment not found". - format(environment_id)) - - if failure_count == len(parsed_args.id): - raise exceptions.CommandError("Unable to find and delete any of " - "the specified environments.") - - data = client.environments.list() - - columns = ('id', 'name', 'status', 'created', 'updated') - column_headers = [c.capitalize() for c in columns] - - return ( - column_headers, - list(utils.get_item_properties( - s, - columns, - ) for s in data) - ) - - -class EnvironmentDeploy(command.ShowOne): - """Start deployment of a murano environment session.""" - - def get_parser(self, prog_name): - parser = super(EnvironmentDeploy, self).get_parser(prog_name) - parser.add_argument( - 'id', - metavar="", - help="ID of Environment to deploy.", - ) - parser.add_argument( - '--session-id', - metavar="", - help="ID of configuration session to deploy.", - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - client.sessions.deploy(parsed_args.id, parsed_args.session_id) - - environment = utils.find_resource(client.environments, - parsed_args.id) - data = client.environments.get(environment.id, - parsed_args.session_id).to_dict() - - data['services'] = jsonutils.dumps(data['services'], indent=2) - - if getattr(parsed_args, 'only_apps', False): - return(['services'], [data['services']]) - else: - return self.dict2columns(data) - - -class EnvironmentAppsEdit(command.Command): - """Edit environment's services list. - - `FILE` is path to a file, that contains jsonpatch, that describes changes - to be made to environment's object-model. - - [ - { "op": "add", "path": "/-", - "value": { ... your-app object model here ... } - }, - { "op": "replace", "path": "/0/?/name", - "value": "new_name" - }, - ] - - NOTE: Values '===id1===', '===id2===', etc. in the resulting object-model - will be substituted with uuids. - - For more info on jsonpatch see RFC 6902 - """ - - def get_parser(self, prog_name): - parser = super(EnvironmentAppsEdit, self).get_parser(prog_name) - parser.add_argument( - 'id', - metavar="", - help="ID of Environment to edit.", - ) - parser.add_argument( - 'filename', - metavar="", - help="File to read jsonpatch from (defaults to stdin).", - ) - parser.add_argument( - '--session-id', - metavar="", - help="ID of configuration session to edit.", - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action(%s)", parsed_args) - client = self.app.client_manager.application_catalog - jp_obj = None - if not parsed_args.filename: - jp_obj = json.load(sys.stdin) - else: - with open(parsed_args.filename) as fpatch: - jp_obj = json.load(fpatch) - - jpatch = jsonpatch.JsonPatch(jp_obj) - environment_id = parsed_args.id - session_id = parsed_args.session_id - environment = client.environments.get(environment_id, session_id) - - object_model = jpatch.apply(environment.services) - murano_utils.traverse_and_replace(object_model) - - client.services.put( - environment_id, - path='/', - data=jpatch.apply(environment.services), - session_id=session_id) - - -class EnvironmentModelShow(command.ShowOne): - """Show environment's object model.""" - - def get_parser(self, prog_name): - parser = super(EnvironmentModelShow, self).get_parser(prog_name) - parser.add_argument( - 'id', - metavar="", - help="ID of Environment to show.", - ) - parser.add_argument( - "--path", - metavar="", - default='/', - help="Path to Environment model section. Defaults to '/'." - ) - parser.add_argument( - '--session-id', - metavar="", - help="Id of a config session.", - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action(%s)", parsed_args) - client = self.app.client_manager.application_catalog - - session_id = parsed_args.session_id or None - path = urllib.parse.quote(parsed_args.path) - env_model = client.environments.get_model(parsed_args.id, path, - session_id) - - return self.dict2columns(env_model) - - -class EnvironmentModelEdit(command.ShowOne): - """Edit environment's object model.""" - - def get_parser(self, prog_name): - parser = super(EnvironmentModelEdit, self).get_parser(prog_name) - parser.add_argument( - 'id', - metavar="", - help="ID of Environment to edit.", - ) - parser.add_argument( - "filename", - metavar="", - nargs="?", - help="File to read JSON-patch from (defaults to stdin)." - ) - parser.add_argument( - '--session-id', - metavar="", - help="Id of a config session.", - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action(%s)", parsed_args) - client = self.app.client_manager.application_catalog - - jp_obj = None - if not parsed_args.filename: - jp_obj = json.load(sys.stdin) - else: - with open(parsed_args.filename) as fpatch: - jp_obj = json.load(fpatch) - - if not isinstance(jp_obj, list): - raise exceptions.CommandError( - 'JSON-patch must be a list of changes') - for change in jp_obj: - if 'op' not in change or 'path' not in change: - raise exceptions.CommandError( - 'Every change in JSON-patch must contain "op" and "path" ' - 'keys') - op = change['op'] - if op not in ['add', 'replace', 'remove']: - raise exceptions.CommandError('The value of "op" item must be ' - '"add", "replace" or "remove", ' - 'got {0}'.format(op)) - if op != 'remove' and 'value' not in change: - raise exceptions.CommandError('"add" or "replace" change in ' - 'JSON-patch must contain "value"' - ' key') - session_id = parsed_args.session_id - new_model = client.environments.update_model(parsed_args.id, jp_obj, - session_id) - - return self.dict2columns(new_model) diff --git a/muranoclient/osc/v1/package.py b/muranoclient/osc/v1/package.py deleted file mode 100644 index 718d8ca6..00000000 --- a/muranoclient/osc/v1/package.py +++ /dev/null @@ -1,797 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Application-catalog v1 package action implementation""" - -import collections -import functools -import itertools -import os -import shutil -import sys -import tempfile -import zipfile - -from osc_lib.command import command -from osc_lib import exceptions as exc -from osc_lib import utils -from oslo_log import log as logging -from oslo_serialization import jsonutils -from oslo_utils import strutils - -from muranoclient.apiclient import exceptions -from muranoclient.common import exceptions as common_exceptions -from muranoclient.common import utils as murano_utils -from muranoclient.v1.package_creator import hot_package -from muranoclient.v1.package_creator import mpl_package - - -LOG = logging.getLogger(__name__) - -DEFAULT_REPO_URL = "http://apps.openstack.org/api/v1/murano_repo/liberty/" - -_bool_from_str_strict = functools.partial( - strutils.bool_from_string, strict=True) - - -class CreatePackage(command.Command): - """Create an application package.""" - - def get_parser(self, prog_name): - parser = super(CreatePackage, self).get_parser(prog_name) - parser.add_argument( - '-t', '--template', - metavar='', - help=("Path to the Heat template to import as " - "an Application Definition."), - ) - parser.add_argument( - '-c', '--classes-dir', - metavar='', - help=("Path to the directory containing application classes."), - ) - parser.add_argument( - '-r', '--resources-dir', - metavar='', - help=("Path to the directory containing application resources."), - ) - parser.add_argument( - '-n', '--name', - metavar='', - help=("Display name of the Application in Catalog."), - ) - parser.add_argument( - '-f', '--full-name', - metavar='', - help=("Fully-qualified name of the Application in Catalog."), - ) - parser.add_argument( - '-a', '--author', - metavar='', - help=("Name of the publisher."), - ) - parser.add_argument( - '--tags', - metavar='', - nargs='*', - help=("A list of keywords connected to the application."), - ) - parser.add_argument( - '-d', '--description', - metavar='', - help=("Detailed description for the Application in Catalog."), - ) - parser.add_argument( - '-o', '--output', - metavar='', - help=("The name of the output file archive to save locally."), - ) - parser.add_argument( - '-u', '--ui', - metavar='', - help=("Dynamic UI form definition."), - ) - parser.add_argument( - '--type', - metavar='', - help=("Package type. Possible values: Application or Library."), - ) - parser.add_argument( - '-l', '--logo', - metavar='', - help=("Path to the package logo."), - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - parsed_args.os_username = os.getenv('OS_USERNAME') - - def _make_archive(archive_name, path): - zip_file = zipfile.ZipFile(archive_name, 'w') - for root, dirs, files in os.walk(path): - for f in files: - zip_file.write(os.path.join(root, f), - arcname=os.path.join( - os.path.relpath(root, path), f)) - - if parsed_args.template and parsed_args.classes_dir: - raise exc.CommandError( - "Provide --template for a HOT-based package, OR" - " --classes-dir for a MuranoPL-based package") - if not parsed_args.template and not parsed_args.classes_dir: - raise exc.CommandError( - "Provide --template for a HOT-based package, OR at least" - " --classes-dir for a MuranoPL-based package") - directory_path = None - try: - archive_name = parsed_args.output if parsed_args.output else None - if parsed_args.template: - directory_path = hot_package.prepare_package(parsed_args) - if not archive_name: - archive_name = os.path.basename(parsed_args.template) - archive_name = os.path.splitext(archive_name)[0] + ".zip" - else: - directory_path = mpl_package.prepare_package(parsed_args) - if not archive_name: - archive_name = tempfile.mkstemp( - prefix="murano_", dir=os.getcwd())[1] + ".zip" - - _make_archive(archive_name, directory_path) - print("Application package is available at " + - os.path.abspath(archive_name)) - finally: - if directory_path: - shutil.rmtree(directory_path) - - -class ListPackages(command.Lister): - """List available packages.""" - - def get_parser(self, prog_name): - parser = super(ListPackages, self).get_parser(prog_name) - parser.add_argument( - "--limit", - type=int, - default=0, - help='Show limited number of packages' - ) - parser.add_argument( - "--marker", - default='', - help='Show packages starting from package with id excluding it' - ) - parser.add_argument( - "--include-disabled", - default=False, - action="store_true" - ) - parser.add_argument( - "--owned", - default=False, - action="store_true" - ) - parser.add_argument( - '--search', - metavar='', - dest='search', - required=False, - help='Show packages, that match search keys fuzzily' - ) - parser.add_argument( - '--name', - metavar='', - dest='name', - required=False, - help='Show packages, whose name match parameter exactly' - ) - parser.add_argument( - '--fqn', - metavar="", - dest='fqn', - required=False, - help='Show packages, ' - 'whose fully qualified name match parameter exactly' - ) - parser.add_argument( - '--type', - metavar='', - dest='type', - required=False, - help='Show packages, whose type match parameter exactly' - ) - parser.add_argument( - '--category', - metavar='', - dest='category', - required=False, - help='Show packages, whose categories include parameter' - ) - parser.add_argument( - '--class_name', - metavar='', - dest='class_name', - required=False, - help='Show packages, whose class name match parameter exactly' - ) - parser.add_argument( - '--tag', - metavar='', - dest='tag', - required=False, - help='Show packages, whose tags include parameter' - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - filter_args = { - "include_disabled": getattr(parsed_args, - 'include_disabled', False), - "owned": getattr(parsed_args, 'owned', False), - } - if parsed_args: - if parsed_args.limit < 0: - raise exceptions.CommandError( - '--limit parameter must be non-negative') - if parsed_args.limit != 0: - filter_args['limit'] = parsed_args.limit - if parsed_args.marker: - filter_args['marker'] = parsed_args.marker - if parsed_args.search: - filter_args['search'] = parsed_args.search - if parsed_args.name: - filter_args['name'] = parsed_args.name - if parsed_args.fqn: - filter_args['fqn'] = parsed_args.fqn - if parsed_args.type: - filter_args['type'] = parsed_args.type - if parsed_args.category: - filter_args['category'] = parsed_args.category - if parsed_args.class_name: - filter_args['class_name'] = parsed_args.class_name - if parsed_args.tag: - filter_args['tag'] = parsed_args.tag - - data = client.packages.filter(**filter_args) - - columns = ('id', 'name', 'fully_qualified_name', 'author', 'active', - 'is public', 'type', 'version') - column_headers = [c.capitalize() for c in columns] - if not parsed_args or parsed_args.limit == 0: - return ( - column_headers, - list(utils.get_item_properties( - s, - columns, - ) for s in data) - ) - else: - return ( - column_headers, - list(utils.get_item_properties( - s, - columns, - ) for s in itertools.islice(data, parsed_args.limit)) - ) - - -class DeletePackage(command.Lister): - """Delete a package.""" - - def get_parser(self, prog_name): - parser = super(DeletePackage, self).get_parser(prog_name) - parser.add_argument( - 'id', - metavar="", - nargs="+", - help="Package ID to delete.", - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - - client = self.app.client_manager.application_catalog - - failure_count = 0 - for package_id in parsed_args.id: - try: - client.packages.delete(package_id) - except exceptions.NotFound: - failure_count += 1 - print("Failed to delete '{0}'; package not found". - format(package_id)) - - if failure_count == len(parsed_args.id): - raise exceptions.CommandError("Unable to find and delete any of " - "the specified packages.") - data = client.packages.filter() - - columns = ('id', 'name', 'fully_qualified_name', 'author', 'active', - 'is public', 'type', 'version') - column_headers = [c.capitalize() for c in columns] - - return ( - column_headers, - list(utils.get_item_properties( - s, - columns, - ) for s in data) - ) - - -def _handle_package_exists(mc, data, package, exists_action): - name = package.manifest['FullName'] - version = package.manifest.get('Version', '0') - while True: - print("Importing package {0}".format(name)) - try: - return mc.packages.create(data, {name: package.file()}) - except common_exceptions.HTTPConflict: - print("Importing package {0} failed. Package with the same" - " name/classes is already registered.".format(name)) - allowed_results = ['s', 'u', 'a'] - res = exists_action - if not res: - while True: - print("What do you want to do? (s)kip, (u)pdate, (a)bort") - res = input() - if res in allowed_results: - break - if res == 's': - print("Skipping.") - return None - elif res == 'a': - print("Exiting.") - sys.exit() - elif res == 'u': - pkgs = list(mc.packages.filter(fqn=name, version=version, - owned=True)) - if not pkgs: - msg = ( - "Got a conflict response, but could not find the " - "package '{0}' in the current tenant.\nThis probably " - "means the conflicting package is in another tenant.\n" - "Please delete it manually." - ).format(name) - raise exceptions.CommandError(msg) - elif len(pkgs) > 1: - msg = ( - "Got {0} packages with name '{1}'.\nI do not trust " - "myself, please delete the package manually." - ).format(len(pkgs), name) - raise exceptions.CommandError(msg) - print("Deleting package {0}({1})".format(name, pkgs[0].id)) - mc.packages.delete(pkgs[0].id) - continue - - -class ImportPackage(command.Lister): - """Import a package.""" - - def get_parser(self, prog_name): - parser = super(ImportPackage, self).get_parser(prog_name) - parser.add_argument( - 'filename', - metavar='', - nargs='+', - help='URL of the murano zip package, FQPN, path to zip package' - ' or path to directory with package.' - ) - parser.add_argument( - '--categories', - metavar='', - nargs='*', - help='Category list to attach.', - ) - parser.add_argument( - '--is-public', - action='store_true', - default=False, - help="Make the package available for users from other tenants.", - ) - parser.add_argument( - '--package-version', - default='', - help='Version of the package to use from repository ' - '(ignored when importing with multiple packages).' - ) - parser.add_argument( - '--exists-action', - default='', - choices=['a', 's', 'u'], - help='Default action when a package already exists: ' - '(s)kip, (u)pdate, (a)bort.' - ) - parser.add_argument( - '--dep-exists-action', - default='', - choices=['a', 's', 'u'], - help='Default action when a dependency package already exists: ' - '(s)kip, (u)pdate, (a)bort.' - ) - parser.add_argument('--murano-repo-url', - default=murano_utils.env( - 'MURANO_REPO_URL', - default=DEFAULT_REPO_URL), - help=('Defaults to env[MURANO_REPO_URL] ' - 'or {0}'.format(DEFAULT_REPO_URL))) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - - client = self.app.client_manager.application_catalog - - data = {"is_public": parsed_args.is_public} - version = parsed_args.package_version - if version and len(parsed_args.filename) >= 2: - print("Requested to import more than one package, " - "ignoring version.") - version = '' - - if parsed_args.categories: - data["categories"] = parsed_args.categories - - total_reqs = collections.OrderedDict() - main_packages_names = [] - for filename in parsed_args.filename: - if os.path.isfile(filename) or os.path.isdir(filename): - _file = filename - else: - print("Package file '{0}' does not exist, attempting to " - "download".format(filename)) - _file = murano_utils.to_url( - filename, - version=version, - base_url=parsed_args.murano_repo_url, - extension='.zip', - path='apps/', - ) - try: - package = murano_utils.Package.from_file(_file) - except Exception as e: - print("Failed to create package for '{0}', reason: {1}".format( - filename, e)) - continue - total_reqs.update( - package.requirements(base_url=parsed_args.murano_repo_url)) - main_packages_names.append(package.manifest['FullName']) - - imported_list = [] - - dep_exists_action = parsed_args.dep_exists_action - if dep_exists_action == '': - dep_exists_action = parsed_args.exists_action - - for name, package in iter(total_reqs.items()): - image_specs = package.images() - if image_specs: - print("Inspecting required images") - try: - imgs = murano_utils.ensure_images( - glance_client=client.glance_client, - image_specs=image_specs, - base_url=parsed_args.murano_repo_url, - is_package_public=parsed_args.is_public) - for img in imgs: - print("Added {0}, {1} image".format( - img['name'], img['id'])) - except Exception as e: - print("Error {0} occurred while installing " - "images for {1}".format(e, name)) - - if name in main_packages_names: - exists_action = parsed_args.exists_action - else: - exists_action = dep_exists_action - try: - imported_package = _handle_package_exists( - client, data, package, exists_action) - if imported_package: - imported_list.append(imported_package) - except Exception as e: - print("Error {0} occurred while installing package {1}".format( - e, name)) - - columns = ('id', 'name', 'fully_qualified_name', 'author', 'active', - 'is public', 'type', 'version') - column_headers = [c.capitalize() for c in columns] - - return ( - column_headers, - list(utils.get_item_properties( - s, - columns, - ) for s in imported_list) - ) - - -class ImportBundle(command.Lister): - """Import a bundle.""" - - def get_parser(self, prog_name): - parser = super(ImportBundle, self).get_parser(prog_name) - parser.add_argument( - 'filename', - metavar='', - nargs='+', - help='Bundle URL, bundle name, or path to the bundle file.' - ) - parser.add_argument( - '--is-public', - action='store_true', - default=False, - help="Make the package available for users from other tenants.", - ) - parser.add_argument( - '--exists-action', - default='', - choices=['a', 's', 'u'], - help='Default action when a package already exists: ' - '(s)kip, (u)pdate, (a)bort.' - ) - parser.add_argument('--murano-repo-url', - default=murano_utils.env( - 'MURANO_REPO_URL', - default=DEFAULT_REPO_URL), - help=('Defaults to env[MURANO_REPO_URL] ' - 'or {0}'.format(DEFAULT_REPO_URL))) - - return parser - - def take_action(self, parsed_args): - - LOG.debug("take_action({0})".format(parsed_args)) - - client = self.app.client_manager.application_catalog - - total_reqs = collections.OrderedDict() - for filename in parsed_args.filename: - local_path = None - if os.path.isfile(filename): - _file = filename - local_path = os.path.dirname(os.path.abspath(filename)) - else: - print("Bundle file '{0}' does not exist, attempting " - "to download".format(filename)) - _file = murano_utils.to_url( - filename, - base_url=parsed_args.murano_repo_url, - path='bundles/', - extension='.bundle', - ) - - try: - bundle_file = murano_utils.Bundle.from_file(_file) - except Exception as e: - print("Failed to create bundle for '{0}', reason: {1}".format( - filename, e)) - continue - - data = {"is_public": parsed_args.is_public} - - for package in bundle_file.packages( - base_url=parsed_args.murano_repo_url, path=local_path): - - requirements = package.requirements( - base_url=parsed_args.murano_repo_url, - path=local_path, - ) - total_reqs.update(requirements) - - imported_list = [] - - for name, dep_package in total_reqs.items(): - image_specs = dep_package.images() - if image_specs: - print("Inspecting required images") - try: - imgs = parsed_args.ensure_images( - glance_client=client.glance_client, - image_specs=image_specs, - base_url=parsed_args.murano_repo_url, - local_path=local_path, - is_package_public=parsed_args.is_public) - for img in imgs: - print("Added {0}, {1} image".format( - img['name'], img['id'])) - except Exception as e: - print("Error {0} occurred while installing " - "images for {1}".format(e, name)) - try: - imported_package = _handle_package_exists( - client, data, dep_package, parsed_args.exists_action) - if imported_package: - imported_list.append(imported_package) - except exceptions.CommandError: - raise - except Exception as e: - print("Error {0} occurred while " - "installing package {1}".format(e, name)) - - columns = ('id', 'name', 'fully_qualified_name', 'author', 'active', - 'is public', 'type', 'version') - column_headers = [c.capitalize() for c in columns] - - return ( - column_headers, - list(utils.get_item_properties( - s, - columns, - ) for s in imported_list) - ) - - -class ShowPackage(command.ShowOne): - """Display details for a package.""" - - def get_parser(self, prog_name): - parser = super(ShowPackage, self).get_parser(prog_name) - parser.add_argument( - "id", - metavar="", - help=("Package ID to show."), - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - try: - package = client.packages.get(parsed_args.id) - except common_exceptions.HTTPNotFound: - raise exceptions.CommandError("Package with id %s not " - "found" % parsed_args.id) - else: - to_display = dict( - id=package.id, - type=package.type, - owner_id=package.owner_id, - name=package.name, - fully_qualified_name=package.fully_qualified_name, - is_public=package.is_public, - enabled=package.enabled, - class_definitions=jsonutils.dumps(package.class_definitions, - indent=2), - categories=jsonutils.dumps(package.categories, indent=2), - tags=jsonutils.dumps(package.tags, indent=2), - description=package.description - ) - - return self.dict2columns(to_display) - - -class UpdatePackage(command.ShowOne): - """Update an existing package.""" - - def get_parser(self, prog_name): - parser = super(UpdatePackage, self).get_parser(prog_name) - parser.add_argument( - 'id', - metavar="", - help="Package ID to update.", - ) - parser.add_argument( - '--is-public', - type=_bool_from_str_strict, - metavar="{true|false}", - help="Make package available to users from other tenants.", - ) - parser.add_argument( - '--enabled', - type=_bool_from_str_strict, - metavar="{true|false}", - help="Make package active and available for deployments.", - ) - parser.add_argument( - '--name', - default=None, - help="New name for the package.", - ) - parser.add_argument( - '--description', - default=None, - help="New package description.", - ) - parser.add_argument( - '--tags', - metavar='', nargs='*', - default=None, - help="A list of keywords connected to the application.", - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - data = {} - parameters = ('is_public', 'enabled', - 'name', 'description', - 'tags') - for parameter in parameters: - param_value = getattr(parsed_args, parameter, None) - if param_value is not None: - data[parameter] = param_value - - _, package = client.packages.update(parsed_args.id, data) - - to_display = dict( - id=package["id"], - type=package["type"], - owner_id=package["owner_id"], - name=package["name"], - fully_qualified_name=package["fully_qualified_name"], - is_public=package["is_public"], - enabled=package["enabled"], - class_definitions=jsonutils.dumps(package["class_definitions"], - indent=2), - categories=jsonutils.dumps(package["categories"], indent=2), - tags=jsonutils.dumps(package["tags"], indent=2), - description=package["description"] - ) - - return self.dict2columns(to_display) - - -class DownloadPackage(command.Command): - """Download a package to a filename or stdout.""" - - def get_parser(self, prog_name): - parser = super(DownloadPackage, self).get_parser(prog_name) - parser.add_argument( - "id", - metavar="", - help=("Package ID to download."), - ) - parser.add_argument( - "filename", - metavar="file", nargs="?", - help=("Filename to save package to. If it is not " - "specified and there is no stdout redirection " - "the package won't be saved."), - ) - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - - def download_to_fh(package_id, fh): - try: - fh.write(client.packages.download(package_id)) - except common_exceptions.HTTPNotFound: - raise exceptions.CommandError("Package with id %s not " - "found" % parsed_args.id) - - if parsed_args.filename: - with open(parsed_args.filename, 'wb') as fh: - download_to_fh(parsed_args.id, fh) - print("Package downloaded to %s" % parsed_args.filename) - elif not sys.stdout.isatty(): - download_to_fh(parsed_args.id, sys.stdout) - else: - msg = ("No stdout redirection or local file specified for " - "downloaded package. Please specify a local file to " - "save downloaded package or redirect output to " - "another source.") - raise exceptions.CommandError(msg) diff --git a/muranoclient/osc/v1/schema.py b/muranoclient/osc/v1/schema.py deleted file mode 100644 index 8dc4c127..00000000 --- a/muranoclient/osc/v1/schema.py +++ /dev/null @@ -1,52 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -"""Application-catalog v1 class-schema action implementation""" - -from osc_lib.command import command -from oslo_log import log as logging - -LOG = logging.getLogger(__name__) - - -class ShowSchema(command.ShowOne): - """Show class schema.""" - - def get_parser(self, prog_name): - parser = super(ShowSchema, self).get_parser(prog_name) - parser.add_argument( - "class_name", metavar="", help="Class FQN") - parser.add_argument( - "method_names", metavar="", - help="Method name", nargs='*') - parser.add_argument( - "--package-name", default=None, - help="FQN of the package where the class is located") - parser.add_argument( - "--class-version", default='=0', - help="Class version or version range (version spec)") - - return parser - - def take_action(self, parsed_args): - LOG.debug("take_action({0})".format(parsed_args)) - client = self.app.client_manager.application_catalog - schema = client.schemas.get( - parsed_args.class_name, parsed_args.method_names, - class_version=parsed_args.class_version, - package_name=parsed_args.package_name) - - return self.dict2columns(schema.data) - - @property - def formatter_default(self): - return 'json' diff --git a/muranoclient/shell.py b/muranoclient/shell.py deleted file mode 100644 index 47cfbf45..00000000 --- a/muranoclient/shell.py +++ /dev/null @@ -1,530 +0,0 @@ -# Copyright (c) 2013 Mirantis, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -""" -Command-line interface to the Murano Project. -""" - -import argparse -import sys - -import glanceclient -from keystoneclient.auth.identity.generic.cli import DefaultCLI -from keystoneclient.auth.identity import v3 as identity -from keystoneclient import discover -from keystoneclient import exceptions as ks_exc -from keystoneclient import session as ksession -from oslo_log import handlers -from oslo_log import log as logging -from oslo_utils import encodeutils -from oslo_utils import importutils -import urllib.parse as urlparse - -import muranoclient -from muranoclient.apiclient import exceptions as exc -from muranoclient import client as murano_client -from muranoclient.common import utils -from muranoclient.glance import client as art_client - - -logger = logging.getLogger(__name__) - -DEFAULT_REPO_URL = "http://apps.openstack.org/api/v1/murano_repo/liberty/" - - -# quick local fix for keystoneclient bug which blocks built-in reauth -# functionality in case of expired token. -# bug: https://bugs.launchpad.net/python-keystoneclient/+bug/1551392 -# fix: https://review.opendev.org/#/c/286236/ -class AuthCLI(DefaultCLI): - def invalidate(self): - retval = super(AuthCLI, self).invalidate() - if self._token: - self._token = None - retval = True - return retval - - -class MuranoShell(object): - - def _append_global_identity_args(self, parser): - # Register the CLI arguments that have moved to the session object. - ksession.Session.register_cli_options(parser) - - identity.Password.register_argparse_arguments(parser) - - def get_base_parser(self, argv): - - parser = argparse.ArgumentParser( - prog='murano', - description=__doc__.strip(), - epilog='See "murano help COMMAND" ' - 'for help on a specific command.', - add_help=False, - formatter_class=HelpFormatter, - ) - - # Global arguments - parser.add_argument('-h', '--help', - action='store_true', - help=argparse.SUPPRESS, ) - - parser.add_argument('--version', - action='version', - version=muranoclient.__version__, - help="Show program's version number and exit.") - - parser.add_argument('-d', '--debug', - default=bool(utils.env('MURANOCLIENT_DEBUG')), - action='store_true', - help='Defaults to env[MURANOCLIENT_DEBUG].') - - parser.add_argument('-v', '--verbose', - default=False, action="store_true", - help="Print more verbose output.") - - parser.add_argument('--api-timeout', - help='Number of seconds to wait for an ' - 'API response, ' - 'defaults to system socket timeout.') - - parser.add_argument('--os-tenant-id', - default=utils.env('OS_TENANT_ID'), - help='Defaults to env[OS_TENANT_ID].') - - parser.add_argument('--os-tenant-name', - default=utils.env('OS_TENANT_NAME'), - help='Defaults to env[OS_TENANT_NAME].') - - parser.add_argument('--os-region-name', - default=utils.env('OS_REGION_NAME'), - help='Defaults to env[OS_REGION_NAME].') - - parser.add_argument('--os-auth-token', - default=utils.env('OS_AUTH_TOKEN'), - help='Defaults to env[OS_AUTH_TOKEN].') - - parser.add_argument('--os-no-client-auth', - default=utils.env('OS_NO_CLIENT_AUTH'), - action='store_true', - help="Do not contact keystone for a token. " - "Defaults to env[OS_NO_CLIENT_AUTH].") - parser.add_argument('--murano-url', - default=utils.env('MURANO_URL'), - help='Defaults to env[MURANO_URL].') - - parser.add_argument('--glance-url', - default=utils.env('GLANCE_URL'), - help='Defaults to env[GLANCE_URL].') - - parser.add_argument('--glare-url', - default=utils.env('GLARE_URL'), - help='Defaults to env[GLARE_URL].') - - parser.add_argument('--murano-api-version', - default=utils.env( - 'MURANO_API_VERSION', default='1'), - help='Defaults to env[MURANO_API_VERSION] ' - 'or 1.') - - parser.add_argument('--os-service-type', - default=utils.env('OS_SERVICE_TYPE'), - help='Defaults to env[OS_SERVICE_TYPE].') - - parser.add_argument('--os-endpoint-type', - default=utils.env('OS_ENDPOINT_TYPE'), - help='Defaults to env[OS_ENDPOINT_TYPE].') - - parser.add_argument('--include-password', - default=bool(utils.env('MURANO_INCLUDE_PASSWORD')), - action='store_true', - help='Send os-username and os-password to murano.') - - parser.add_argument('--murano-repo-url', - default=utils.env( - 'MURANO_REPO_URL', - default=DEFAULT_REPO_URL), - help=('Defaults to env[MURANO_REPO_URL] ' - 'or {0}'.format(DEFAULT_REPO_URL))) - - parser.add_argument('--murano-packages-service', - choices=['murano', 'glance', 'glare'], - default=utils.env('MURANO_PACKAGES_SERVICE', - default='murano'), - help='Specifies if murano-api ("murano") or ' - 'Glance Artifact Repository ("glare") ' - 'should be used to store murano packages. ' - 'Defaults to env[MURANO_PACKAGES_SERVICE] or ' - 'to "murano"') - - # The following 3 arguments are deprecated and are all added - # by keystone session register_cli_opts later. Only add these - # arguments if they are present on the command line. - - if '--cert-file' in argv: - parser.add_argument('--cert-file', - dest='os_cert', - help='DEPRECATED! Use --os-cert.') - - if '--key-file' in argv: - parser.add_argument('--key-file', - dest='os_key', - help='DEPRECATED! Use --os-key.') - - if '--ca-file' in argv: - parser.add_argument('--ca-file', - dest='os_cacert', - help='DEPRECATED! Use --os-cacert.') - - self._append_global_identity_args(parser) - - return parser - - def get_subcommand_parser(self, version, argv): - parser = self.get_base_parser(argv) - - self.subcommands = {} - subparsers = parser.add_subparsers(metavar='') - submodule = importutils.import_versioned_module('muranoclient', - version, 'shell') - self._find_actions(subparsers, submodule) - self._find_actions(subparsers, self) - - return parser - - def _find_actions(self, subparsers, actions_module): - for attr in (a for a in dir(actions_module) if a.startswith('do_')): - # I prefer to be hypen-separated instead of underscores. - command = attr[3:].replace('_', '-') - callback = getattr(actions_module, attr) - desc = callback.__doc__ or '' - help = desc.strip().split('\n')[0] - arguments = getattr(callback, 'arguments', []) - - subparser = subparsers.add_parser(command, help=help, - description=desc, - add_help=False, - formatter_class=HelpFormatter) - subparser.add_argument('-h', '--help', action='help', - help=argparse.SUPPRESS) - self.subcommands[command] = subparser - for (args, kwargs) in arguments: - subparser.add_argument(*args, **kwargs) - subparser.set_defaults(func=callback) - - def _discover_auth_versions(self, session, auth_url): - # discover the API versions the server is supporting base on the - # given URL - v2_auth_url = None - v3_auth_url = None - try: - ks_discover = discover.Discover(session=session, auth_url=auth_url) - v2_auth_url = ks_discover.url_for('2.0') - v3_auth_url = ks_discover.url_for('3.0') - except ks_exc.ClientException as e: - # Identity service may not support discover API version. - # Lets trying to figure out the API version from the original URL. - url_parts = urlparse.urlparse(auth_url) - (scheme, netloc, path, params, query, fragment) = url_parts - path = path.lower() - if path.startswith('/v3'): - v3_auth_url = auth_url - elif path.startswith('/v2'): - v2_auth_url = auth_url - else: - # not enough information to determine the auth version - msg = ('Unable to determine the Keystone version ' - 'to authenticate with using the given ' - 'auth_url. Identity service may not support API ' - 'version discovery. Please provide a versioned ' - 'auth_url instead. error=%s') % (e) - raise exc.CommandError(msg) - - return (v2_auth_url, v3_auth_url) - - def _setup_logging(self, debug): - # Output the logs to command-line interface - color_handler = handlers.ColorHandler(sys.stdout) - logger_root = logging.getLogger(None).logger - logger_root.level = logging.DEBUG if debug else logging.WARNING - logger_root.addHandler(color_handler) - - # Set the logger level of special library - logging.getLogger('iso8601') \ - .logger.setLevel(logging.WARNING) - logging.getLogger('urllib3.connectionpool') \ - .logger.setLevel(logging.WARNING) - - def main(self, argv): - # Parse args once to find version - parser = self.get_base_parser(argv) - (options, args) = parser.parse_known_args(argv) - self._setup_logging(options.debug) - - # build available subcommands based on version - api_version = options.murano_api_version - subcommand_parser = self.get_subcommand_parser(api_version, argv) - self.parser = subcommand_parser - - keystone_session = None - keystone_auth = None - - # Handle top-level --help/-h before attempting to parse - # a command off the command line. - if (not args and options.help) or not argv: - self.do_help(options) - return 0 - - # Parse args again and call whatever callback was selected. - args = subcommand_parser.parse_args(argv) - - # Short-circuit and deal with help command right away. - if args.func == self.do_help: - self.do_help(args) - return 0 - elif args.func == self.do_bash_completion: - self.do_bash_completion(args) - return 0 - - if not args.os_username and not args.os_auth_token: - raise exc.CommandError("You must provide a username via" - " either --os-username or env[OS_USERNAME]" - " or a token via --os-auth-token or" - " env[OS_AUTH_TOKEN]") - - if args.murano_packages_service == 'glance': - args.murano_packages_service = 'glare' - - if args.os_no_client_auth: - if not args.murano_url: - raise exc.CommandError( - "If you specify --os-no-client-auth" - " you must also specify a Murano API URL" - " via either --murano-url or env[MURANO_URL]") - if (not args.glare_url and - args.murano_packages_service == 'glare'): - raise exc.CommandError( - "If you specify --os-no-client-auth and" - " set murano-packages-service to 'glare'" - " you must also specify a glare API URL" - " via either --glare-url or env[GLARE_API]") - if (not any([args.os_tenant_id, args.os_project_id]) and - args.murano_packages_service == 'glare'): - # TODO(kzaitsev): see if we can use project's name here - # NOTE(kzaitsev): glare v0.1 needs project_id to operate - # correctly - raise exc.CommandError( - "If you specify --os-no-client-auth and" - " set murano-packages-service to 'glare'" - " you must also specify your project's id" - " via either --os-project-id or env[OS_PROJECT_ID] or" - " --os-tenant-id or env[OS_TENANT_ID]") - - else: - # Tenant name or ID is needed to make keystoneclient retrieve a - # service catalog, it's not required if os_no_client_auth is - # specified, neither is the auth URL. - if not any([args.os_tenant_name, args.os_tenant_id, - args.os_project_id, args.os_project_name]): - raise exc.CommandError("You must provide a project name or" - " project id via --os-project-name," - " --os-project-id, env[OS_PROJECT_ID]" - " or env[OS_PROJECT_NAME]. You may" - " use os-project and os-tenant" - " interchangeably.") - if not args.os_auth_url: - raise exc.CommandError("You must provide an auth url via" - " either --os-auth-url or via" - " env[OS_AUTH_URL]") - - endpoint_type = args.os_endpoint_type or 'publicURL' - endpoint = args.murano_url - glance_endpoint = args.glance_url - - if args.os_no_client_auth: - # Authenticate through murano, don't use session - kwargs = { - 'username': args.os_username, - 'password': args.os_password, - 'auth_token': args.os_auth_token, - 'auth_url': args.os_auth_url, - 'token': args.os_auth_token, - 'insecure': args.insecure, - 'timeout': args.api_timeout, - 'tenant': args.os_project_id or args.os_tenant_id, - } - glance_kwargs = kwargs.copy() - - if args.os_region_name: - kwargs['region_name'] = args.os_region_name - glance_kwargs['region_name'] = args.os_region_name - else: - # Create a keystone session and keystone auth - keystone_session = ksession.Session.load_from_cli_options(args) - - args.os_project_name = args.os_project_name or args.os_tenant_name - args.os_project_id = args.os_project_id or args.os_tenant_id - - # make args compatible with DefaultCLI/AuthCLI - args.os_token = args.os_auth_token - args.os_endpoint = '' - # avoid password prompt if no password given - args.os_password = args.os_password or '' - (v2_auth_url, v3_auth_url) = self._discover_auth_versions( - keystone_session, args.os_auth_url) - if v3_auth_url: - if (not args.os_user_domain_id and - not args.os_user_domain_name): - args.os_user_domain_name = 'default' - - if (not args.os_project_domain_id and - not args.os_project_domain_name): - args.os_project_domain_name = 'default' - - keystone_auth = AuthCLI.load_from_argparse_arguments(args) - - service_type = args.os_service_type or 'application-catalog' - - if not endpoint: - endpoint = keystone_auth.get_endpoint( - keystone_session, - service_type=service_type, - interface=endpoint_type, - region_name=args.os_region_name) - - kwargs = { - 'session': keystone_session, - 'auth': keystone_auth, - 'service_type': service_type, - 'region_name': args.os_region_name, - } - glance_kwargs = kwargs.copy() - - # glance doesn't need endpoint_type - kwargs['endpoint_type'] = endpoint_type - kwargs['tenant'] = keystone_auth.get_project_id(keystone_session) - - if args.api_timeout: - kwargs['timeout'] = args.api_timeout - - if not glance_endpoint: - try: - glance_endpoint = keystone_auth.get_endpoint( - keystone_session, - service_type='image', - interface=endpoint_type, - region_name=args.os_region_name) - except Exception: - pass - - glance_client = None - if glance_endpoint: - try: - # TODO(starodubcevna): switch back to glance APIv2 when it will - # be ready for use. - glance_client = glanceclient.Client( - '1', glance_endpoint, **glance_kwargs) - except Exception: - pass - if glance_client: - kwargs['glance_client'] = glance_client - else: - logger.warning("Could not initialize glance client. " - "Image creation will be unavailable.") - kwargs['glance_client'] = None - - if args.murano_packages_service == 'glare': - glare_endpoint = args.glare_url - - if not glare_endpoint: - # no glare_endpoint and we requested to store packages in glare - # let's check keystone - try: - glare_endpoint = keystone_auth.get_endpoint( - keystone_session, - service_type='artifact', - interface=endpoint_type, - region_name=args.os_region_name) - except Exception: - raise exc.CommandError( - "You set murano-packages-service to {}" - " but there is not 'artifact' endpoint in keystone" - " Either register one or specify endpoint " - " via either --glare-url or env[GLARE_API]".format( - args.murano_packages_service)) - - auth_token = \ - args.os_auth_token or keystone_auth.get_token(keystone_session) - - artifacts_client = art_client.Client(endpoint=glare_endpoint, - type_name='murano', - type_version=1, - token=auth_token, - insecure=args.insecure) - kwargs['artifacts_client'] = artifacts_client - - client = murano_client.Client(api_version, endpoint, **kwargs) - - args.func(client, args) - - def do_bash_completion(self, args): - """Prints all of the commands and options to stdout.""" - commands = set() - options = set() - for sc_str, sc in self.subcommands.items(): - commands.add(sc_str) - for option in list(sc._optionals._option_string_actions): - options.add(option) - - commands.remove('bash-completion') - print(' '.join(commands | options)) - - @utils.arg('command', metavar='', nargs='?', - help='Display help for ') - def do_help(self, args): - """Display help about this program or one of its subcommands.""" - if getattr(args, 'command', None): - if args.command in self.subcommands: - self.subcommands[args.command].print_help() - else: - msg = "'%s' is not a valid subcommand" - raise exc.CommandError(msg % args.command) - else: - self.parser.print_help() - - -class HelpFormatter(argparse.HelpFormatter): - def start_section(self, heading): - # Title-case the headings - heading = '%s%s' % (heading[0].upper(), heading[1:]) - super(HelpFormatter, self).start_section(heading) - - -def main(args=sys.argv[1:]): - try: - MuranoShell().main(args) - - except KeyboardInterrupt: - print('... terminating murano client', file=sys.stderr) - sys.exit(1) - except Exception as e: - if '--debug' in args or '-d' in args: - raise - else: - print(encodeutils.safe_encode(str(e)), file=sys.stderr) - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/muranoclient/tests/__init__.py b/muranoclient/tests/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/muranoclient/tests/functional/__init__.py b/muranoclient/tests/functional/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/muranoclient/tests/functional/cli/MockApp/Classes/mock_muranopl.yaml b/muranoclient/tests/functional/cli/MockApp/Classes/mock_muranopl.yaml deleted file mode 100644 index 2bb2ac58..00000000 --- a/muranoclient/tests/functional/cli/MockApp/Classes/mock_muranopl.yaml +++ /dev/null @@ -1,34 +0,0 @@ -Namespaces: - =: io.murano.apps - std: io.murano - -# Name: MockApp # Using Name from manifest.yaml - -Extends: std:Application - -Properties: - greeting: - Usage: Static - Contract: $.string() - Default: 'Hello, ' - -Methods: - testAction: - Usage: Action - Body: - - sleep(1) - - $this.find(std:Environment).reporter.report($this, 'Completed') - deploy: - Body: - - $this.find(std:Environment).reporter.report($this, 'Follow the white rabbit') - staticAction: - Scope: Public - Usage: Static - Arguments: - - myName: - Contract: $.string().notNull() - - myAge: - Contract: $.int().notNull() - Body: - - $futureAge: $myAge + 5 - - Return: concat($.greeting, $myName, ". In 5 years you will be {0} years old.".format($futureAge)) diff --git a/muranoclient/tests/functional/cli/__init__.py b/muranoclient/tests/functional/cli/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/muranoclient/tests/functional/cli/murano_test_utils.py b/muranoclient/tests/functional/cli/murano_test_utils.py deleted file mode 100644 index c5a2d5ce..00000000 --- a/muranoclient/tests/functional/cli/murano_test_utils.py +++ /dev/null @@ -1,256 +0,0 @@ -# Copyright (c) 2015 Mirantis, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from http import server as SimpleHTTPServer -import json -import multiprocessing -import os -import shutil -import socketserver -import tempfile -import time - -from oslo_utils import uuidutils -from tempest.lib.cli import output_parser -from tempest.lib import exceptions - -from muranoclient.tests.functional.cli import utils -from muranoclient.tests.functional import muranoclient - - -class CLIUtilsTestBase(muranoclient.ClientTestBase): - """Basic methods for Murano CLI client.""" - - def delete_murano_object(self, murano_object, obj_to_del): - """Delete Murano object - - Delete Murano object like environment, category or - environment-template. - """ - if obj_to_del not in self.listing('{0}-list'.format(murano_object)): - return - object_list = self.listing('{0}-delete'.format(murano_object), - params=obj_to_del['ID']) - start_time = time.time() - while obj_to_del in self.listing('{0}-list'.format(murano_object)): - if start_time - time.time() > 60: - self.fail("{0} is not deleted in 60 seconds". - format(murano_object)) - return object_list - - def create_murano_object(self, murano_object, prefix_object_name): - """Create Murano object - - Create Murano object like environment, category or - environment-template. - """ - object_name = self.generate_name(prefix_object_name) - mrn_objects = self.listing('{0}-create'.format(murano_object), - params=object_name) - mrn_object = None - for obj in mrn_objects: - if object_name == obj['Name']: - mrn_object = obj - break - if mrn_object is None: - self.fail("Murano {0} has not been created!".format(murano_object)) - - self.addCleanup(self.delete_murano_object, murano_object, mrn_object) - return mrn_object - - def create_murano_object_parameter(self, murano_object, prefix_object_name, - param): - """Create Murano object - - Create Murano object like environment, category or - environment-template. - """ - object_name = self.generate_name(prefix_object_name) - params = '{0} {1}'.format(param, object_name) - - mrn_objects = self.listing('{0}-create'.format(murano_object), - params=params) - mrn_object = None - for obj in mrn_objects: - if object_name == obj['Name']: - mrn_object = obj - break - if mrn_object is None: - self.fail("Murano {0} has not been created!".format(murano_object)) - - self.addCleanup(self.delete_murano_object, murano_object, mrn_object) - return mrn_object - - @staticmethod - def generate_uuid(): - """Generate uuid for objects.""" - return uuidutils.generate_uuid(dashed=False) - - @staticmethod - def generate_name(prefix): - """Generate name for objects.""" - suffix = CLIUtilsTestBase.generate_uuid()[:8] - return "{0}_{1}".format(prefix, suffix) - - def get_table_struct(self, command, params=""): - """Get table structure i.e. header of table.""" - return output_parser.table(self.murano(command, - params=params))['headers'] - - def get_object(self, object_list, object_value): - """"Get Murano object by value from list of Murano objects.""" - for obj in object_list: - if object_value in obj.values(): - return obj - - def get_property_value(self, obj, prop): - return [o['Value'] for o in obj - if o['Property'] == '{0}'.format(prop)][0] - - -class TestSuiteRepository(CLIUtilsTestBase): - - def setUp(self): - super(TestSuiteRepository, self).setUp() - self.serve_dir = tempfile.mkdtemp(suffix="repo") - self.app_name = self.generate_name("dummy_app") - self.dummy_app_path = self._compose_app(name=self.app_name) - - def tearDown(self): - super(TestSuiteRepository, self).tearDown() - shutil.rmtree(self.serve_dir) - - def run_server(self): - def serve_function(): - class Handler(SimpleHTTPServer.SimpleHTTPRequestHandler): - pass - os.chdir(self.serve_dir) - httpd = socketserver.TCPServer( - ("0.0.0.0", 8089), - Handler, bind_and_activate=False) - httpd.allow_reuse_address = True - httpd.server_bind() - httpd.server_activate() - httpd.serve_forever() - self.p = multiprocessing.Process(target=serve_function) - self.p.start() - - def stop_server(self): - self.p.terminate() - - def _compose_app(self, name, require=None): - package_dir = os.path.join(self.serve_dir, 'apps/', name) - shutil.copytree(os.path.join(os.path.dirname( - os.path.realpath(__file__)), 'MockApp'), package_dir) - - app_name = utils.compose_package( - name, - os.path.join(package_dir, 'manifest.yaml'), - package_dir, - require=require, - archive_dir=os.path.join(self.serve_dir, 'apps/'), - ) - - return app_name - - -class CLIUtilsTestPackagesBase(TestSuiteRepository): - """Basic methods for Murano Packages CLI client.""" - - def import_package(self, pkg_name, pkg_path, *args): - """Create Murano dummy package and import it by url.""" - - actions = ' '.join(args) - params = '{0} {1}'.format(pkg_path, actions) - package = self.listing('package-import', params=params) - package = self.get_object(package, pkg_name) - self.addCleanup(self.delete_murano_object, 'package', package) - - return package - - def prepare_file_with_obj_model(self, obj_model): - temp_file = tempfile.NamedTemporaryFile(prefix="murano-obj-model", - delete=False) - self.addCleanup(os.remove, temp_file.name) - - with open(temp_file.name, 'w') as tf: - tf.write(json.dumps([obj_model])) - - return temp_file.name - - def wait_deployment_result(self, env_id, timeout=180): - start_time = time.time() - - env = self.listing('environment-show', params=env_id) - env_status = self.get_property_value(env, 'status') - - expected_statuses = ['ready', 'deploying'] - - while env_status != 'ready': - if time.time() - start_time > timeout: - msg = ("Environment exceeds timeout {0} to change state " - "to Ready. Environment: {1}".format(timeout, env)) - raise exceptions.TimeoutException(msg) - - env = self.listing('environment-show', params=env_id) - env_status = self.get_property_value(env, 'status') - - if env_status not in expected_statuses: - msg = ("Environment status %s is not in expected " - "statuses: %s" % (env_status, expected_statuses)) - raise exceptions.TempestException(msg) - - time.sleep(2) - - return True - - def prepare_bundle_with_non_existed_package(self): - temp_file = tempfile.NamedTemporaryFile(mode='w', - delete=False) - self.addCleanup(os.remove, temp_file.name) - - with open(temp_file.name, 'w') as tf: - tf.write(json.dumps({'Packages': [ - {'Name': 'first_app'}, - {'Name': 'second_app', 'Version': '1.0'} - ]})) - - return temp_file.name - - def prepare_bundle_with_invalid_format(self): - temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False) - self.addCleanup(os.remove, temp_file.name) - - with open(temp_file.name, 'w') as tf: - tf.write('Packages: [{Name: first_app}, {Name: second_app}]') - - return temp_file.name - - def deploy_environment(self, env_id, obj_model): - session = self.listing('environment-session-create', - params=env_id) - session_id = self.get_property_value(session, 'id') - - temp_file = self.prepare_file_with_obj_model(obj_model) - - self.listing('environment-apps-edit', - params='--session-id {0} {1} {2}'. - format(session_id, env_id, temp_file)) - - self.listing('environment-deploy', - params='{0} --session-id {1}'. - format(env_id, session_id)) - - result = self.wait_deployment_result(env_id) - self.assertTrue(result) diff --git a/muranoclient/tests/functional/cli/test_murano.py b/muranoclient/tests/functional/cli/test_murano.py deleted file mode 100644 index 132839b6..00000000 --- a/muranoclient/tests/functional/cli/test_murano.py +++ /dev/null @@ -1,893 +0,0 @@ -# Copyright (c) 2015 Mirantis, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import json -import unittest - -from muranoclient.tests.functional.cli import \ - murano_test_utils as utils -from muranoclient.tests.functional import muranoclient as murano_client -from oslo_utils.strutils import bool_from_string as str2bool - - -# TODO(mstolyarenko): need to remove this raw when -# https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed -backend_name =\ - murano_client.ClientTestBase.get_backend_flag().rstrip().split()[-1] - - -class SimpleReadOnlyMuranoClientTest(utils.CLIUtilsTestBase): - """Basic, read-only tests for Murano CLI client. - - Basic smoke test for the Murano CLI commands which do not require - creating or modifying murano objects. - """ - - def test_category_list(self): - category = self.get_table_struct('category-list') - self.assertEqual(['ID', 'Name'], category) - - def test_env_template_list(self): - templates = self.get_table_struct('env-template-list') - self.assertEqual(['ID', 'Name', 'Created', 'Updated', 'Is public'], - templates) - - def test_environment_list(self): - environment = self.get_table_struct('environment-list') - self.assertEqual(['ID', 'Name', 'Status', 'Created', 'Updated'], - environment) - - def test_package_list(self): - packages = self.get_table_struct('package-list') - self.assertEqual(['ID', 'Name', 'FQN', 'Author', 'Active', - 'Is Public', 'Type', 'Version'], packages) - - -class TableStructureMuranoClientTest(utils.CLIUtilsTestBase): - """Smoke test for the Murano CLI commands - - Smoke test for the Murano CLI commands which checks table - structure after create or delete category, env-template - environment and package. - """ - - def test_table_struct_deployment_list(self): - """Test scenario: - - 1) create environment - 2) check table structure - """ - environment = self.create_murano_object('environment', - 'MuranoTestTS-depl-list') - table_struct = self.get_table_struct('deployment-list', - params=environment['ID']) - self.assertEqual(['ID', 'State', 'Created', 'Updated', 'Finished'], - table_struct) - - def test_table_struct_of_environment_create(self): - """Test scenario: - - 1) create environment - 2) check table structure - """ - self.create_murano_object('environment', 'MuranoTestTS-env-create') - table_struct = self.get_table_struct('environment-list') - self.assertEqual(['ID', 'Name', 'Status', 'Created', 'Updated'], - table_struct) - - def test_table_struct_of_environment_delete(self): - """Test scenario: - - 1) create environment - 2) delete environment - 3) check table structure - """ - environment = self.create_murano_object('environment', - 'MuranoTestTS-env-del') - self.delete_murano_object('environment', environment) - table_struct = self.get_table_struct('environment-list') - self.assertEqual(['ID', 'Name', 'Status', 'Created', 'Updated'], - table_struct) - - def test_table_struct_of_category_create(self): - """Test scenario: - - 1) create category - 2) check table structure - """ - self.create_murano_object('category', 'MuranoTestTS-cat-create') - table_struct = self.get_table_struct('category-list') - self.assertEqual(['ID', 'Name'], table_struct) - - def test_table_struct_of_category_delete(self): - """Test scenario: - - 1) create category - 2) delete category - 3) check table structure - """ - category = self.create_murano_object('category', - 'MuranoTestTS-cat-create') - self.delete_murano_object('category', category) - category = self.get_table_struct('category-list') - self.assertEqual(['ID', 'Name'], category) - - def test_table_struct_of_env_template_create(self): - """Test scenario: - - 1) create env_template - 2) check table structure - """ - self.create_murano_object('env-template', - 'MuranoTestTS-env-tmp-create') - table_struct = self.get_table_struct('env-template-list') - self.assertEqual(['ID', 'Name', 'Created', 'Updated', 'Is public'], - table_struct) - - def test_table_struct_of_env_template_delete(self): - """Test scenario: - - 1) create env_template - 2) delete env_template - 3) check table structure - """ - env_template = self.create_murano_object('env-template', - 'MuranoTestTS-env-tmp-create') - self.delete_murano_object('env-template', env_template) - table_struct = self.get_table_struct('env-template-list') - self.assertEqual(['ID', 'Name', 'Created', 'Updated', 'Is public'], - table_struct) - - -class EnvironmentMuranoSanityClientTest(utils.CLIUtilsTestBase): - """Sanity tests for testing actions with environment. - - Smoke test for the Murano CLI commands which checks basic actions with - environment command like create, delete, rename etc. - """ - - def test_environment_create(self): - """Test scenario: - - 1) create environment - 2) check that created environment exist - """ - environment = self.create_murano_object('environment', - 'TestMuranoSanityEnv') - env_list = self.listing('environment-list') - - # Deleting dates from dictionaries to skip it in assert - list(map(lambda x: x.pop('Updated', None), - env_list + [environment])) - list(map(lambda x: x.pop('Created', None), - env_list + [environment])) - - self.assertIn(environment, env_list) - - def test_environment_delete(self): - """Test scenario: - - 1) create environment - 2) delete environment - """ - environment = self.create_murano_object('environment', - 'TestMuranoSanityEnv') - self.delete_murano_object('environment', environment) - env_list = self.listing('environment-list') - - self.assertNotIn(environment, env_list) - - def test_environment_rename(self): - """Test scenario: - - 1) create environment - 2) rename environment - """ - environment = self.create_murano_object('environment', - 'TestMuranoSanityEnv') - - new_env_name = self.generate_name('TestMuranoSEnv-env-rename') - rename_params = "{0} {1}".format(environment['Name'], new_env_name) - new_list = self.listing('environment-rename', params=rename_params) - renamed_env = self.get_object(new_list, new_env_name) - self.addCleanup(self.delete_murano_object, 'environment', renamed_env) - new_env_list = self.listing('environment-list') - - # Deleting dates from dictionaries to skip it in assert - list(map(lambda x: x.pop('Updated', None), - new_env_list + [environment] + [renamed_env])) - list(map(lambda x: x.pop('Created', None), - new_env_list + [environment] + [renamed_env])) - - self.assertIn(renamed_env, new_env_list) - self.assertNotIn(environment, new_env_list) - - def test_table_struct_env_show(self): - """Test scenario: - - 1) create environment - 2) check structure of env_show object - """ - environment = self.create_murano_object('environment', - 'TestMuranoSanityEnv') - env_show = self.listing('environment-show', params=environment['Name']) - # Check structure of env_show object - self.assertEqual(['acquired_by', 'created', 'description_text', 'id', - 'name', 'services', 'status', 'tenant_id', - 'updated', 'version'], - list(map(lambda x: x['Property'], env_show))) - - def test_environment_show(self): - """Test scenario: - - 1) create environment - 2) check that env_name, ID, updated and created values - exist in env_show object - """ - environment = self.create_murano_object('environment', - 'TestMuranoSanityEnv') - env_show = self.listing('environment-show', params=environment['Name']) - - self.assertIn(environment['Created'], - list(map(lambda x: x['Value'], env_show))) - self.assertIn(environment['Updated'], - list(map(lambda x: x['Value'], env_show))) - self.assertIn(environment['Name'], - list(map(lambda x: x['Value'], env_show))) - self.assertIn(environment['ID'], - list(map(lambda x: x['Value'], env_show))) - - def test_environment_delete_by_id(self): - """Test scenario: - - 1) create environment - 2) delete environment by environment ID - """ - env_name = self.generate_name('TestMuranoSanityEnv') - environment = self.create_murano_object('environment', env_name) - result = self.murano('environment-delete', params=environment['ID'], - fail_ok=False) - self.assertNotIn(environment['Name'], result) - env_list = self.listing('environment-list') - self.assertNotIn(environment, env_list) - - def test_environment_model_show(self): - """Test scenario: - - 1) create environment - 2) check that the result of environment-model-show is a valid - non-empty json - """ - env_name = self.generate_name('TestMuranoSanityEnv') - environment = self.create_murano_object('environment', env_name) - model = self.murano('environment-model-show', params=environment['ID']) - result = json.loads(model) - self.assertEqual(4, len(result)) - - -class CategoryMuranoSanityClientTest(utils.CLIUtilsTestBase): - """Sanity tests for testing actions with Category. - - Smoke test for the Murano CLI commands which checks basic actions with - category command like create, delete etc. - """ - - def test_category_create(self): - """Test scenario: - - 1) create category - 2) check that created category exist - """ - category = self.create_murano_object('category', - 'TestMuranoSanityCategory') - category_list = self.listing('category-list') - - self.assertIn(category, category_list) - - def test_category_delete(self): - """Test scenario: - - 1) create category - 2) delete category - 3) check that category has been deleted successfully - """ - category = self.create_murano_object('category', - 'TestMuranoSanityCategory') - self.delete_murano_object('category', category) - category_list = self.listing('category-list') - - self.assertNotIn(category, category_list) - - def test_table_struct_category_show(self): - """Test scenario: - - 1) create category - 2) check table structure of category-show object - """ - category = self.create_murano_object('category', - 'TestMuranoSanityCategory') - category_show = self.listing('category-show', params=category['ID']) - - self.assertEqual(['id', 'name', 'packages'], - list(map(lambda x: x['Property'], category_show))) - - def test_category_show(self): - """Test scenario: - - 1) create category - 2) check that category values exist in category_show object - """ - category = self.create_murano_object('category', - 'TestMuranoSanityCategory') - category_show = self.listing('category-show', params=category['ID']) - - self.assertIn(category['ID'], - list(map(lambda x: x['Value'], category_show))) - self.assertIn(category['Name'], - list(map(lambda x: x['Value'], category_show))) - - def test_non_existing_category_delete(self): - """Test scenario: - - 1) try to call category-delete for non existing category - 2) check that error message contains user friendly substring - """ - result = self.murano('category-delete', params='non-existing', - fail_ok=True) - self.assertIn("Failed to delete 'non-existing'; category not found", - result) - - def test_non_existing_category_show(self): - """Test scenario: - - 1) try to call category-show for non existing category - 2) check that error message contains user friendly substring - """ - result = self.murano('category-show', params='non-existing', - fail_ok=True) - self.assertIn("Category id 'non-existing' not found", str(result)) - - def test_category_create_with_long_name(self): - """Test scenario: - - 1) try to create category with long name (>80) - 2) check that error message contains user friendly substring - """ - result = self.murano('category-create', params='name' * 21, - fail_ok=True) - self.assertIn( - "Category name should be 80 characters maximum", - result) - - -class EnvTemplateMuranoSanityClientTest(utils.CLIUtilsTestBase): - """Sanity tests for testing actions with Environment template. - - Smoke test for the Murano CLI commands which checks basic actions with - env-temlate command like create, delete etc. - """ - def test_environment_template_create(self): - """Test scenario: - - 1) create environment template - 2) check that created environment template exist - """ - env_template = self.create_murano_object('env-template', - 'TestMuranoSanityEnvTemp') - env_template_list = self.listing('env-template-list') - - # Deleting dates from dictionaries to skip it in assert - list(map(lambda x: x.pop('Updated', None), - env_template_list + [env_template])) - list(map(lambda x: x.pop('Created', None), - env_template_list + [env_template])) - - self.assertIn(env_template, env_template_list) - - def test_environment_template_delete(self): - """Test scenario: - - 1) create environment template - 2) delete environment template - 3) check that deleted environment template doesn't exist - """ - env_template = self.create_murano_object('env-template', - 'TestMuranoSanityEnvTemp') - env_template_list = self.delete_murano_object('env-template', - env_template) - - self.assertNotIn(env_template, env_template_list) - - def test_table_struct_env_template_show(self): - """Test scenario: - - 1) create environment template - 2) check table structure of env-template-show object - """ - env_template = self.create_murano_object('env-template', - 'TestMuranoSanityEnvTemp') - env_template_show = self.listing('env-template-show', - params=env_template['ID']) - tested_env_template = list( - map(lambda x: x['Property'], env_template_show)) - - self.assertIn('created', tested_env_template) - self.assertIn('id', tested_env_template) - self.assertIn('name', tested_env_template) - self.assertIn('services', tested_env_template) - self.assertIn('tenant_id', tested_env_template) - self.assertIn('updated', tested_env_template) - self.assertIn('version', tested_env_template) - - def test_env_template_show(self): - """Test scenario: - - 1) create environment template - 2) check that environment template values exist in - env-template-show object - """ - env_template = self.create_murano_object('env-template', - 'TestMuranoSanityEnvTemp') - env_template_show = self.listing('env-template-show', - params=env_template['ID']) - tested_env = list(map(lambda x: x['Value'], env_template_show)) - - self.assertIn(env_template['ID'], tested_env) - self.assertIn(env_template['Name'], tested_env) - - def test_env_template_create_environment(self): - """Test scenario: - - 1) create environment template - 2) create environment from template - """ - env_template = self.create_murano_object('env-template', - 'TestMuranoSanityEnvTemp') - new_env_name = self.generate_name('EnvFromTemp') - params = "{0} {1}".format(env_template['ID'], new_env_name) - env_created = self.listing('env-template-create-env', params=params) - tested_env_created = list(map(lambda x: x['Property'], env_created)) - - self.assertIn('environment_id', tested_env_created) - self.assertIn('session_id', tested_env_created) - - def test_env_template_clone(self): - """Test scenario: - - 1) create environment template - 2) clone template - 3) check that create environment template has the new name - 4) delete new template - """ - - env_template = self.create_murano_object_parameter( - 'env-template', 'TestMuranoSanityEnvTemp', '--is-public') - new_template = self.generate_name('TestMuranoSanityEnvTemp') - - params = "{0} {1}".format(env_template['ID'], new_template) - template_created = self.listing('env-template-clone', params=params) - tp_list = list( - map(lambda x: ({x['Property']: x['Value']}), template_created)) - result_name = list(filter(lambda x: x.get('name'), tp_list))[0]['name'] - result_id = list(filter(lambda x: x.get('id'), tp_list))[0]['id'] - self.listing('env-template-delete', params=result_id) - - self.assertIn(result_name, new_template) - - -class PackageMuranoSanityClientTest(utils.CLIUtilsTestPackagesBase): - """Sanity tests for testing actions with Packages. - - Smoke tests for the Murano CLI commands which check basic actions with - packages like import, create, delete etc. - """ - - def test_package_import_by_url(self): - """Test scenario: - - 1) import package by url - 2) check that package exists - """ - try: - self.run_server() - package = self.import_package( - self.app_name, - 'http://localhost:8089/apps/{0}.zip'.format(self.app_name) - ) - finally: - self.stop_server() - package_list = self.listing('package-list') - - self.assertIn(package, package_list) - - def test_package_import_by_path(self): - """Test scenario: - - 1) import package by path - 2) check that package exists - """ - package = self.import_package( - self.app_name, - self.dummy_app_path - ) - package_list = self.listing('package-list') - - self.assertIn(package, package_list) - - def test_package_is_public(self): - """Test scenario: - - 1) import package - 2) check that package is public - """ - package = self.import_package( - self.app_name, - self.dummy_app_path, - '--is-public') - - package_show = self.listing('package-show', params=package['ID']) - package_show = {item['Property']: item['Value'] - for item in package_show} - - self.assertEqual(package['Is Public'], 'True') - self.assertEqual( - str2bool(package['Is Public']), - str2bool(package_show['is_public'])) - - def test_package_delete(self): - """Test scenario: - - 1) import package - 2) delete package - 3) check that package has been deleted - """ - - package = self.import_package( - self.app_name, - self.dummy_app_path - ) - package_list = self.delete_murano_object('package', package) - - self.assertNotIn(package, package_list) - - def test_package_show(self): - """Test scenario: - - 1) import package - 2) check that package values exist in - return by package-show object - """ - - package = self.import_package( - self.app_name, - self.dummy_app_path - ) - package_show = self.listing('package-show', params=package['ID']) - package_show = {item['Property']: item['Value'] - for item in package_show} - - self.assertEqual( - str2bool(package['Active']), - str2bool(package_show['enabled'])) - self.assertEqual( - package['FQN'], - package_show['fully_qualified_name']) - self.assertEqual( - package['ID'], - package_show['id']) - self.assertEqual( - str2bool(package['Is Public']), - str2bool(package_show['is_public'])) - self.assertEqual( - package['Name'], - package_show['name']) - self.assertEqual( - package['Type'], - package_show['type']) - - def test_package_import_update(self): - """Test scenario: - - 1) import package - 2) import new_package using option 'u' - update - 3) check that package has been updated - """ - package = self.import_package( - self.app_name, - self.dummy_app_path - ) - upd_package = self.import_package( - self.app_name, - self.dummy_app_path, - '--exists-action', 'u' - ) - self.assertEqual(package['Name'], upd_package['Name']) - self.assertNotEqual(package['ID'], upd_package['ID']) - - def test_package_import_skip(self): - """Test scenario: - - 1) import package using option 's' - skip for existing package - 2) try to import the same package using option 's' - skip - 3) check that package hasn't been updated - """ - package = self.import_package( - self.app_name, - self.dummy_app_path, - '--exists-action', 's' - ) - updated_package = self.import_package( - self.app_name, - self.dummy_app_path, - '--exists-action', 's' - ) - package_list = self.listing("package-list") - - self.assertIn(package, package_list) - self.assertIsNone(updated_package) - - def test_package_import_abort(self): - """Test scenario: - - 1) import package - 2) import new_package using option 'a' - skip - 3) check that package hasn't been updated - """ - package = self.import_package( - self.app_name, - self.dummy_app_path - ) - package_list = self.listing('package-list') - - self.assertIn(package, package_list) - - package = self.import_package( - self.app_name, - self.dummy_app_path, - '--exists-action', 'a' - ) - package_list = self.listing('package-list') - self.assertNotIn(package, package_list) - - -class DeployMuranoEnvironmentTest(utils.CLIUtilsTestPackagesBase): - """Test for testing Murano environment deployment. - - Test for the Murano CLI commands which checks addition of app - to the environment, session creation and deployment of - environment. - """ - # TODO(mstolyarenko): need to unskip this test when - # https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed - @unittest.skipIf(backend_name == 'glare', - "This test fails when GLARE is used as packages " - "service. To be fixed as part of #1625039") - def test_environment_deployment(self): - """Test scenario: - - 1) import package - 2) create environment - 3) create session for created environment - 4) add application to the environment - 5) send environment to deploy - 6) check that deployment was successful - """ - self.import_package( - self.app_name, - self.dummy_app_path - ) - - env_id = self.create_murano_object('environment', - 'TestMuranoDeployEnv')['ID'] - obj_model = { - 'op': 'add', - 'path': '/-', - 'value': { - '?': { - 'type': 'io.murano.apps.{0}'.format(self.app_name), - 'id': '{0}'.format(self.generate_uuid()), - } - } - } - self.deploy_environment(env_id, obj_model) - deployments = self.listing('deployment-list', params=env_id) - - self.assertEqual('success', deployments[0]['State']) - self.assertEqual(1, len(deployments)) - - # TODO(mstolyarenko): need to unskip this test when - # https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed - @unittest.skipIf(backend_name == 'glare', - "This test fails when GLARE is used as packages " - "service. To be fixed as part of #1625039") - def test_add_component_to_deployed_env(self): - """Test scenario: - - 1) import package - 2) create environment - 3) create session for created environment - 4) add application to the environment - 5) send environment to deploy - 6) check that deployment was successful - 7) add application to environment - 8) deploy environment again - """ - self.import_package( - self.app_name, - self.dummy_app_path - ) - - env_id = self.create_murano_object('environment', - 'TestMuranoDeployEnv')['ID'] - obj_model = { - 'op': 'add', - 'path': '/-', - 'value': { - '?': { - 'type': 'io.murano.apps.{0}'.format(self.app_name), - 'id': '', - } - } - } - obj_model['value']['?']['id'] = self.generate_uuid() - self.deploy_environment(env_id, obj_model) - - deployments = self.listing('deployment-list', params=env_id) - self.assertEqual('success', deployments[0]['State']) - self.assertEqual(1, len(deployments)) - - obj_model['value']['?']['id'] = self.generate_uuid() - self.deploy_environment(env_id, obj_model) - - deployments = self.listing('deployment-list', params=env_id) - self.assertEqual('success', deployments[1]['State']) - self.assertEqual(2, len(deployments)) - - # TODO(mstolyarenko): need to unskip this test when - # https://bugs.launchpad.net/python-muranoclient/+bug/1625039 is fixed - @unittest.skipIf(backend_name == 'glare', - "This test fails when GLARE is used as packages " - "service. To be fixed as part of #1625039") - def test_delete_component_from_deployed_env(self): - """Test scenario: - - 1) import package - 2) create environment - 3) create session for created environment - 4) add application to the environment - 5) send environment to deploy - 6) check that deployment was successful - 7) delete application from environment - 8) deploy environment again - """ - self.import_package( - self.app_name, - self.dummy_app_path - ) - - env_id = self.create_murano_object('environment', - 'TestMuranoDeployEnv')['ID'] - - obj_model = { - 'op': 'add', - 'path': '/-', - 'value': { - '?': { - 'type': 'io.murano.apps.{0}'.format(self.app_name), - 'id': '{0}'.format(self.generate_uuid()), - } - } - } - self.deploy_environment(env_id, obj_model) - - obj_model = { - 'op': 'remove', - 'path': '/0' - } - self.deploy_environment(env_id, obj_model) - - deployments = self.listing('deployment-list', params=env_id) - self.assertEqual('success', deployments[1]['State']) - self.assertEqual(2, len(deployments)) - - -class BundleMuranoSanityClientTest(utils.CLIUtilsTestPackagesBase): - """Sanity tests for testing actions with bundle. - - Tests for the Murano CLI commands which check basic actions with - bundles. - """ - - def test_bundle_import_without_bundle_name(self): - """Test scenario: - - 1) Execute murano bundle-import command without bundle name - 2) check that error message contains user friendly substring - """ - result = self.murano('bundle-import', params='', - fail_ok=True) - self.assertIn("murano bundle-import: error: the following " - "arguments are required", result) - - @unittest.skip("Skip due to apps.openstack.org website is retired.") - def test_bundle_import_with_non_existing_package_name(self): - """Test scenario: - - 1) Execute murano bundle-import command with non-existing packages - name inside - 2) check that error message contains user friendly substring - """ - result = self.murano( - 'bundle-import', - params=self.prepare_bundle_with_non_existed_package(), - fail_ok=False) - self.assertIn("Couldn't find file for package", result) - self.assertIn("Error Got non-ok status(404) while connecting", result) - - @unittest.skip("Skip due to apps.openstack.org website is retired.") - def test_bundle_import_with_non_existing_name(self): - """Test scenario: - - 1) Execute murano bundle-import command with non-existing bundle - name - 2) check that error message contains user friendly substring - """ - result = self.murano('bundle-import', params=self.app_name, - fail_ok=True) - self.assertIn("Bundle file '{}' does not exist".format(self.app_name), - result) - self.assertIn("reason: Got non-ok status(404) while connecting to", - result) - - def test_bundle_import_with_invalid_file_format(self): - """Test scenario: - - 1) Execute murano bundle-import command with invalid bundle file - format - 2) check that error message contains user friendly substring - """ - try: - self.murano( - 'bundle-import', - params=self.prepare_bundle_with_invalid_format(), - fail_ok=False) - except utils.exceptions.CommandFailed as exception: - self.assertIn("Can't parse bundle contents", str(exception)) - - -class StaticActionMuranoClientTest(utils.CLIUtilsTestPackagesBase): - """Tests for testing static actions execution. - - Tests for the Murano CLI commands which check the result of sample - static action execution. - """ - - def test_static_action_call(self): - """Test scenario: - - 1) import package - 2) call static action of the class in that package - 3) check the result of action - """ - package = self.import_package( - self.app_name, - self.dummy_app_path - ) - result = self.murano( - 'static-action-call', params='{0} staticAction --package-name {1} ' - '--arguments myName=John myAge=28'.format(package['FQN'], - package['FQN'])) - expected = "Waiting for result...\nStatic action result: Hello, " \ - "John. In 5 years you will be 33 years old.\n" - self.assertEqual(expected, result) diff --git a/muranoclient/tests/functional/cli/utils.py b/muranoclient/tests/functional/cli/utils.py deleted file mode 100644 index b111d3d4..00000000 --- a/muranoclient/tests/functional/cli/utils.py +++ /dev/null @@ -1,61 +0,0 @@ -# Copyright (c) 2015 Mirantis, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os -import zipfile - -import yaml - - -MANIFEST = {'Format': 'MuranoPL/1.0', - 'Type': 'Application', - 'Description': 'MockApp for CLI tests', - 'Author': 'Mirantis, Inc'} - - -def compose_package(app_name, manifest, package_dir, - require=None, archive_dir=None): - """Composes a murano package - - Composes package `app_name` with `manifest` file as a template for the - manifest and files from `package_dir`. - Includes `require` section if any in the manifest file. - Puts the resulting .zip file into `archive_dir` if present or in the - `package_dir`. - """ - with open(manifest, 'w') as f: - fqn = 'io.murano.apps.' + app_name - mfest_copy = MANIFEST.copy() - mfest_copy['FullName'] = fqn - mfest_copy['Name'] = app_name - mfest_copy['Classes'] = {fqn: 'mock_muranopl.yaml'} - if require: - mfest_copy['Require'] = require - f.write(yaml.dump(mfest_copy, default_flow_style=False)) - - name = app_name + '.zip' - - if not archive_dir: - archive_dir = os.path.dirname(os.path.abspath(__file__)) - archive_path = os.path.join(archive_dir, name) - - with zipfile.ZipFile(archive_path, 'w') as zip_file: - for root, dirs, files in os.walk(package_dir): - for f in files: - zip_file.write( - os.path.join(root, f), - arcname=os.path.join(os.path.relpath(root, package_dir), f) - ) - - return archive_path diff --git a/muranoclient/tests/functional/muranoclient.py b/muranoclient/tests/functional/muranoclient.py deleted file mode 100644 index e617cc98..00000000 --- a/muranoclient/tests/functional/muranoclient.py +++ /dev/null @@ -1,70 +0,0 @@ -# Copyright (c) 2014 Mirantis, Inc. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import configparser -import os - -from tempest.lib.cli import base - - -class ClientTestBase(base.ClientTestBase): - - def murano(self, action, flags='', params='', - fail_ok=False, endpoint_type='publicURL', merge_stderr=True): - flags += self.get_backend_flag() - return self.clients.cmd_with_auth( - 'murano', action, flags, params, fail_ok, merge_stderr) - - def _get_clients(self): - cli_dir = os.environ.get( - 'OS_MURANOCLIENT_EXEC_DIR', - os.path.join(os.path.abspath('.'), '.tox/functional/bin')) - - self.username = os.environ.get('OS_USERNAME') - self.password = os.environ.get('OS_PASSWORD') - self.tenant_name = os.environ.get('OS_PROJECT_NAME', - os.environ.get('OS_TENANT_NAME')) - self.uri = os.environ.get('OS_AUTH_URL') - config = configparser.RawConfigParser() - if config.read('functional_creds.conf'): - # the OR pattern means the environment is preferred for - # override - self.username = self.username or config.get('admin', 'user') - self.password = self.password or config.get('admin', 'pass') - self.tenant_name = self.tenant_name or config.get('admin', - 'tenant') - self.uri = self.uri or config.get('auth', 'uri') - - clients = base.CLIClient( - username=self.username, - password=self.password, - tenant_name=self.tenant_name, - uri=self.uri, - cli_dir=cli_dir - ) - return clients - - def listing(self, command, params=""): - return self.parser.listing(self.murano(command, params=params)) - - def get_value(self, need_field, known_field, known_value, somelist): - for element in somelist: - if element[known_field] == known_value: - return element[need_field] - - @staticmethod - def get_backend_flag(): - backend = os.environ.get('MURANO_PACKAGES_SERVICE', 'murano') - backend_flag = " --murano-packages-service {0} ".format(backend) - return backend_flag diff --git a/muranoclient/tests/unit/__init__.py b/muranoclient/tests/unit/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/muranoclient/tests/unit/base.py b/muranoclient/tests/unit/base.py deleted file mode 100644 index fc595709..00000000 --- a/muranoclient/tests/unit/base.py +++ /dev/null @@ -1,44 +0,0 @@ -# Copyright 2011 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import os - -import fixtures -import testtools - - -class TestCaseShell(testtools.TestCase): - TEST_REQUEST_BASE = { - 'verify': True, - } - - def setUp(self): - super(TestCaseShell, self).setUp() - if (os.environ.get('OS_STDOUT_CAPTURE') == 'True' or - os.environ.get('OS_STDOUT_CAPTURE') == '1'): - stdout = self.useFixture(fixtures.StringStream('stdout')).stream - self.useFixture(fixtures.MonkeyPatch('sys.stdout', stdout)) - if (os.environ.get('OS_STDERR_CAPTURE') == 'True' or - os.environ.get('OS_STDERR_CAPTURE') == '1'): - stderr = self.useFixture(fixtures.StringStream('stderr')).stream - self.useFixture(fixtures.MonkeyPatch('sys.stderr', stderr)) - - -class TestAdditionalAsserts(testtools.TestCase): - - def check_dict_is_subset(self, dict1, dict2): - # There is an assert for this in Python 2.7 but not 2.6 - self.assertTrue(all(k in dict2 and dict2[k] == v for k, v - in dict1.items())) diff --git a/muranoclient/tests/unit/fakes.py b/muranoclient/tests/unit/fakes.py deleted file mode 100644 index 544eb45c..00000000 --- a/muranoclient/tests/unit/fakes.py +++ /dev/null @@ -1,47 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -from oslo_serialization import jsonutils - - -class FakeHTTPResponse(object): - - version = 1.1 - - def __init__(self, status_code, reason, headers, content): - self.headers = headers - self.content = content - self.status_code = status_code - self.reason = reason - self.raw = FakeRaw() - - def getheader(self, name, default=None): - return self.headers.get(name, default) - - def getheaders(self): - return self.headers.items() - - def read(self, amt=None): - b = self.content - self.content = None - return b - - def iter_content(self, chunksize): - return self.content - - def json(self): - return jsonutils.loads(self.content) - - -class FakeRaw(object): - version = 110 diff --git a/muranoclient/tests/unit/fixture_data/empty-app/manifest.yaml b/muranoclient/tests/unit/fixture_data/empty-app/manifest.yaml deleted file mode 100644 index cefc672d..00000000 --- a/muranoclient/tests/unit/fixture_data/empty-app/manifest.yaml +++ /dev/null @@ -1,18 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -Format: 1.3 -Type: Application -FullName: empty -Name: empty -Description: empty description -Author: 'Mirantis, Inc' diff --git a/muranoclient/tests/unit/fixture_data/heat-template.yaml b/muranoclient/tests/unit/fixture_data/heat-template.yaml deleted file mode 100644 index d7e80909..00000000 --- a/muranoclient/tests/unit/fixture_data/heat-template.yaml +++ /dev/null @@ -1 +0,0 @@ -heat_template_version: 2013-05-23 diff --git a/muranoclient/tests/unit/fixture_data/logo.png b/muranoclient/tests/unit/fixture_data/logo.png deleted file mode 100644 index e1a24717ead46edf8a7a111f11a97e48bde97693..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11634 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 Cd~?zO diff --git a/muranoclient/tests/unit/fixture_data/test-app/Classes/testapp.yaml b/muranoclient/tests/unit/fixture_data/test-app/Classes/testapp.yaml deleted file mode 100644 index 95b1210a..00000000 --- a/muranoclient/tests/unit/fixture_data/test-app/Classes/testapp.yaml +++ /dev/null @@ -1,35 +0,0 @@ -Namespaces: - =: io.murano.apps.test - std: io.murano - res: io.murano.resources - -Name: APP - -Extends: std:Application - -Properties: - name: - Contract: $.string().notNull() - - instance: - Contract: $.class(res:Instance).notNull() - - -Workflow: - initialize: - Body: - - $.environment: $.find(std:Environment).require() - - deploy: - Body: - - $securityGroupIngress: - - ToPort: 23 - FromPort: 23 - IpProtocol: tcp - External: True - - $.environment.securityGroupManager.addGroupIngress($securityGroupIngress) - - $.instance.deploy() - - $resources: new('io.murano.system.Resources') - - $template: $resources.yaml('Deploy.template') - - $.instance.agent.call($template, $resources) - diff --git a/muranoclient/tests/unit/fixture_data/test-app/Resources/Deploy.template b/muranoclient/tests/unit/fixture_data/test-app/Resources/Deploy.template deleted file mode 100644 index 04bd4d5e..00000000 --- a/muranoclient/tests/unit/fixture_data/test-app/Resources/Deploy.template +++ /dev/null @@ -1,21 +0,0 @@ -FormatVersion: 2.0.0 -Version: 1.0.0 -Name: Deploy - -Parameters: - appName: $appName - -Body: | - return deploy(args.appName).stdout - -Scripts: - deploy: - Type: Application - Version: 1.0.0 - EntryPoint: deploy.sh - Files: - - installer.sh - - common.sh - Options: - captureStdout: true - captureStderr: false diff --git a/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/common.sh b/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/common.sh deleted file mode 100644 index a9bf588e..00000000 --- a/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/common.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/bash diff --git a/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/deploy.sh b/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/deploy.sh deleted file mode 100644 index a9bf588e..00000000 --- a/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/deploy.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/bash diff --git a/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/installer.sh b/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/installer.sh deleted file mode 100644 index a9bf588e..00000000 --- a/muranoclient/tests/unit/fixture_data/test-app/Resources/scripts/installer.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/bash diff --git a/muranoclient/tests/unit/fixture_data/test-app/ui.yaml b/muranoclient/tests/unit/fixture_data/test-app/ui.yaml deleted file mode 100644 index 939ba388..00000000 --- a/muranoclient/tests/unit/fixture_data/test-app/ui.yaml +++ /dev/null @@ -1 +0,0 @@ -Version: 2 diff --git a/muranoclient/tests/unit/osc/__init__.py b/muranoclient/tests/unit/osc/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/muranoclient/tests/unit/osc/test_plugin.py b/muranoclient/tests/unit/osc/test_plugin.py deleted file mode 100644 index 4e0d554e..00000000 --- a/muranoclient/tests/unit/osc/test_plugin.py +++ /dev/null @@ -1,34 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from unittest import mock - -from muranoclient.osc import plugin -from muranoclient.tests.unit import base - - -class TestApplicationCatalogPlugin(base.TestCaseShell): - - @mock.patch("muranoclient.v1.client.Client") - def test_make_client(self, p_client): - - instance = mock.Mock() - instance._api_version = {"application_catalog": '1'} - instance._region_name = 'murano_region' - instance.session = 'murano_session' - - plugin.make_client(instance) - p_client.assert_called_with( - mock.ANY, - region_name='murano_region', - session='murano_session', - service_type='application-catalog') diff --git a/muranoclient/tests/unit/osc/v1/__init__.py b/muranoclient/tests/unit/osc/v1/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/muranoclient/tests/unit/osc/v1/fakes.py b/muranoclient/tests/unit/osc/v1/fakes.py deleted file mode 100644 index 80988ef8..00000000 --- a/muranoclient/tests/unit/osc/v1/fakes.py +++ /dev/null @@ -1,22 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from osc_lib.tests import utils -from unittest import mock - - -class TestApplicationCatalog(utils.TestCommand): - - def setUp(self): - super(TestApplicationCatalog, self).setUp() - - self.app.client_manager.application_catalog = mock.Mock() diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/heat-template.yaml b/muranoclient/tests/unit/osc/v1/fixture_data/heat-template.yaml deleted file mode 100644 index d7e80909..00000000 --- a/muranoclient/tests/unit/osc/v1/fixture_data/heat-template.yaml +++ /dev/null @@ -1 +0,0 @@ -heat_template_version: 2013-05-23 diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/logo.png b/muranoclient/tests/unit/osc/v1/fixture_data/logo.png deleted file mode 100644 index e1a24717ead46edf8a7a111f11a97e48bde97693..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 11634 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 Cd~?zO diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Classes/testapp.yaml b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Classes/testapp.yaml deleted file mode 100644 index 95b1210a..00000000 --- a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Classes/testapp.yaml +++ /dev/null @@ -1,35 +0,0 @@ -Namespaces: - =: io.murano.apps.test - std: io.murano - res: io.murano.resources - -Name: APP - -Extends: std:Application - -Properties: - name: - Contract: $.string().notNull() - - instance: - Contract: $.class(res:Instance).notNull() - - -Workflow: - initialize: - Body: - - $.environment: $.find(std:Environment).require() - - deploy: - Body: - - $securityGroupIngress: - - ToPort: 23 - FromPort: 23 - IpProtocol: tcp - External: True - - $.environment.securityGroupManager.addGroupIngress($securityGroupIngress) - - $.instance.deploy() - - $resources: new('io.murano.system.Resources') - - $template: $resources.yaml('Deploy.template') - - $.instance.agent.call($template, $resources) - diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/Deploy.template b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/Deploy.template deleted file mode 100644 index 04bd4d5e..00000000 --- a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/Deploy.template +++ /dev/null @@ -1,21 +0,0 @@ -FormatVersion: 2.0.0 -Version: 1.0.0 -Name: Deploy - -Parameters: - appName: $appName - -Body: | - return deploy(args.appName).stdout - -Scripts: - deploy: - Type: Application - Version: 1.0.0 - EntryPoint: deploy.sh - Files: - - installer.sh - - common.sh - Options: - captureStdout: true - captureStderr: false diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/common.sh b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/common.sh deleted file mode 100644 index a9bf588e..00000000 --- a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/common.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/bash diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/deploy.sh b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/deploy.sh deleted file mode 100644 index a9bf588e..00000000 --- a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/deploy.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/bash diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/installer.sh b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/installer.sh deleted file mode 100644 index a9bf588e..00000000 --- a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/Resources/scripts/installer.sh +++ /dev/null @@ -1 +0,0 @@ -#!/bin/bash diff --git a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/ui.yaml b/muranoclient/tests/unit/osc/v1/fixture_data/test-app/ui.yaml deleted file mode 100644 index 939ba388..00000000 --- a/muranoclient/tests/unit/osc/v1/fixture_data/test-app/ui.yaml +++ /dev/null @@ -1 +0,0 @@ -Version: 2 diff --git a/muranoclient/tests/unit/osc/v1/test_action.py b/muranoclient/tests/unit/osc/v1/test_action.py deleted file mode 100644 index 3909435b..00000000 --- a/muranoclient/tests/unit/osc/v1/test_action.py +++ /dev/null @@ -1,80 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from unittest import mock - -from muranoclient.osc.v1 import action as osc_action -from muranoclient.tests.unit.osc.v1 import fakes -from muranoclient.v1 import static_actions as api_static_actions - - -class TestAction(fakes.TestApplicationCatalog): - def setUp(self): - super(TestAction, self).setUp() - self.static_actions_mock = \ - self.app.client_manager.application_catalog.static_actions - - -class TestStaticActionCall(TestAction): - def setUp(self): - super(TestStaticActionCall, self).setUp() - self.static_actions_mock.call.return_value = \ - api_static_actions.StaticActionResult('result') - - # Command to test - self.cmd = osc_action.StaticActionCall(self.app, None) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_static_action_call_basic(self, mock_util): - mock_util.return_value = 'result' - - arglist = ['class.name', 'method.name'] - verifylist = [('class_name', 'class.name'), - ('method_name', 'method.name')] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['Static action result'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = ['result'] - self.assertEqual(expected_data, data) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_static_action_call_full(self, mock_util): - mock_util.return_value = 'result' - - arglist = ['class.name', 'method.name', - '--arguments', 'food=spam', 'parrot=dead', - '--package-name', 'package.name', - '--class-version', '>1'] - verifylist = [('class_name', 'class.name'), - ('method_name', 'method.name'), - ('arguments', ['food=spam', 'parrot=dead']), - ('package_name', 'package.name'), - ('class_version', '>1')] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['Static action result'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = ['result'] - self.assertEqual(expected_data, data) diff --git a/muranoclient/tests/unit/osc/v1/test_category.py b/muranoclient/tests/unit/osc/v1/test_category.py deleted file mode 100644 index 9f9a9c30..00000000 --- a/muranoclient/tests/unit/osc/v1/test_category.py +++ /dev/null @@ -1,149 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from unittest import mock - -from muranoclient.osc.v1 import category as osc_category -from muranoclient.tests.unit.osc.v1 import fakes -from muranoclient.v1 import categories as api_category -from muranoclient.v1 import packages as api_packages - -CATEGORY_INFO = {'id': 'xyz123', - 'name': 'fake1', - 'packages': [{'name': 'package1'}, {'name': 'package2'}]} - - -class TestCategory(fakes.TestApplicationCatalog): - def setUp(self): - super(TestCategory, self).setUp() - self.category_mock = self.app.client_manager.application_catalog.\ - categories - self.category_mock.reset_mock() - self.packages_mock = \ - self.app.client_manager.application_catalog.packages - - -class TestListCategories(TestCategory): - def setUp(self): - super(TestListCategories, self).setUp() - self.category_mock.list.return_value = [api_category.Category(None, - CATEGORY_INFO)] - - # Command to test - self.cmd = osc_category.ListCategories(self.app, None) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_category_list(self, mock_util): - mock_util.return_value = ('xyz123', 'fake1') - - columns, data = self.cmd.take_action(parsed_args=None) - - # Check that columns are correct - expected_columns = ['ID', 'Name'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('xyz123', 'fake1')] - self.assertEqual(expected_data, data) - - -class TestShowCategory(TestCategory): - def setUp(self): - super(TestShowCategory, self).setUp() - self.category_mock.get.return_value = api_category.\ - Category(None, CATEGORY_INFO) - - self.packages_mock.filter.return_value = [ - api_packages.Package(None, pkg_info) for pkg_info in CATEGORY_INFO[ - 'packages'] - ] - - # Command to test - self.cmd = osc_category.ShowCategory(self.app, None) - - @mock.patch('textwrap.wrap') - def test_category_show(self, mock_wrap): - arglist = ['xyz123'] - verifylist = [('id', 'xyz123')] - - mock_wrap.return_value = ['package1, package2'] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ('id', 'name', 'packages') - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = ('xyz123', 'fake1', 'package1, package2') - self.assertEqual(expected_data, data) - - -class TestCreateCategory(TestCategory): - def setUp(self): - super(TestCreateCategory, self).setUp() - self.category_mock.add.return_value = [api_category.Category(None, - CATEGORY_INFO)] - - # Command to test - self.cmd = osc_category.CreateCategory(self.app, None) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_category_list(self, mock_util): - arglist = ['fake1'] - verifylist = [('name', 'fake1')] - - mock_util.return_value = ('xyz123', 'fake1') - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['ID', 'Name'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('xyz123', 'fake1')] - self.assertEqual(expected_data, data) - - -class TestDeleteCategory(TestCategory): - def setUp(self): - super(TestDeleteCategory, self).setUp() - self.category_mock.delete.return_value = None - self.category_mock.list.return_value = [api_category.Category(None, - CATEGORY_INFO)] - - # Command to test - self.cmd = osc_category.DeleteCategory(self.app, None) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_category_list(self, mock_util): - arglist = ['abc123', '123abc'] - verifylist = [('id', ['abc123', '123abc'])] - - mock_util.return_value = ('xyz123', 'fake1') - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['ID', 'Name'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('xyz123', 'fake1')] - self.assertEqual(expected_data, data) diff --git a/muranoclient/tests/unit/osc/v1/test_deployment.py b/muranoclient/tests/unit/osc/v1/test_deployment.py deleted file mode 100644 index a39da924..00000000 --- a/muranoclient/tests/unit/osc/v1/test_deployment.py +++ /dev/null @@ -1,82 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from unittest import mock - -from muranoclient.osc.v1 import deployment as osc_deployment -from muranoclient.tests.unit.osc.v1 import fakes -from muranoclient.v1 import deployments as api_deployment - -DEPLOYMENT_COLUMNS = ('id', 'state', 'created', 'updated', 'finished') -DEPLOYMENT_DATA = ('xyz123', 'success', '2016-06-25T12:21:37', - '2016-06-25T12:21:47', '2016-06-25T12:21:47') -ALL_DEPLOYMENT_DATA = (('abc123', 'success', '2016-06-25T12:21:37', - '2016-06-25T12:21:47', '2016-06-25T12:21:47'), - ('xyz456', 'success', '2017-01-31T11:22:35', - '2017-01-31T11:22:47', '2017-01-31T11:22:47')) - - -class TestDeployment(fakes.TestApplicationCatalog): - def setUp(self): - super(TestDeployment, self).setUp() - self.deployment_mock = self.app.client_manager.application_catalog.\ - deployments - self.deployment_mock.reset_mock() - self.environment_mock = self.app.client_manager.application_catalog.\ - environments - - -class TestListDeployment(TestDeployment): - def setUp(self): - super(TestListDeployment, self).setUp() - deployment_info = dict(zip(DEPLOYMENT_COLUMNS, DEPLOYMENT_DATA)) - self.deployment_mock.list.return_value = \ - [api_deployment.Deployment(None, deployment_info)] - - # Command to test - self.cmd = osc_deployment.ListDeployment(self.app, None) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_deployment_list(self, mock_util): - arglist = ['xyz123'] - verifylist = [('id', 'xyz123')] - mock_util.return_value = DEPLOYMENT_DATA - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = [c.title() for c in DEPLOYMENT_COLUMNS] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [DEPLOYMENT_DATA] - self.assertEqual(expected_data, data) - - @mock.patch('osc_lib.utils.get_item_properties', autospec=True) - def test_deployment_list_all_environments(self, mock_util): - arglist = ['--all-environments'] - verifylist = [('id', None), ('all_environments', True)] - mock_util.return_value = ALL_DEPLOYMENT_DATA - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = [c.title() for c in DEPLOYMENT_COLUMNS] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [ALL_DEPLOYMENT_DATA] - self.assertEqual(expected_data, data) diff --git a/muranoclient/tests/unit/osc/v1/test_environment.py b/muranoclient/tests/unit/osc/v1/test_environment.py deleted file mode 100644 index 627abcd8..00000000 --- a/muranoclient/tests/unit/osc/v1/test_environment.py +++ /dev/null @@ -1,584 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import collections -import json -import tempfile -from unittest import mock - - -from muranoclient.osc.v1 import environment as osc_env -from muranoclient.tests.unit.osc.v1 import fakes -from muranoclient.v1 import environments as api_env - -ENV_INFO = {'id': '1234', - 'name': 'Fake Environment', - 'created': '2015-12-16T17:31:54', - 'updated': '2015-12-16T17:31:54', - 'networking': {}, - 'services': ['fake services'], - 'status': 'fake deployed', - 'tenant_id': 'xyz123', - 'version': '1'} - -ENV_MODEL = { - "defaultNetworks": { - "environment": { - "name": "env-network", - "?": { - "type": "io.murano.resources.NeutronNetwork", - "id": "5678" - } - }, - "flat": None - }, - "region": "RegionOne", - "name": "env", - "?": { - "updated": "2016-10-03 09:33:41.039789", - "type": "io.murano.Environment", - "id": "1234" - } -} - - -class TestEnvironment(fakes.TestApplicationCatalog): - def setUp(self): - super(TestEnvironment, self).setUp() - self.environment_mock = self.app.client_manager.application_catalog.\ - environments - self.session_mock = self.app.client_manager.application_catalog.\ - sessions - self.services_mock = self.app.client_manager.application_catalog.\ - services - self.environment_mock.reset_mock() - - -class TestListEnvironment(TestEnvironment): - def setUp(self): - super(TestListEnvironment, self).setUp() - self.environment_mock.list.return_value = [api_env.Environment(None, - ENV_INFO)] - - # Command to test - self.cmd = osc_env.ListEnvironments(self.app, None) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_environment_list_with_no_options(self, mock_util): - arglist = [] - verifylist = [] - - mock_util.return_value = ('1234', 'Environment of all tenants', - 'fake deployed', '2015-12-16T17:31:54', - '2015-12-16T17:31:54' - ) - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('1234', 'Environment of all tenants', - 'fake deployed', '2015-12-16T17:31:54', - '2015-12-16T17:31:54')] - self.assertEqual(expected_data, data) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_environment_list_with_all_tenants(self, mock_util): - arglist = ['--all-tenants'] - verifylist = [('all_tenants', True), ('tenant', None)] - - mock_util.return_value = ('1234', 'Environment of all tenants', - 'fake deployed', '2015-12-16T17:31:54', - '2015-12-16T17:31:54' - ) - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('1234', 'Environment of all tenants', - 'fake deployed', '2015-12-16T17:31:54', - '2015-12-16T17:31:54')] - self.assertEqual(expected_data, data) - self.environment_mock.list.assert_called_once_with(True, None) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_environment_list_with_tenant(self, mock_util): - arglist = ['--tenant=ABC'] - verifylist = [('all_tenants', False), ('tenant', 'ABC')] - - mock_util.return_value = ('1234', 'Environment of tenant ABC', - 'fake deployed', '2015-12-16T17:31:54', - '2015-12-16T17:31:54' - ) - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('1234', 'Environment of tenant ABC', - 'fake deployed', '2015-12-16T17:31:54', - '2015-12-16T17:31:54')] - self.assertEqual(expected_data, data) - self.environment_mock.list.assert_called_once_with(False, 'ABC') - - -class TestShowEnvironment(TestEnvironment): - def setUp(self): - super(TestShowEnvironment, self).setUp() - mock_to_dict = self.environment_mock.get.return_value.to_dict - mock_to_dict.return_value = ENV_INFO - - self.cmd = osc_env.ShowEnvironment(self.app, None) - - @mock.patch('oslo_serialization.jsonutils.dumps') - def test_environment_show_with_no_options(self, mock_json): - arglist = ['fake'] - verifylist = [('id', 'fake')] - - mock_json.return_value = ['fake services'] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ('created', 'id', 'name', 'networking', 'services', - 'status', 'tenant_id', 'updated', 'version') - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = ('2015-12-16T17:31:54', '1234', 'Fake Environment', - {}, ['fake services'], 'fake deployed', 'xyz123', - '2015-12-16T17:31:54', '1') - self.assertEqual(expected_data, data) - - @mock.patch('oslo_serialization.jsonutils.dumps') - def test_environment_show_with_only_app_option(self, mock_json): - arglist = ['fake', '--only-apps'] - verifylist = [('id', 'fake'), ('only_apps', True)] - - mock_json.return_value = ['fake services'] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['services'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [['fake services']] - self.assertEqual(expected_data, data) - - @mock.patch('oslo_serialization.jsonutils.dumps') - def test_environment_show_with_session_id_option(self, mock_json): - arglist = ['fake', '--session-id', 'abc123'] - verifylist = [('id', 'fake'), ('session_id', 'abc123')] - - mock_json.return_value = ['fake services'] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ('created', 'id', 'name', 'networking', 'services', - 'status', 'tenant_id', 'updated', 'version') - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = ('2015-12-16T17:31:54', '1234', 'Fake Environment', - {}, ['fake services'], 'fake deployed', 'xyz123', - '2015-12-16T17:31:54', '1') - self.assertEqual(expected_data, data) - - -class TestRenameEnvironment(TestEnvironment): - def setUp(self): - super(TestRenameEnvironment, self).setUp() - self.environment_mock.update.return_value = [api_env.Environment(None, - ENV_INFO)] - - # Command to test - self.cmd = osc_env.RenameEnvironment(self.app, None) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_environment_rename(self, mock_util): - arglist = ['1234', 'fake-1'] - verifylist = [('id', '1234'), ('name', 'fake-1')] - - mock_util.return_value = ('1234', 'fake-1', 'fake deployed', - '2015-12-16T17:31:54', '2015-12-16T17:31:54' - ) - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('1234', 'fake-1', 'fake deployed', - '2015-12-16T17:31:54', '2015-12-16T17:31:54')] - self.assertEqual(expected_data, data) - - -class TestEnvironmentSessionCreate(TestEnvironment): - def setUp(self): - super(TestEnvironmentSessionCreate, self).setUp() - - # Command to test - self.cmd = osc_env.EnvironmentSessionCreate(self.app, None) - - @mock.patch('muranoclient.common.utils.text_wrap_formatter') - def test_environment_session_create(self, mock_util): - arglist = ['1234'] - verifylist = [('id', '1234')] - - mock_util.return_value = '1abc2xyz' - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['id'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = ['1abc2xyz'] - self.assertEqual(expected_data, data) - - -class TestEnvironmentCreate(TestEnvironment): - def setUp(self): - super(TestEnvironmentCreate, self).setUp() - self.environment_mock.create.return_value = [api_env.Environment(None, - ENV_INFO)] - - # Command to test - self.cmd = osc_env.EnvironmentCreate(self.app, None) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_environment_create_with_no_option(self, mock_util): - arglist = ['fake'] - verifylist = [('name', 'fake')] - - mock_util.return_value = ('1234', 'fake', 'ready', - '2015-12-16T17:31:54', '2015-12-16T17:31:54') - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('1234', 'fake', 'ready', - '2015-12-16T17:31:54', '2015-12-16T17:31:54')] - self.assertEqual(expected_data, data) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_environment_create_with_region_option(self, mock_util): - arglist = ['fake', '--region', 'region_one'] - verifylist = [('name', 'fake'), ('region', 'region_one')] - - mock_util.return_value = ('1234', 'fake', 'ready', - '2015-12-16T17:31:54', '2015-12-16T17:31:54') - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that correct arguments are passed - self.environment_mock.create.assert_has_calls([mock.call( - {'name': 'fake', 'region': 'region_one'})]) - - # Check that columns are correct - expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('1234', 'fake', 'ready', - '2015-12-16T17:31:54', '2015-12-16T17:31:54')] - self.assertEqual(expected_data, data) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_environment_create_with_net_option(self, mock_util): - arglist = ['fake', '--join-net-id', 'x1y2z3'] - verifylist = [('name', 'fake'), ('join_net_id', 'x1y2z3')] - - mock_util.return_value = ('1234', 'fake', 'ready', - '2015-12-16T17:31:54', '2015-12-16T17:31:54') - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - expected_call = { - 'defaultNetworks': { - 'environment': { - 'internalNetworkName': 'x1y2z3', - '?': { - 'type': 'io.murano.resources.ExistingNeutronNetwork', - 'id': mock.ANY - } - }, - 'flat': None - }, - 'name': 'fake', - 'region': None - } - - # Check that correct arguments are passed - self.environment_mock.create.assert_called_with(expected_call) - - # Check that columns are correct - expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('1234', 'fake', 'ready', - '2015-12-16T17:31:54', '2015-12-16T17:31:54')] - self.assertEqual(expected_data, data) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_environment_create_with_subnet_option(self, mock_util): - arglist = ['fake', '--join-subnet-id', 'x1y2z3'] - verifylist = [('name', 'fake'), ('join_subnet_id', 'x1y2z3')] - - mock_util.return_value = ('1234', 'fake', 'ready', - '2015-12-16T17:31:54', '2015-12-16T17:31:54') - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - expected_call = { - 'defaultNetworks': { - 'environment': { - 'internalSubnetworkName': 'x1y2z3', - '?': { - 'type': 'io.murano.resources.ExistingNeutronNetwork', - 'id': mock.ANY - } - }, - 'flat': None - }, - 'name': 'fake', - 'region': None - } - - # Check that correct arguments are passed - self.environment_mock.create.assert_called_with(expected_call) - - # Check that columns are correct - expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('1234', 'fake', 'ready', - '2015-12-16T17:31:54', '2015-12-16T17:31:54')] - self.assertEqual(expected_data, data) - - -class TestEnvironmentDelete(TestEnvironment): - def setUp(self): - super(TestEnvironmentDelete, self).setUp() - self.environment_mock.delete.return_value = None - self.environment_mock.list.return_value = [api_env.Environment(None, - ENV_INFO)] - - # Command to test - self.cmd = osc_env.EnvironmentDelete(self.app, None) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_environment_delete(self, mock_util): - arglist = ['fake1', 'fake2'] - verifylist = [('id', ['fake1', 'fake2'])] - - mock_util.return_value = ('1234', 'Environment of all tenants', - 'fake deployed', '2015-12-16T17:31:54', - '2015-12-16T17:31:54' - ) - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ['Id', 'Name', 'Status', 'Created', 'Updated'] - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = [('1234', 'Environment of all tenants', - 'fake deployed', '2015-12-16T17:31:54', - '2015-12-16T17:31:54')] - self.assertEqual(expected_data, data) - - -class TestEnvironmentDeploy(TestEnvironment): - def setUp(self): - super(TestEnvironmentDeploy, self).setUp() - mock_to_dict = self.environment_mock.get.return_value.to_dict - mock_to_dict.return_value = ENV_INFO - - # Command to test - self.cmd = osc_env.EnvironmentDeploy(self.app, None) - - @mock.patch('oslo_serialization.jsonutils.dumps') - def test_environment_deploy(self, mock_json): - arglist = ['fake', '--session-id', 'abc123'] - verifylist = [('id', 'fake'), ('session_id', 'abc123')] - - mock_json.return_value = ['fake services'] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ('created', 'id', 'name', 'networking', 'services', - 'status', 'tenant_id', 'updated', 'version') - self.assertEqual(expected_columns, columns) - - # Check that data is correct - expected_data = ('2015-12-16T17:31:54', '1234', 'Fake Environment', - {}, ['fake services'], 'fake deployed', 'xyz123', - '2015-12-16T17:31:54', '1') - self.assertEqual(expected_data, data) - - -class TestEnvironmentAppsEdit(TestEnvironment): - def setUp(self): - super(TestEnvironmentAppsEdit, self).setUp() - - # Command to test - self.cmd = osc_env.EnvironmentAppsEdit(self.app, None) - - def test_environment_apps_edit(self): - fake = collections.namedtuple('fakeEnv', 'services') - self.environment_mock.get.side_effect = [ - fake(services=[ - {'?': {'name': "foo"}} - ]), - ] - - temp_file = tempfile.NamedTemporaryFile(prefix="murano-test", mode='w') - json.dump([ - {'op': 'replace', 'path': '/0/?/name', - 'value': "dummy" - } - ], temp_file) - temp_file.file.flush() - - arglist = ['fake', '--session-id', 'abc123', temp_file.name] - - parsed_args = self.check_parser(self.cmd, arglist, []) - - self.cmd.take_action(parsed_args) - - self.services_mock.put.assert_called_once_with( - 'fake', - session_id='abc123', - path='/', - data=[{'?': {'name': 'dummy'}}] - ) - - -class TestEnvironmentModelShow(TestEnvironment): - def setUp(self): - super(TestEnvironmentModelShow, self).setUp() - self.env_mock = \ - self.app.client_manager.application_catalog.environments - self.env_mock.get_model.return_value = ENV_MODEL - - # Command to test - self.cmd = osc_env.EnvironmentModelShow(self.app, None) - - def test_environment_model_show_basic(self): - arglist = ['env-id'] - verifylist = [('id', 'env-id')] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ('?', 'defaultNetworks', 'name', 'region') - self.assertEqual(expected_columns, columns) - - # Check that data is correct - self.assertCountEqual(ENV_MODEL.values(), data) - - def test_environment_model_show_full(self): - arglist = ['env-id', '--path', '/path', '--session-id', 'sess-id'] - verifylist = [('id', 'env-id'), ('path', '/path'), - ('session_id', 'sess-id')] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ('?', 'defaultNetworks', 'name', 'region') - self.assertEqual(expected_columns, columns) - - # Check that data is correct - self.assertCountEqual(ENV_MODEL.values(), data) - - -class TestEnvironmentModelEdit(TestEnvironment): - def setUp(self): - super(TestEnvironmentModelEdit, self).setUp() - self.env_mock = \ - self.app.client_manager.application_catalog.environments - self.env_mock.update_model.return_value = ENV_MODEL - - # Command to test - self.cmd = osc_env.EnvironmentModelEdit(self.app, None) - - def test_environment_model_edit(self): - temp_file = tempfile.NamedTemporaryFile(prefix="murano-test", mode='w') - patch = [{'op': 'replace', 'path': '/name', 'value': 'dummy'}] - json.dump(patch, temp_file) - temp_file.file.flush() - - arglist = ['env-id', temp_file.name, '--session-id', 'sess-id'] - verifylist = [('id', 'env-id'), ('filename', temp_file.name), - ('session_id', 'sess-id')] - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ('?', 'defaultNetworks', 'name', 'region') - self.assertEqual(expected_columns, columns) - - # Check that data is correct - self.assertCountEqual(ENV_MODEL.values(), data) diff --git a/muranoclient/tests/unit/osc/v1/test_package.py b/muranoclient/tests/unit/osc/v1/test_package.py deleted file mode 100644 index 31bfb16c..00000000 --- a/muranoclient/tests/unit/osc/v1/test_package.py +++ /dev/null @@ -1,781 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import io -import json -import os -import shutil -import sys -import tempfile -from unittest import mock - -from testtools import matchers - -from muranoclient.common import exceptions as common_exceptions -from muranoclient.common import utils as mc_utils -from muranoclient.osc.v1 import package as osc_pkg -from muranoclient.tests.unit.osc.v1 import fakes -from muranoclient.tests.unit import test_utils -from muranoclient.v1 import packages - -from osc_lib import exceptions as exc -from osc_lib import utils -import requests_mock - -make_pkg = test_utils.make_pkg - -FIXTURE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), - 'fixture_data')) - -COLUMNS = ['Id', 'Name', 'Fully_qualified_name', 'Author', 'Active', - 'Is public', 'Type', 'Version'] - -DATA = { - 'class_definitions': ['com.example.apache.ApacheHttpServer'], - 'updated': '2016-09-20T06:23:45.000000', - 'description': 'Test description.\n', - 'created': '2016-09-20T06:23:15.000000', - 'author': 'Mirantis, Inc', - 'enabled': True, - 'owner_id': 'a203405ea871484a940850d6c0b8dfd9', - 'tags': ['Server', 'WebServer', 'Apache', 'HTTP', 'HTML'], - 'is_public': False, - 'fully_qualified_name': 'com.example.apache.ApacheHttpServer', - 'type': 'Application', - 'id': '46860070-5f8a-4936-96e8-d7b89e5187d7', - 'categories': [], - 'name': 'Apache HTTP Server' -} - - -class TestPackage(fakes.TestApplicationCatalog): - def setUp(self): - super(TestPackage, self).setUp() - self.package_mock = self.app.client_manager.application_catalog.\ - packages - self.package_mock.reset_mock() - - -class TestCreatePackage(TestPackage): - def setUp(self): - super(TestCreatePackage, self).setUp() - - # Command to test - self.cmd = osc_pkg.CreatePackage(self.app, None) - - def test_create_package_without_args(self): - arglist = [] - parsed_args = self.check_parser(self.cmd, arglist, []) - error = self.assertRaises(exc.CommandError, - self.cmd.take_action, parsed_args) - self.assertEqual('Provide --template for a HOT-based package, OR at ' - 'least --classes-dir for a MuranoPL-based package', - str(error)) - - def test_create_package_template_and_classes_args(self): - heat_template = os.path.join(FIXTURE_DIR, 'heat-template.yaml') - classes_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Classes') - arglist = ['--template', heat_template, '--classes-dir', classes_dir] - parsed_args = self.check_parser(self.cmd, arglist, []) - error = self.assertRaises(exc.CommandError, - self.cmd.take_action, parsed_args) - self.assertEqual('Provide --template for a HOT-based package, OR' - ' --classes-dir for a MuranoPL-based package', - str(error)) - - def test_create_hot_based_package(self): - with tempfile.NamedTemporaryFile() as f: - RESULT_PACKAGE = f.name - heat_template = os.path.join(FIXTURE_DIR, 'heat-template.yaml') - logo = os.path.join(FIXTURE_DIR, 'logo.png') - arglist = ['--template', heat_template, '--output', RESULT_PACKAGE, - '-l', logo] - parsed_args = self.check_parser(self.cmd, arglist, []) - orig = sys.stdout - try: - sys.stdout = io.StringIO() - self.cmd.take_action(parsed_args) - finally: - stdout = sys.stdout.getvalue() - sys.stdout.close() - sys.stdout = orig - matchers.MatchesRegex(stdout, - "Application package " - "is available at {0}".format(RESULT_PACKAGE)) - - def test_create_mpl_package(self): - with tempfile.NamedTemporaryFile() as f: - RESULT_PACKAGE = f.name - classes_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Classes') - resources_dir = os.path.join(FIXTURE_DIR, 'test-app', 'Resources') - ui = os.path.join(FIXTURE_DIR, 'test-app', 'ui.yaml') - arglist = ['-c', classes_dir, '-r', resources_dir, - '-u', ui, '-o', RESULT_PACKAGE] - parsed_args = self.check_parser(self.cmd, arglist, []) - orig = sys.stdout - try: - sys.stdout = io.StringIO() - self.cmd.take_action(parsed_args) - finally: - stdout = sys.stdout.getvalue() - sys.stdout.close() - sys.stdout = orig - matchers.MatchesRegex(stdout, - "Application package " - "is available at {0}".format(RESULT_PACKAGE)) - - -class TestPackageList(TestPackage): - - def setUp(self): - super(TestPackageList, self).setUp() - self.cmd = osc_pkg.ListPackages(self.app, None) - self.package_mock.filter.return_value = \ - [packages.Package(None, DATA)] - utils.get_dict_properties = mock.MagicMock(return_value='') - - def test_package_list_defaults(self): - arglist = [] - parsed_args = self.check_parser(self.cmd, arglist, []) - - columns, data = self.cmd.take_action(parsed_args) - - self.package_mock.filter.assert_called_with( - include_disabled=False, - owned=False) - self.assertEqual(COLUMNS, columns) - - def test_package_list_with_limit(self): - arglist = ['--limit', '10'] - parsed_args = self.check_parser(self.cmd, arglist, []) - - columns, data = self.cmd.take_action(parsed_args) - - self.package_mock.filter.assert_called_with( - include_disabled=False, - limit=10, - owned=False) - - def test_package_list_with_marker(self): - arglist = ['--marker', '12345'] - parsed_args = self.check_parser(self.cmd, arglist, []) - - columns, data = self.cmd.take_action(parsed_args) - - self.package_mock.filter.assert_called_with( - include_disabled=False, - marker='12345', - owned=False) - - def test_package_list_with_name(self): - arglist = ['--name', 'mysql'] - parsed_args = self.check_parser(self.cmd, arglist, []) - - columns, data = self.cmd.take_action(parsed_args) - - self.package_mock.filter.assert_called_with( - include_disabled=False, - name='mysql', - owned=False) - - def test_package_list_with_fqn(self): - arglist = ['--fqn', 'mysql'] - parsed_args = self.check_parser(self.cmd, arglist, []) - - columns, data = self.cmd.take_action(parsed_args) - - self.package_mock.filter.assert_called_with( - include_disabled=False, - fqn='mysql', - owned=False) - - -class TestPackageDelete(TestPackage): - def setUp(self): - super(TestPackageDelete, self).setUp() - self.package_mock.delete.return_value = None - self.package_mock.filter.return_value = \ - [packages.Package(None, DATA)] - - # Command to test - self.cmd = osc_pkg.DeletePackage(self.app, None) - - @mock.patch('osc_lib.utils.get_item_properties') - def test_package_delete(self, mock_util): - arglist = ['fake1'] - verifylist = [('id', ['fake1'])] - - mock_util.return_value = ('1234', 'Core library', - 'io.murano', 'murano.io', '', - 'True', 'Library', '0.0.0' - ) - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - self.assertEqual(COLUMNS, columns) - - # Check that data is correct - expected_data = [('1234', 'Core library', 'io.murano', - 'murano.io', '', 'True', 'Library', '0.0.0')] - self.assertEqual(expected_data, data) - - -class TestPackageImport(TestPackage): - def setUp(self): - super(TestPackageImport, self).setUp() - self.package_mock.filter.return_value = \ - [packages.Package(None, DATA)] - - # Command to test - self.cmd = osc_pkg.ImportPackage(self.app, None) - - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import(self, from_file): - with tempfile.NamedTemporaryFile() as f: - RESULT_PACKAGE = f.name - categories = ['Cat1', 'Cat2 with space'] - - pkg = make_pkg({'FullName': RESULT_PACKAGE}) - from_file.return_value = mc_utils.Package(mc_utils.File(pkg)) - - arglist = [RESULT_PACKAGE, '--categories', - categories, '--is-public'] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - self.package_mock.create.assert_called_once_with({ - 'categories': [categories], - 'is_public': True - }, {RESULT_PACKAGE: mock.ANY},) - - def _test_conflict(self, - packages, from_file, raw_input_mock, - input_action, exists_action=''): - packages.create = mock.MagicMock( - side_effect=[common_exceptions.HTTPConflict("Conflict"), None]) - - packages.filter.return_value = [mock.Mock(id='test_id')] - - raw_input_mock.return_value = input_action - with tempfile.NamedTemporaryFile() as f: - pkg = make_pkg({'FullName': f.name}) - from_file.return_value = mc_utils.Package(mc_utils.File(pkg)) - if exists_action: - arglist = [f.name, '--exists-action', exists_action] - else: - arglist = [f.name] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - return f.name - - @mock.patch('builtins.input') - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_conflict_skip(self, from_file, raw_input_mock): - - name = self._test_conflict( - self.package_mock, - from_file, - raw_input_mock, - 's', - ) - - self.package_mock.create.assert_called_once_with({ - 'is_public': False, - }, {name: mock.ANY},) - - @mock.patch('builtins.input') - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_conflict_skip_ea(self, from_file, raw_input_mock): - - name = self._test_conflict( - self.package_mock, - from_file, - raw_input_mock, - '', - exists_action='s', - ) - - self.package_mock.create.assert_called_once_with({ - 'is_public': False, - }, {name: mock.ANY},) - self.assertFalse(raw_input_mock.called) - - @mock.patch('builtins.input') - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_conflict_abort(self, from_file, raw_input_mock): - - self.assertRaises(SystemExit, self._test_conflict, - self.package_mock, - from_file, - raw_input_mock, - 'a', - ) - - self.package_mock.create.assert_called_once_with({ - 'is_public': False, - }, mock.ANY,) - - @mock.patch('builtins.input') - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_conflict_abort_ea(self, - from_file, raw_input_mock): - - self.assertRaises(SystemExit, self._test_conflict, - self.package_mock, - from_file, - raw_input_mock, - '', - exists_action='a', - ) - - self.package_mock.create.assert_called_once_with({ - 'is_public': False, - }, mock.ANY,) - self.assertFalse(raw_input_mock.called) - - @mock.patch('builtins.input') - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_conflict_update(self, from_file, raw_input_mock): - - name = self._test_conflict( - self.package_mock, - from_file, - raw_input_mock, - 'u', - ) - - self.assertEqual(2, self.package_mock.create.call_count) - self.package_mock.delete.assert_called_once_with('test_id') - - self.package_mock.create.assert_has_calls( - [ - mock.call({'is_public': False}, {name: mock.ANY},), - mock.call({'is_public': False}, {name: mock.ANY},) - ], any_order=True, - ) - - @mock.patch('builtins.input') - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_conflict_update_ea(self, - from_file, raw_input_mock): - - name = self._test_conflict( - self.package_mock, - from_file, - raw_input_mock, - '', - exists_action='u', - ) - - self.assertEqual(2, self.package_mock.create.call_count) - self.package_mock.delete.assert_called_once_with('test_id') - - self.package_mock.create.assert_has_calls( - [ - mock.call({'is_public': False}, {name: mock.ANY},), - mock.call({'is_public': False}, {name: mock.ANY},) - ], any_order=True, - ) - self.assertFalse(raw_input_mock.called) - - def _test_conflict_dep(self, - packages, from_file, - dep_exists_action=''): - packages.create = mock.MagicMock( - side_effect=[common_exceptions.HTTPConflict("Conflict"), - common_exceptions.HTTPConflict("Conflict"), - None]) - - packages.filter.return_value = [mock.Mock(id='test_id')] - - pkg1 = make_pkg( - {'FullName': 'first_app', 'Require': {'second_app': '1.0'}, }) - pkg2 = make_pkg({'FullName': 'second_app', }) - - def side_effect(name): - if 'first_app' in name: - return mc_utils.Package(mc_utils.File(pkg1)) - if 'second_app' in name: - return mc_utils.Package(mc_utils.File(pkg2)) - - from_file.side_effect = side_effect - - arglist = ['first_app', '--exists-action', 's', - '--dep-exists-action', dep_exists_action] - - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_conflict_dep_skip_ea(self, from_file): - self._test_conflict_dep( - self.package_mock, - from_file, - dep_exists_action='s', - ) - - self.assertEqual(2, self.package_mock.create.call_count) - self.package_mock.create.assert_has_calls( - [ - mock.call({'is_public': False}, {'first_app': mock.ANY}), - mock.call({'is_public': False}, {'second_app': mock.ANY}), - ], any_order=True, - ) - - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_conflict_dep_abort_ea(self, from_file): - self.assertRaises(SystemExit, self._test_conflict_dep, - self.package_mock, - from_file, - dep_exists_action='a', - ) - - self.package_mock.create.assert_called_with({ - 'is_public': False, - }, {'second_app': mock.ANY},) - - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_conflict_dep_update_ea(self, from_file): - self._test_conflict_dep( - self.package_mock, - from_file, - dep_exists_action='u', - ) - - self.assertGreater(self.package_mock.create.call_count, 2) - self.assertLess(self.package_mock.create.call_count, 5) - - self.assertTrue(self.package_mock.delete.called) - - self.package_mock.create.assert_has_calls( - [ - mock.call({'is_public': False}, {'first_app': mock.ANY}), - mock.call({'is_public': False}, {'second_app': mock.ANY}), - mock.call({'is_public': False}, {'second_app': mock.ANY}), - ], any_order=True, - ) - - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_no_categories(self, from_file): - with tempfile.NamedTemporaryFile() as f: - RESULT_PACKAGE = f.name - pkg = make_pkg({'FullName': RESULT_PACKAGE}) - from_file.return_value = mc_utils.Package(mc_utils.File(pkg)) - - arglist = [RESULT_PACKAGE] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - self.package_mock.create.assert_called_once_with( - {'is_public': False}, - {RESULT_PACKAGE: mock.ANY}, - ) - - @requests_mock.mock() - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_url(self, rm, from_file): - filename = "http://127.0.0.1/test_package.zip" - - pkg = make_pkg({'FullName': 'test_package'}) - from_file.return_value = mc_utils.Package(mc_utils.File(pkg)) - - rm.get(filename, body=make_pkg({'FullName': 'test_package'})) - - arglist = [filename] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - self.package_mock.create.assert_called_once_with( - {'is_public': False}, - {'test_package': mock.ANY}, - ) - - @requests_mock.mock() - @mock.patch('muranoclient.common.utils.Package.from_file') - def test_package_import_by_name(self, rm, from_file): - filename = "io.test.apps.test_application" - murano_repo_url = "http://127.0.0.1" - - pkg = make_pkg({'FullName': filename}) - from_file.return_value = mc_utils.Package(mc_utils.File(pkg)) - - rm.get(murano_repo_url + '/apps/' + filename + '.zip', - body=make_pkg({'FullName': 'first_app'})) - - arglist = [filename, '--murano-repo-url', murano_repo_url] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - self.assertTrue(self.package_mock.create.called) - self.package_mock.create.assert_called_once_with( - {'is_public': False}, - {filename: mock.ANY}, - ) - - @requests_mock.mock() - def test_package_import_multiple(self, rm): - filename = ["io.test.apps.test_application", - "http://127.0.0.1/test_app2.zip", ] - murano_repo_url = "http://127.0.0.1" - - rm.get(murano_repo_url + '/apps/' + filename[0] + '.zip', - body=make_pkg({'FullName': 'first_app'})) - - rm.get(filename[1], - body=make_pkg({'FullName': 'second_app'})) - - arglist = [filename[0], filename[1], - '--murano-repo-url', murano_repo_url] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - self.assertEqual(2, self.package_mock.create.call_count) - - self.package_mock.create.assert_has_calls( - [ - mock.call({'is_public': False}, {'first_app': mock.ANY}), - mock.call({'is_public': False}, {'second_app': mock.ANY}), - ], any_order=True, - ) - - -class TestBundleImport(TestPackage): - def setUp(self): - super(TestBundleImport, self).setUp() - - # Command to test - self.cmd = osc_pkg.ImportBundle(self.app, None) - - @requests_mock.mock() - def test_import_bundle_by_name(self, m): - """Asserts bundle import calls packages create once for each pkg.""" - pkg1 = make_pkg({'FullName': 'first_app'}) - pkg2 = make_pkg({'FullName': 'second_app'}) - - murano_repo_url = "http://127.0.0.1" - - m.get(murano_repo_url + '/apps/first_app.zip', body=pkg1) - m.get(murano_repo_url + '/apps/second_app.1.0.zip', - body=pkg2) - s = io.StringIO() - bundle_contents = {'Packages': [ - {'Name': 'first_app'}, - {'Name': 'second_app', 'Version': '1.0'} - ]} - json.dump(bundle_contents, s) - s = io.BytesIO(s.getvalue().encode('ascii')) - - m.get(murano_repo_url + '/bundles/test_bundle.bundle', - body=s) - - arglist = ["test_bundle", '--murano-repo-url', murano_repo_url] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - self.package_mock.create.assert_has_calls( - [ - mock.call({'is_public': False}, {'first_app': mock.ANY}), - mock.call({'is_public': False}, {'second_app': mock.ANY}), - ], any_order=True, - ) - - @requests_mock.mock() - def test_import_bundle_wrong_url(self, m): - url = 'http://127.0.0.2/test_bundle.bundle' - m.get(url, status_code=404) - - arglist = [url] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - self.assertFalse(self.package_mock.packages.create.called) - - @requests_mock.mock() - def test_import_bundle_no_bundle(self, m): - url = 'http://127.0.0.1/bundles/test_bundle.bundle' - m.get(url, status_code=404) - - arglist = ["test_bundle"] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - self.assertFalse(self.package_mock.packages.create.called) - - @requests_mock.mock() - def test_import_bundle_by_url(self, m): - """Asserts bundle import calls packages create once for each pkg.""" - pkg1 = make_pkg({'FullName': 'first_app'}) - pkg2 = make_pkg({'FullName': 'second_app'}) - - murano_repo_url = 'http://127.0.0.1' - m.get(murano_repo_url + '/apps/first_app.zip', body=pkg1) - m.get(murano_repo_url + '/apps/second_app.1.0.zip', - body=pkg2) - s = io.StringIO() - bundle_contents = {'Packages': [ - {'Name': 'first_app'}, - {'Name': 'second_app', 'Version': '1.0'} - ]} - json.dump(bundle_contents, s) - s = io.BytesIO(s.getvalue().encode('ascii')) - - url = 'http://127.0.0.2/test_bundle.bundle' - m.get(url, body=s) - - arglist = [url, '--murano-repo-url', murano_repo_url] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - self.package_mock.create.assert_has_calls( - [ - mock.call({'is_public': False}, {'first_app': mock.ANY}), - mock.call({'is_public': False}, {'second_app': mock.ANY}), - ], any_order=True, - ) - - @requests_mock.mock() - def test_import_local_bundle(self, m): - """Asserts local bundles are first searched locally.""" - tmp_dir = tempfile.mkdtemp() - bundle_file = os.path.join(tmp_dir, 'bundle.bundle') - with open(os.path.join(tmp_dir, 'bundle.bundle'), 'w') as f: - - bundle_contents = {'Packages': [ - {'Name': 'first_app'}, - {'Name': 'second_app', 'Version': '1.0'} - ]} - json.dump(bundle_contents, f) - - pkg1 = make_pkg({'FullName': 'first_app', - 'Require': {'third_app': None}}) - pkg2 = make_pkg({'FullName': 'second_app'}) - pkg3 = make_pkg({'FullName': 'third_app'}) - with open(os.path.join(tmp_dir, 'first_app'), 'wb') as f: - f.write(pkg1.read()) - with open(os.path.join(tmp_dir, 'third_app'), 'wb') as f: - f.write(pkg3.read()) - - murano_repo_url = "http://127.0.0.1" - m.get(murano_repo_url + '/apps/first_app.zip', - status_code=404) - m.get(murano_repo_url + '/apps/second_app.1.0.zip', - body=pkg2) - m.get(murano_repo_url + '/apps/third_app.zip', - status_code=404) - - arglist = [bundle_file, '--murano-repo-url', murano_repo_url] - parsed_args = self.check_parser(self.cmd, arglist, []) - self.cmd.take_action(parsed_args) - - self.package_mock.create.assert_has_calls( - [ - mock.call({'is_public': False}, {'first_app': mock.ANY}), - mock.call({'is_public': False}, {'second_app': mock.ANY}), - mock.call({'is_public': False}, {'third_app': mock.ANY}), - ], any_order=True, - ) - shutil.rmtree(tmp_dir) - - -class TestShowPackage(TestPackage): - def setUp(self): - super(TestShowPackage, self).setUp() - - # Command to test - self.cmd = osc_pkg.ShowPackage(self.app, None) - - def test_package_show(self): - arglist = ['fake'] - verifylist = [('id', 'fake')] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - - # Check that columns are correct - expected_columns = ('categories', 'class_definitions', 'description', - 'enabled', 'fully_qualified_name', 'id', - 'is_public', 'name', 'owner_id', 'tags', 'type') - self.assertEqual(expected_columns, columns) - - self.package_mock.get.assert_called_with('fake') - - -class TestUpdatePackage(TestPackage): - def setUp(self): - super(TestUpdatePackage, self).setUp() - - self.package_mock.update.return_value = \ - (mock.MagicMock(), mock.MagicMock()) - - # Command to test - self.cmd = osc_pkg.UpdatePackage(self.app, None) - - def test_package_update(self): - arglist = ['123', '--is-public', 'true'] - verifylist = [('id', '123'), ('is_public', True)] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - - self.package_mock.update.assert_called_with('123', {'is_public': True}) - - arglist = ['123', '--enabled', 'true'] - verifylist = [('id', '123'), ('enabled', True)] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - - self.package_mock.update.assert_called_with('123', {'enabled': True}) - - arglist = ['123', '--name', 'foo', '--description', 'bar'] - verifylist = [('id', '123'), ('name', 'foo'), ('description', 'bar')] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - - self.package_mock.update.assert_called_with( - '123', {'name': 'foo', 'description': 'bar'}) - - arglist = ['123', '--tags', 'foo'] - verifylist = [('id', '123'), ('tags', ['foo'])] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - self.cmd.take_action(parsed_args) - - self.package_mock.update.assert_called_with( - '123', {'tags': ['foo']}) - - -class TestDownloadPackage(TestPackage): - def setUp(self): - super(TestDownloadPackage, self).setUp() - - self.package_mock.download.return_value = \ - b'This is a fake package buffer' - - # Command to test - self.cmd = osc_pkg.DownloadPackage(self.app, None) - - def test_package_download(self): - arglist = ['1234', '/tmp/foo.zip'] - verifylist = [('id', '1234'), ('filename', '/tmp/foo.zip')] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - self.cmd.take_action(parsed_args) - - self.package_mock.download.assert_called_with('1234') diff --git a/muranoclient/tests/unit/osc/v1/test_schema.py b/muranoclient/tests/unit/osc/v1/test_schema.py deleted file mode 100644 index c8fbb909..00000000 --- a/muranoclient/tests/unit/osc/v1/test_schema.py +++ /dev/null @@ -1,54 +0,0 @@ -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -from muranoclient.osc.v1 import schema as osc_schema -from muranoclient.tests.unit.osc.v1 import fakes -from muranoclient.v1 import schemas as api_schemas - -SAMPLE_CLASS_SCHEMA = { - '': { - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "properties": {} - }, - 'modelBuilder': { - "$schema": "http://json-schema.org/draft-04/schema#", - "type": "object", - "properties": {} - } -} - - -class TestSchema(fakes.TestApplicationCatalog): - def setUp(self): - super(TestSchema, self).setUp() - self.schemas_mock = \ - self.app.client_manager.application_catalog.schemas - self.schemas_mock.get.return_value = api_schemas.Schema( - None, SAMPLE_CLASS_SCHEMA) - self.cmd = osc_schema.ShowSchema(self.app, None) - - def test_query_class_schema(self): - arglist = ['class.name', 'methodName1', - '--package-name', 'package.name', - '--class-version', '>1'] - verifylist = [('class_name', 'class.name'), - ('method_names', ['methodName1']), - ('package_name', 'package.name'), - ('class_version', '>1')] - - parsed_args = self.check_parser(self.cmd, arglist, verifylist) - - columns, data = self.cmd.take_action(parsed_args) - expected_columns = ['', 'modelBuilder'] - self.assertCountEqual(expected_columns, columns) - self.assertCountEqual(tuple(SAMPLE_CLASS_SCHEMA.values()), data) diff --git a/muranoclient/tests/unit/test_base.py b/muranoclient/tests/unit/test_base.py deleted file mode 100644 index 87ccd742..00000000 --- a/muranoclient/tests/unit/test_base.py +++ /dev/null @@ -1,46 +0,0 @@ -# Copyright 2015 Huawei. -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import testtools - -from muranoclient.common import base -from muranoclient.v1 import packages - - -class BaseTest(testtools.TestCase): - - def test_two_resources_with_same_id_are_not_equal(self): - # Two resources with same ID: never equal if their info is not equal - r1 = base.Resource(None, {'id': 1, 'name': 'hi'}) - r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) - self.assertNotEqual(r1, r2) - - def test_two_resources_with_same_id_and_info_are_equal(self): - # Two resources with same ID: equal if their info is equal - r1 = base.Resource(None, {'id': 1, 'name': 'hello'}) - r2 = base.Resource(None, {'id': 1, 'name': 'hello'}) - self.assertEqual(r1, r2) - - def test_two_resources_with_diff_type_are_not_equal(self): - # Two resources of different types: never equal - r1 = base.Resource(None, {'id': 1}) - r2 = packages.Package(None, {'id': 1}) - self.assertNotEqual(r1, r2) - - def test_two_resources_with_no_id_are_equal(self): - # Two resources with no ID: equal if their info is equal - r1 = base.Resource(None, {'name': 'joe', 'age': 12}) - r2 = base.Resource(None, {'name': 'joe', 'age': 12}) - self.assertEqual(r1, r2) diff --git a/muranoclient/tests/unit/test_common_http.py b/muranoclient/tests/unit/test_common_http.py deleted file mode 100644 index 69a03f8a..00000000 --- a/muranoclient/tests/unit/test_common_http.py +++ /dev/null @@ -1,463 +0,0 @@ -# -*- coding:utf-8 -*- -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -import socket -from unittest import mock - -import testtools - -from muranoclient.common import exceptions as exc -from muranoclient.common import http -from muranoclient.tests.unit import fakes - - -@mock.patch('muranoclient.common.http.requests.request') -class HttpClientTest(testtools.TestCase): - - def test_http_raw_request(self, mock_request): - headers = {'User-Agent': 'python-muranoclient'} - mock_request.return_value = \ - fakes.FakeHTTPResponse( - 200, 'OK', - {}, - '') - - client = http.HTTPClient('http://example.com:8082') - resp = client.request('', 'GET') - self.assertEqual(200, resp.status_code) - self.assertEqual('', ''.join([x for x in resp.content])) - mock_request.assert_called_with('GET', 'http://example.com:8082', - allow_redirects=False, - headers=headers) - - def test_token_or_credentials(self, mock_request): - # Record a 200 - fake200 = fakes.FakeHTTPResponse( - 200, 'OK', - {}, - '') - - mock_request.side_effect = [fake200, fake200, fake200] - - # Replay, create client, assert - client = http.HTTPClient('http://example.com:8082') - resp = client.request('', 'GET') - self.assertEqual(200, resp.status_code) - - client.username = 'user' - client.password = 'pass' - resp = client.request('', 'GET') - self.assertEqual(200, resp.status_code) - - client.auth_token = 'abcd1234' - resp = client.request('', 'GET') - self.assertEqual(200, resp.status_code) - - # no token or credentials - mock_request.assert_has_calls([ - mock.call('GET', 'http://example.com:8082', - allow_redirects=False, - headers={'User-Agent': 'python-muranoclient'}), - mock.call('GET', 'http://example.com:8082', - allow_redirects=False, - headers={'User-Agent': 'python-muranoclient', - 'X-Auth-Key': 'pass', - 'X-Auth-User': 'user'}), - mock.call('GET', 'http://example.com:8082', - allow_redirects=False, - headers={'User-Agent': 'python-muranoclient', - 'X-Auth-Token': 'abcd1234'}) - ]) - - def test_region_name(self, mock_request): - # Record a 200 - fake200 = fakes.FakeHTTPResponse( - 200, 'OK', - {}, - '') - - mock_request.return_value = fake200 - - client = http.HTTPClient('http://example.com:8082') - client.region_name = 'RegionOne' - resp = client.request('', 'GET') - self.assertEqual(200, resp.status_code) - - mock_request.assert_called_once_with( - 'GET', 'http://example.com:8082', - allow_redirects=False, - headers={'X-Region-Name': 'RegionOne', - 'User-Agent': 'python-muranoclient'}) - - def test_http_json_request(self, mock_request): - # Record a 200 - mock_request.return_value = \ - fakes.FakeHTTPResponse( - 200, 'OK', - {'content-type': 'application/json'}, - '{}') - client = http.HTTPClient('http://example.com:8082') - resp, body = client.json_request('', 'GET') - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - - mock_request.assert_called_once_with( - 'GET', 'http://example.com:8082', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - - def test_http_json_request_argument_passed_to_requests(self, mock_request): - """Check that we have sent the proper arguments to requests.""" - # Record a 200 - mock_request.return_value = \ - fakes.FakeHTTPResponse( - 200, 'OK', - {'content-type': 'application/json'}, - '{}') - - client = http.HTTPClient('http://example.com:8082') - client.verify_cert = True - client.cert_file = 'RANDOM_CERT_FILE' - client.key_file = 'RANDOM_KEY_FILE' - client.auth_url = 'http://AUTH_URL' - resp, body = client.json_request('', 'GET', data='text') - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - - mock_request.assert_called_once_with( - 'GET', 'http://example.com:8082', - allow_redirects=False, - cert=('RANDOM_CERT_FILE', 'RANDOM_KEY_FILE'), - verify=True, - data='"text"', - headers={'Content-Type': 'application/json', - 'X-Auth-Url': 'http://AUTH_URL', - 'User-Agent': 'python-muranoclient'}) - - def test_http_json_request_w_req_body(self, mock_request): - # Record a 200 - mock_request.return_value = \ - fakes.FakeHTTPResponse( - 200, 'OK', - {'content-type': 'application/json'}, - '{}') - - client = http.HTTPClient('http://example.com:8082') - resp, body = client.json_request('', 'GET', data='test-body') - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - mock_request.assert_called_once_with( - 'GET', 'http://example.com:8082', - data='"test-body"', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - - def test_http_json_request_non_json_resp_cont_type(self, mock_request): - # Record a 200 - mock_request.return_value = \ - fakes.FakeHTTPResponse( - 200, 'OK', - {'content-type': 'not/json'}, - '{}') - - client = http.HTTPClient('http://example.com:8082') - resp, body = client.json_request('', 'GET', data='test-data') - self.assertEqual(200, resp.status_code) - self.assertIsNone(body) - mock_request.assert_called_once_with( - 'GET', 'http://example.com:8082', data='"test-data"', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - - def test_http_json_request_invalid_json(self, mock_request): - # Record a 200 - mock_request.return_value = \ - fakes.FakeHTTPResponse( - 200, 'OK', - {'content-type': 'application/json'}, - 'invalid-json') - - client = http.HTTPClient('http://example.com:8082') - resp, body = client.json_request('', 'GET') - self.assertEqual(200, resp.status_code) - self.assertEqual('invalid-json', body) - mock_request.assert_called_once_with( - 'GET', 'http://example.com:8082', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - - def test_http_manual_redirect_delete(self, mock_request): - mock_request.side_effect = [ - fakes.FakeHTTPResponse( - 302, 'Found', - {'location': 'http://example.com:8082/foo/bar'}, - ''), - fakes.FakeHTTPResponse( - 200, 'OK', - {'content-type': 'application/json'}, - '{}')] - - client = http.HTTPClient('http://example.com:8082/foo') - resp, body = client.json_request('', 'DELETE') - - self.assertEqual(200, resp.status_code) - mock_request.assert_has_calls([ - mock.call('DELETE', 'http://example.com:8082/foo', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}), - mock.call('DELETE', 'http://example.com:8082/foo/bar', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - ]) - - def test_http_manual_redirect_post(self, mock_request): - mock_request.side_effect = [ - fakes.FakeHTTPResponse( - 302, 'Found', - {'location': 'http://example.com:8082/foo/bar'}, - ''), - fakes.FakeHTTPResponse( - 200, 'OK', - {'content-type': 'application/json'}, - '{}')] - - client = http.HTTPClient('http://example.com:8082/foo') - resp, body = client.json_request('', 'POST') - - self.assertEqual(200, resp.status_code) - mock_request.assert_has_calls([ - mock.call('POST', 'http://example.com:8082/foo', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}), - mock.call('POST', 'http://example.com:8082/foo/bar', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - ]) - - def test_http_manual_redirect_put(self, mock_request): - mock_request.side_effect = [ - fakes.FakeHTTPResponse( - 302, 'Found', - {'location': 'http://example.com:8082/foo/bar'}, - ''), - fakes.FakeHTTPResponse( - 200, 'OK', - {'content-type': 'application/json'}, - '{}')] - - client = http.HTTPClient('http://example.com:8082/foo') - resp, body = client.json_request('', 'PUT') - - self.assertEqual(200, resp.status_code) - mock_request.assert_has_calls([ - mock.call('PUT', 'http://example.com:8082/foo', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}), - mock.call('PUT', 'http://example.com:8082/foo/bar', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - ]) - - def test_http_manual_redirect_prohibited(self, mock_request): - mock_request.return_value = \ - fakes.FakeHTTPResponse( - 302, 'Found', - {'location': 'http://example.com:8082/'}, - '') - client = http.HTTPClient('http://example.com:8082/foo') - self.assertRaises(exc.InvalidEndpoint, - client.json_request, '', 'DELETE') - mock_request.assert_called_once_with( - 'DELETE', 'http://example.com:8082/foo', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - - def test_http_manual_redirect_error_without_location(self, mock_request): - mock_request.return_value = \ - fakes.FakeHTTPResponse( - 302, 'Found', - {}, - '') - client = http.HTTPClient('http://example.com:8082/foo') - self.assertRaises(exc.InvalidEndpoint, - client.json_request, '', 'DELETE') - mock_request.assert_called_once_with( - 'DELETE', 'http://example.com:8082/foo', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - - def test_http_json_request_redirect(self, mock_request): - # Record the 302 - mock_request.side_effect = [ - fakes.FakeHTTPResponse( - 302, 'Found', - {'location': 'http://example.com:8082'}, - ''), - fakes.FakeHTTPResponse( - 200, 'OK', - {'content-type': 'application/json'}, - '{}')] - - client = http.HTTPClient('http://example.com:8082') - resp, body = client.json_request('', 'GET') - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - - mock_request.assert_has_calls([ - mock.call('GET', 'http://example.com:8082', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}), - mock.call('GET', 'http://example.com:8082', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - ]) - - def test_http_404_json_request(self, mock_request): - mock_request.return_value = \ - fakes.FakeHTTPResponse( - 404, 'Not Found', {'content-type': 'application/json'}, - '{}') - - client = http.HTTPClient('http://example.com:8082') - e = self.assertRaises(exc.HTTPNotFound, client.json_request, '', 'GET') - # Assert that the raised exception can be converted to string - self.assertIsNotNone(str(e)) - # Record a 404 - mock_request.assert_called_once_with( - 'GET', 'http://example.com:8082', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - - def test_http_300_json_request(self, mock_request): - mock_request.return_value = \ - fakes.FakeHTTPResponse( - 300, 'OK', {'content-type': 'application/json'}, - '{}') - client = http.HTTPClient('http://example.com:8082') - e = self.assertRaises( - exc.HTTPMultipleChoices, client.json_request, '', 'GET') - # Assert that the raised exception can be converted to string - self.assertIsNotNone(str(e)) - - # Record a 300 - mock_request.assert_called_once_with( - 'GET', 'http://example.com:8082', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}) - - def test_fake_json_request(self, mock_request): - headers = {'User-Agent': 'python-muranoclient'} - mock_request.side_effect = [socket.gaierror] - - client = http.HTTPClient('fake://example.com:8082') - self.assertRaises(exc.InvalidEndpoint, - client.request, "/", "GET") - mock_request.assert_called_once_with('GET', 'fake://example.com:8082/', - allow_redirects=False, - headers=headers) - - def test_http_request_socket_error(self, mock_request): - headers = {'User-Agent': 'python-muranoclient'} - mock_request.side_effect = [socket.gaierror] - - client = http.HTTPClient('http://example.com:8082') - self.assertRaises(exc.InvalidEndpoint, - client.request, "/", "GET") - mock_request.assert_called_once_with('GET', 'http://example.com:8082/', - allow_redirects=False, - headers=headers) - - def test_http_request_socket_timeout(self, mock_request): - headers = {'User-Agent': 'python-muranoclient'} - mock_request.side_effect = [socket.timeout] - - client = http.HTTPClient('http://example.com:8082') - self.assertRaises(exc.CommunicationError, - client.request, "/", "GET") - mock_request.assert_called_once_with('GET', 'http://example.com:8082/', - allow_redirects=False, - headers=headers) - - def test_http_request_specify_timeout(self, mock_request): - mock_request.return_value = \ - fakes.FakeHTTPResponse( - 200, 'OK', - {'content-type': 'application/json'}, - '{}') - - client = http.HTTPClient('http://example.com:8082', timeout='123') - resp, body = client.json_request('', 'GET') - self.assertEqual(200, resp.status_code) - self.assertEqual({}, body) - mock_request.assert_called_once_with( - 'GET', 'http://example.com:8082', - allow_redirects=False, - headers={'Content-Type': 'application/json', - 'User-Agent': 'python-muranoclient'}, - timeout=float(123)) - - def test_get_system_ca_file(self, mock_request): - chosen = '/etc/ssl/certs/ca-certificates.crt' - with mock.patch('os.path.exists') as mock_os: - mock_os.return_value = chosen - - ca = http.get_system_ca_file() - self.assertEqual(chosen, ca) - - mock_os.assert_called_once_with(chosen) - - def test_insecure_verify_cert_None(self, mock_request): - client = http.HTTPClient('https://foo', insecure=True) - self.assertFalse(client.verify_cert) - - def test_passed_cert_to_verify_cert(self, mock_request): - client = http.HTTPClient('https://foo', cacert="NOWHERE") - self.assertEqual("NOWHERE", client.verify_cert) - - with mock.patch('muranoclient.common.http.get_system_ca_file') as gsf: - gsf.return_value = "SOMEWHERE" - client = http.HTTPClient('https://foo') - self.assertEqual("SOMEWHERE", client.verify_cert) - -# def test_curl_log_i18n_headers(self, mock_request): -# self.m.StubOutWithMock(logging.Logger, 'debug') -# kwargs = {'headers': {'Key': b'foo\xe3\x8a\x8e'}} -# -# mock_logging_debug = logging.Logger.debug( -# u"curl -i -X GET -H 'Key: foo㊎' http://somewhere" -# ) -# mock_logging_debug.AndReturn(None) -# -# self.m.ReplayAll() -# -# client = http.HTTPClient('http://somewhere') -# client.log_curl_request('', "GET", kwargs=kwargs) -# -# self.m.VerifyAll() diff --git a/muranoclient/tests/unit/test_exc.py b/muranoclient/tests/unit/test_exc.py deleted file mode 100644 index 41ea7116..00000000 --- a/muranoclient/tests/unit/test_exc.py +++ /dev/null @@ -1,63 +0,0 @@ -# Copyright 2012 OpenStack Foundation -# All Rights Reserved. -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may -# not use this file except in compliance with the License. You may obtain -# a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. - -import testtools -from unittest import mock - -from muranoclient.common import exceptions as exc - -HTML_MSG = """ - - 403 Forbidden - - -

403 Forbidden

- Access was denied to this resource.

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